feat(header): session tabs — a horizontal working set of open sessions

Web/desktop header replaces the single session title with a strip of
soft pill tabs, one per session the user has opened (sidebar, palette
or deep link — opening anywhere adds a tab once). The active tab is the
familiar title block — rename, meta row and the full session menu —
inside a gently selected pill; a brand-new draft shows as a transient
pill until its session exists. Inactive tabs show the title with a
hover-revealed "..." menu (close tab, close other tabs, copy id) that
nudges the text like sidebar rows, and close by middle-click too.

Tabs drag to reorder, scroll behind the right-side header buttons with
soft fade edges, respect the reserved window-controls inset, and
persist across reloads. Closing the active tab activates its neighbour
(or opens a new draft when it was the last). Tab ids whose session is
not in the loaded list stay stored but hidden, so a partial session
list never destroys the working set. VS Code keeps the plain title;
mobile is untouched.
This commit is contained in:
Bohdan Triapitsyn
2026-08-24 17:07:09 +03:00
parent 7bd27d44d6
commit a3813f57e9
15 changed files with 645 additions and 1 deletions
+4
View File
@@ -419,6 +419,10 @@ export const dict = {
'sessions.sidebar.activity.chatsEmpty': 'Noch keine Chats.',
'chat.chatInput.chooseProject': 'Projekt auswählen',
'sessions.switcher.openAria': 'Sitzungswechsler öffnen',
'header.sessionTabs.stripAria': 'Offene Sitzungen',
'header.sessionTabs.tabMenuAria': 'Aktionen für den Sitzungs-Tab',
'header.sessionTabs.closeTab': 'Tab schließen',
'header.sessionTabs.closeOtherTabs': 'Andere Tabs schließen',
'sessions.switcher.empty': 'Keine kürzlichen Sitzungen',
'sessions.switcher.draftTitle': 'Neue Sitzung',
'sessions.sidebar.updateCheck.errorTitle': 'Fehler beim Prüfen auf Aktualisierungen',
+4
View File
@@ -481,6 +481,10 @@ export const dict = {
'sessions.archivePage.deleteSessionAria': 'Delete {title}',
'sessions.archivePage.restoreSessionAria': 'Restore {title}',
'sessions.switcher.openAria': 'Open session switcher',
'header.sessionTabs.stripAria': 'Open sessions',
'header.sessionTabs.tabMenuAria': 'Session tab actions',
'header.sessionTabs.closeTab': 'Close tab',
'header.sessionTabs.closeOtherTabs': 'Close other tabs',
'sessions.switcher.empty': 'No recent sessions',
'sessions.switcher.draftTitle': 'New session',
'sessions.sidebar.updateCheck.errorTitle': 'Failed to check for updates',
+4
View File
@@ -482,6 +482,10 @@ export const dict: Record<I18nKey, string> = {
"sessions.archivePage.deleteSessionAria": "Eliminar {title}",
"sessions.archivePage.restoreSessionAria": "Restaurar {title}",
"sessions.switcher.openAria": "Abrir selector de sesiones",
"header.sessionTabs.stripAria": "Sesiones abiertas",
"header.sessionTabs.tabMenuAria": "Acciones de la pestaña de sesión",
"header.sessionTabs.closeTab": "Cerrar pestaña",
"header.sessionTabs.closeOtherTabs": "Cerrar las demás pestañas",
"sessions.switcher.empty": "No hay sesiones recientes",
"sessions.switcher.draftTitle": "Nueva sesión",
"sessions.sidebar.updateCheck.errorTitle": "No se pudo comprobar actualizaciones",
+4
View File
@@ -312,6 +312,10 @@ export const dict = {
'sessions.archivePage.deleteSessionAria': 'Supprimer {title}',
'sessions.archivePage.restoreSessionAria': 'Restaurer {title}',
'sessions.switcher.openAria': 'Sélecteur de session ouvert',
'header.sessionTabs.stripAria': 'Sessions ouvertes',
'header.sessionTabs.tabMenuAria': 'Actions de l\'onglet de session',
'header.sessionTabs.closeTab': 'Fermer l\'onglet',
'header.sessionTabs.closeOtherTabs': 'Fermer les autres onglets',
'sessions.switcher.empty': 'Aucune session récente',
'sessions.switcher.draftTitle': 'Nouvelle session',
'sessions.sidebar.updateCheck.errorTitle': 'Échec de la vérification des mises à jour',
+4
View File
@@ -482,6 +482,10 @@ export const dict: Record<I18nKey, string> = {
'sessions.archivePage.deleteSessionAria': '{title} を削除',
'sessions.archivePage.restoreSessionAria': '{title} を復元',
'sessions.switcher.openAria': 'セッションスイッチャーを開く',
'header.sessionTabs.stripAria': '開いているセッション',
'header.sessionTabs.tabMenuAria': 'セッションタブの操作',
'header.sessionTabs.closeTab': 'タブを閉じる',
'header.sessionTabs.closeOtherTabs': '他のタブを閉じる',
'sessions.switcher.empty': '最近のセッションはありません',
'sessions.switcher.draftTitle': '新しいセッション',
'sessions.sidebar.updateCheck.errorTitle': '更新の確認に失敗しました',
+4
View File
@@ -482,6 +482,10 @@ export const dict: Record<I18nKey, string> = {
'sessions.archivePage.deleteSessionAria': '{title} 삭제',
'sessions.archivePage.restoreSessionAria': '{title} 복원',
'sessions.switcher.openAria': '세션 전환기 열기',
'header.sessionTabs.stripAria': '열린 세션',
'header.sessionTabs.tabMenuAria': '세션 탭 작업',
'header.sessionTabs.closeTab': '탭 닫기',
'header.sessionTabs.closeOtherTabs': '다른 탭 닫기',
'sessions.switcher.empty': '최근 세션 없음',
'sessions.switcher.draftTitle': '새 세션',
'sessions.sidebar.updateCheck.errorTitle': '업데이트 확인 실패',
+4
View File
@@ -293,6 +293,10 @@ export const dict: Record<I18nKey, string> = {
'sessions.archivePage.deleteSessionAria': 'Usuń {title}',
'sessions.archivePage.restoreSessionAria': 'Przywróć {title}',
'sessions.switcher.openAria': 'Otwórz przełącznik sesji',
'header.sessionTabs.stripAria': 'Otwarte sesje',
'header.sessionTabs.tabMenuAria': 'Akcje karty sesji',
'header.sessionTabs.closeTab': 'Zamknij kartę',
'header.sessionTabs.closeOtherTabs': 'Zamknij pozostałe karty',
'sessions.switcher.empty': 'Brak ostatnich sesji',
'sessions.switcher.draftTitle': 'Nowa sesja',
'sessions.sidebar.updateCheck.errorTitle': 'Nie udało się sprawdzić aktualizacji',
@@ -482,6 +482,10 @@ export const dict: Record<I18nKey, string> = {
"sessions.archivePage.deleteSessionAria": "Excluir {title}",
"sessions.archivePage.restoreSessionAria": "Restaurar {title}",
"sessions.switcher.openAria": "Abrir seletor de sessões",
"header.sessionTabs.stripAria": "Sessões abertas",
"header.sessionTabs.tabMenuAria": "Ações da aba de sessão",
"header.sessionTabs.closeTab": "Fechar aba",
"header.sessionTabs.closeOtherTabs": "Fechar outras abas",
"sessions.switcher.empty": "Nenhuma sessão recente",
"sessions.switcher.draftTitle": "Nova sessão",
"sessions.sidebar.updateCheck.errorTitle": "Não foi possível verificar atualizações",
+4
View File
@@ -482,6 +482,10 @@ export const dict: Record<I18nKey, string> = {
"sessions.archivePage.deleteSessionAria": "Видалити {title}",
"sessions.archivePage.restoreSessionAria": "Відновити {title}",
"sessions.switcher.openAria": "Відкрити перемикач сесій",
"header.sessionTabs.stripAria": "Відкриті сесії",
"header.sessionTabs.tabMenuAria": "Дії вкладки сесії",
"header.sessionTabs.closeTab": "Закрити вкладку",
"header.sessionTabs.closeOtherTabs": "Закрити інші вкладки",
"sessions.switcher.empty": "Немає недавніх сесій",
"sessions.switcher.draftTitle": "Нова сесія",
"sessions.sidebar.updateCheck.errorTitle": "Не вдалося перейти на наявність оновлень",
@@ -482,6 +482,10 @@ export const dict: Record<I18nKey, string> = {
'sessions.archivePage.deleteSessionAria': '删除 {title}',
'sessions.archivePage.restoreSessionAria': '还原 {title}',
'sessions.switcher.openAria': '打开会话切换器',
'header.sessionTabs.stripAria': '打开的会话',
'header.sessionTabs.tabMenuAria': '会话标签页操作',
'header.sessionTabs.closeTab': '关闭标签页',
'header.sessionTabs.closeOtherTabs': '关闭其他标签页',
'sessions.switcher.empty': '没有最近会话',
'sessions.switcher.draftTitle': '新会话',
'sessions.sidebar.updateCheck.errorTitle': '检查更新失败',
@@ -495,6 +495,10 @@ export const dict: Record<I18nKey, string> = {
'sessions.archivePage.deleteSessionAria': '刪除 {title}',
'sessions.archivePage.restoreSessionAria': '還原 {title}',
'sessions.switcher.openAria': '開啟會話切換器',
'header.sessionTabs.stripAria': '開啟的會話',
'header.sessionTabs.tabMenuAria': '工作階段分頁動作',
'header.sessionTabs.closeTab': '關閉分頁',
'header.sessionTabs.closeOtherTabs': '關閉其他分頁',
'sessions.switcher.empty': '沒有最近會話',
'sessions.switcher.draftTitle': '新會話',
'sessions.sidebar.updateCheck.errorTitle': '檢查更新失敗',