diff --git a/src/client/Sidebar.tsx b/src/client/Sidebar.tsx index 97c8160..80f4022 100644 --- a/src/client/Sidebar.tsx +++ b/src/client/Sidebar.tsx @@ -37,7 +37,7 @@ import { appendToDraft } from './conversation-draft.ts' import { BOTTOM_MIN, PANEL_MIN, agentUuidOf, dockFloat, firstLeaf, floatTab, isAgentTabId, leafWithTab, migrateBottomTabs, moveFloat, moveTab, moveTabToEdge, openDiffTab, raiseFloat, reconcileAgentTerminals, - resizeFloat, resizeSplitIn, setBottomHeight, setWidth, toggleBottomPanel, toggleExpanded, togglePanel, + resizeFloat, resizeSplitIn, setBottomHeight, setTabPin, setWidth, toggleBottomPanel, toggleExpanded, togglePanel, type DropZone, type SidebarState, type SidebarStore, type SidebarTab, type SplitNode, } from './state.ts' import { IconPanelBottomOutline16, IconPanelRightOutline16 } from './icons.tsx' @@ -1203,6 +1203,13 @@ export function Sidebar(props: { ctx: Context; store: SidebarStore }) { const y = rect !== null ? (rect.top + rect.bottom) / 2 : window.innerHeight / 2 store.reduce(s => floatTab(s, tabId, x, y)) }, + // Pin/unpin a terminal tab (v0.17.0+): the home cwd is snapshotted at + // pin time so a workspace-scoped pin only resurfaces in sessions whose + // cwd matches. Unpin passes null — the tab stays open in its home + // session, just unmarked. + pinTab: (tabId, scope) => { + store.reduce(s => setTabPin(s, tabId, scope === null ? null : { scope, homeCwd: cwd })) + }, }), [store, sessionId, cwd]) /** diff --git a/src/client/TabBar.tsx b/src/client/TabBar.tsx index 476ca5b..c802095 100644 --- a/src/client/TabBar.tsx +++ b/src/client/TabBar.tsx @@ -14,6 +14,7 @@ import { IconCloseFill14, IconPlusOutline16, Menu, } from '@deepseek-ai/dsh-client-ui-primitives' import type { SidebarTab } from './state.ts' +import { isAgentTabId } from './state.ts' import { t } from './locales.ts' import css from './sidebar.module.css' @@ -67,6 +68,13 @@ export function TabBar(props: { /** Float a tab out as a free window (the tab context menu's entry; the * drag-to-conversation gesture is handled at the Sidebar shell level). */ onFloatTab: (tabId: string) => void + /** + * Pin/unpin a terminal tab (v0.17.0+). Called with `'workspace'` or + * `'global'` to pin (the shell snapshots the home cwd), or `null` to + * unpin. Non-terminal tabs never trigger this callback. Optional: the + * menu hides the pin entry when unset (legacy callers). + */ + onPinTab?: (tabId: string, scope: 'workspace' | 'global' | null) => void /** Icon resolver for tab labels (reads from the tab descriptor registry). */ getTabIcon?: (tab: SidebarTab) => ReactNode /** Badge resolver for tab labels (reads the descriptor's `badge`; the @@ -74,7 +82,7 @@ export function TabBar(props: { getTabBadge?: (tab: SidebarTab) => ReactNode }) { const { - paneId, tabs, active, onActivate, onClose, onNewTab, newTabOptions, onDropTab, onFloatTab, getTabIcon, getTabBadge, + paneId, tabs, active, onActivate, onClose, onNewTab, newTabOptions, onDropTab, onFloatTab, onPinTab, getTabIcon, getTabBadge, } = props const [menuOpen, setMenuOpen] = useState(false) // The tab right-click context menu: the target tab plus the cursor @@ -274,13 +282,35 @@ export function TabBar(props: { = 0} onClose={() => { setTabMenu(null) }} - items={[ - { id: 'float', label: t('moveToFreeWindow') }, - { id: 'close', label: t('close') }, - { id: 'closeOthers', label: t('closeOtherTabs'), ...(tabs.length <= 1 ? { disabled: true } : {}) }, - { id: 'closeLeft', label: t('closeLeftTabs'), ...(tabMenuIndex <= 0 ? { disabled: true } : {}) }, - { id: 'closeRight', label: t('closeRightTabs'), ...(tabMenuIndex >= tabs.length - 1 ? { disabled: true } : {}) }, - ]} + items={(() => { + // The target tab drives the pin entry's shape: terminal tabs + // get either a "Pin ▸" submenu (unpinned) or a single "Unpin" + // row (pinned). Non-terminal tabs and missing onPinTab get no + // pin entry at all — the menu stays exactly the legacy 5-item + // shape. + const targetTab = tabMenuIndex >= 0 ? tabs[tabMenuIndex] : undefined + const isTerminal = targetTab?.type === 'terminal' + const pinEntries = isTerminal && onPinTab !== undefined + ? targetTab!.pin !== undefined + ? [{ id: 'unpin', label: t('unpinTerminal') }] + : [{ + id: 'pin', + label: isAgentTabId(targetTab!.id) ? t('pinAgentTerminal') : t('pinTerminal'), + submenu: [ + { id: 'pinWorkspace', label: t('pinToWorkspace') }, + { id: 'pinGlobal', label: t('pinToGlobal') }, + ], + }] + : [] + return [ + { id: 'float', label: t('moveToFreeWindow') }, + ...pinEntries, + { id: 'close', label: t('close') }, + { id: 'closeOthers', label: t('closeOtherTabs'), ...(tabs.length <= 1 ? { disabled: true } : {}) }, + { id: 'closeLeft', label: t('closeLeftTabs'), ...(tabMenuIndex <= 0 ? { disabled: true } : {}) }, + { id: 'closeRight', label: t('closeRightTabs'), ...(tabMenuIndex >= tabs.length - 1 ? { disabled: true } : {}) }, + ] + })()} onSelect={(id) => { const target = tabMenu if (target === null) return @@ -289,6 +319,12 @@ export function TabBar(props: { if (index < 0) return if (id === 'float') { onFloatTab(target.tabId) + } else if (id === 'pinWorkspace') { + onPinTab?.(target.tabId, 'workspace') + } else if (id === 'pinGlobal') { + onPinTab?.(target.tabId, 'global') + } else if (id === 'unpin') { + onPinTab?.(target.tabId, null) } else if (id === 'close') { onClose(target.tabId) } else if (id === 'closeOthers') { diff --git a/src/client/locales-ar.ts b/src/client/locales-ar.ts index 3431011..323513e 100644 --- a/src/client/locales-ar.ts +++ b/src/client/locales-ar.ts @@ -69,6 +69,18 @@ export const ar: Record = { moveToFreeWindow: 'النقل إلى نافذة حرة', floatDropHint: 'حرّك للفتح في نافذة حرة', dockToSidebar: 'العودة إلى الشريط الجانبي', + pinTerminal: 'تثبيت الطرفية', + pinAgentTerminal: 'تثبيت طرفية Agent', + pinToWorkspace: 'تثبيت إلى مساحة العمل', + pinToGlobal: 'تثبيت عام', + unpinTerminal: 'إلغاء التثبيت', + pinnedTerminalTooltip: '{kind} · {scope} · {cwd}', + pinnedTerminalKindUi: 'واجهة طرفية', + pinnedTerminalKindAgent: 'طرفية Agent', + pinnedTerminalScopeWorkspace: 'مثبتة في مساحة العمل', + pinnedTerminalScopeGlobal: 'مثبتة عام', + pinnedRailLabel: 'الطرفيات المثبتة', + closePinnedTerminal: 'إغلاق الطرفية', collapse: 'طي الشريط الجانبي', expand: 'توسيع الشريط الجانبي', collapseBottomPanel: 'طي اللوحة السفلية', diff --git a/src/client/locales-de.ts b/src/client/locales-de.ts index a73124e..3c945c3 100644 --- a/src/client/locales-de.ts +++ b/src/client/locales-de.ts @@ -54,6 +54,18 @@ export const de: Record = { moveToFreeWindow: 'In freies Fenster verschieben', floatDropHint: 'Loslassen, um in einem freien Fenster zu öffnen', dockToSidebar: 'Zurück zur Seitenleiste', + pinTerminal: 'Terminal anheften', + pinAgentTerminal: 'Agent-Terminal anheften', + pinToWorkspace: 'An Arbeitsbereich anheften', + pinToGlobal: 'Global anheften', + unpinTerminal: 'Lösen', + pinnedTerminalTooltip: '{kind} · {scope} · {cwd}', + pinnedTerminalKindUi: 'UI-Terminal', + pinnedTerminalKindAgent: 'Agent-Terminal', + pinnedTerminalScopeWorkspace: 'An Arbeitsbereich angeheftet', + pinnedTerminalScopeGlobal: 'Global angeheftet', + pinnedRailLabel: 'Angeheftete Terminals', + closePinnedTerminal: 'Terminal schließen', collapse: 'Seitenleiste einklappen', expand: 'Seitenleiste ausklappen', collapseBottomPanel: 'Unteres Panel einklappen', diff --git a/src/client/locales-fr.ts b/src/client/locales-fr.ts index eb645a6..6b0cc9f 100644 --- a/src/client/locales-fr.ts +++ b/src/client/locales-fr.ts @@ -61,6 +61,18 @@ export const fr: Record = { moveToFreeWindow: 'Déplacer vers la fenêtre flottante', floatDropHint: 'Relâchez pour ouvrir dans une fenêtre flottante', dockToSidebar: 'Revenir à la barre latérale', + pinTerminal: 'Épingler le terminal', + pinAgentTerminal: 'Épingler le terminal Agent', + pinToWorkspace: 'Épingler à l\'espace de travail', + pinToGlobal: 'Épingler globalement', + unpinTerminal: 'Désépingler', + pinnedTerminalTooltip: '{kind} · {scope} · {cwd}', + pinnedTerminalKindUi: 'Terminal UI', + pinnedTerminalKindAgent: 'Terminal Agent', + pinnedTerminalScopeWorkspace: 'Épinglé à l\'espace de travail', + pinnedTerminalScopeGlobal: 'Épinglé globalement', + pinnedRailLabel: 'Terminaux épinglés', + closePinnedTerminal: 'Fermer le terminal', collapse: 'Réduire la barre latérale', expand: 'Déployer la barre latérale', collapseBottomPanel: 'Réduire le panneau inférieur', diff --git a/src/client/locales-hi.ts b/src/client/locales-hi.ts index cf98156..9e44f39 100644 --- a/src/client/locales-hi.ts +++ b/src/client/locales-hi.ts @@ -68,6 +68,18 @@ export const hi: Record = { moveToFreeWindow: 'फ़्री विंडो में ले जाएँ', floatDropHint: 'फ़्री विंडो में खोलने के लिए छोड़ें', dockToSidebar: 'साइडबार पर वापस जाएँ', + pinTerminal: 'टर्मिनल पिन करें', + pinAgentTerminal: 'Agent टर्मिनल पिन करें', + pinToWorkspace: 'वर्कस्पेस पर पिन करें', + pinToGlobal: 'वैश्विक रूप से पिन करें', + unpinTerminal: 'पिन हटाएँ', + pinnedTerminalTooltip: '{kind} · {scope} · {cwd}', + pinnedTerminalKindUi: 'UI टर्मिनल', + pinnedTerminalKindAgent: 'Agent टर्मिनल', + pinnedTerminalScopeWorkspace: 'वर्कस्पेस पर पिन किया गया', + pinnedTerminalScopeGlobal: 'वैश्विक रूप से पिन किया गया', + pinnedRailLabel: 'पिन किए गए टर्मिनल', + closePinnedTerminal: 'टर्मिनल बंद करें', collapse: 'साइडबार संक्षिप्त करें', expand: 'साइडबार विस्तृत करें', collapseBottomPanel: 'निचला पैनल संक्षिप्त करें', diff --git a/src/client/locales-id.ts b/src/client/locales-id.ts index e14502f..9d150e8 100644 --- a/src/client/locales-id.ts +++ b/src/client/locales-id.ts @@ -66,6 +66,18 @@ export const id: Record = { moveToFreeWindow: 'Pindahkan ke jendela bebas', floatDropHint: 'Lepaskan untuk membuka di jendela bebas', dockToSidebar: 'Kembali ke bilah samping', + pinTerminal: 'Sematkan Terminal', + pinAgentTerminal: 'Sematkan Terminal Agent', + pinToWorkspace: 'Sematkan ke Workspace', + pinToGlobal: 'Sematkan Global', + unpinTerminal: 'Lepas Sematan', + pinnedTerminalTooltip: '{kind} · {scope} · {cwd}', + pinnedTerminalKindUi: 'Terminal UI', + pinnedTerminalKindAgent: 'Terminal Agent', + pinnedTerminalScopeWorkspace: 'Disematkan ke workspace', + pinnedTerminalScopeGlobal: 'Disematkan global', + pinnedRailLabel: 'Terminal Tersemat', + closePinnedTerminal: 'Tutup Terminal', collapse: 'Lipat sidebar', expand: 'Bentangkan sidebar', collapseBottomPanel: 'Lipat panel bawah', diff --git a/src/client/locales-it.ts b/src/client/locales-it.ts index dcff13f..160eb55 100644 --- a/src/client/locales-it.ts +++ b/src/client/locales-it.ts @@ -59,6 +59,18 @@ export const it: Record = { moveToFreeWindow: 'Sposta in finestra libera', floatDropHint: 'Rilascia per aprire in una finestra libera', dockToSidebar: 'Torna alla barra laterale', + pinTerminal: 'Blocca Terminale', + pinAgentTerminal: 'Blocca Terminale Agent', + pinToWorkspace: 'Blocca in Workspace', + pinToGlobal: 'Blocca Globalmente', + unpinTerminal: 'Sblocca', + pinnedTerminalTooltip: '{kind} · {scope} · {cwd}', + pinnedTerminalKindUi: 'Terminale UI', + pinnedTerminalKindAgent: 'Terminale Agent', + pinnedTerminalScopeWorkspace: 'Bloccato in workspace', + pinnedTerminalScopeGlobal: 'Bloccato globalmente', + pinnedRailLabel: 'Terminali Bloccati', + closePinnedTerminal: 'Chiudi Terminale', collapse: 'Comprimi barra laterale', expand: 'Espandi barra laterale', collapseBottomPanel: 'Comprimi pannello inferiore', diff --git a/src/client/locales-ja.ts b/src/client/locales-ja.ts index f984147..7ae7ccd 100644 --- a/src/client/locales-ja.ts +++ b/src/client/locales-ja.ts @@ -68,6 +68,18 @@ export const ja: Record = { moveToFreeWindow: 'フリーウィンドウへ移動', floatDropHint: '離すとフリーウィンドウで開きます', dockToSidebar: 'サイドバーへ戻す', + pinTerminal: 'ターミナルを固定', + pinAgentTerminal: 'Agent ターミナルを固定', + pinToWorkspace: 'ワークスペースに固定', + pinToGlobal: 'グローバルに固定', + unpinTerminal: '固定を解除', + pinnedTerminalTooltip: '{kind} · {scope} · {cwd}', + pinnedTerminalKindUi: 'UI ターミナル', + pinnedTerminalKindAgent: 'Agent ターミナル', + pinnedTerminalScopeWorkspace: 'ワークスペースに固定', + pinnedTerminalScopeGlobal: 'グローバルに固定', + pinnedRailLabel: '固定されたターミナル', + closePinnedTerminal: 'ターミナルを閉じる', collapse: 'サイドバーを折りたたむ', expand: 'サイドバーを展開', collapseBottomPanel: '下パネルを折りたたむ', diff --git a/src/client/locales-ko.ts b/src/client/locales-ko.ts index 9b8d241..86b26df 100644 --- a/src/client/locales-ko.ts +++ b/src/client/locales-ko.ts @@ -60,6 +60,18 @@ export const ko: Record = { moveToFreeWindow: '자유 창으로 이동', floatDropHint: '놓으면 자유 창에서 열립니다', dockToSidebar: '사이드바로 돌아가기', + pinTerminal: '터미널 고정', + pinAgentTerminal: 'Agent 터미널 고정', + pinToWorkspace: '워크스페이스에 고정', + pinToGlobal: '전역에 고정', + unpinTerminal: '고정 해제', + pinnedTerminalTooltip: '{kind} · {scope} · {cwd}', + pinnedTerminalKindUi: 'UI 터미널', + pinnedTerminalKindAgent: 'Agent 터미널', + pinnedTerminalScopeWorkspace: '워크스페이스에 고정됨', + pinnedTerminalScopeGlobal: '전역에 고정됨', + pinnedRailLabel: '고정된 터미널', + closePinnedTerminal: '터미널 닫기', collapse: '사이드바 접기', expand: '사이드바 펼치기', collapseBottomPanel: '하단 패널 접기', diff --git a/src/client/locales-nl.ts b/src/client/locales-nl.ts index dd5854f..aeabad3 100644 --- a/src/client/locales-nl.ts +++ b/src/client/locales-nl.ts @@ -66,6 +66,18 @@ export const nl: Record = { moveToFreeWindow: 'Naar vrij venster verplaatsen', floatDropHint: 'Loslaten om in een vrij venster te openen', dockToSidebar: 'Terug naar de zijbalk', + pinTerminal: 'Terminal vastmaken', + pinAgentTerminal: 'Agent-terminal vastmaken', + pinToWorkspace: 'Vastmaken aan werkruimte', + pinToGlobal: 'Wereldwijd vastmaken', + unpinTerminal: 'Losmaken', + pinnedTerminalTooltip: '{kind} · {scope} · {cwd}', + pinnedTerminalKindUi: 'UI-terminal', + pinnedTerminalKindAgent: 'Agent-terminal', + pinnedTerminalScopeWorkspace: 'Vastgemaakt aan werkruimte', + pinnedTerminalScopeGlobal: 'Wereldwijd vastgemaakt', + pinnedRailLabel: 'Vastgemaakte terminals', + closePinnedTerminal: 'Terminal sluiten', collapse: 'Zijbalk inklappen', expand: 'Zijbalk uitklappen', collapseBottomPanel: 'Onderpaneel inklappen', diff --git a/src/client/locales-pl.ts b/src/client/locales-pl.ts index 0b2fe4f..45ac98e 100644 --- a/src/client/locales-pl.ts +++ b/src/client/locales-pl.ts @@ -70,6 +70,18 @@ export const pl: Record = { moveToFreeWindow: 'Przenieś do wolnego okna', floatDropHint: 'Puść, aby otworzyć w wolnym oknie', dockToSidebar: 'Wróć do panelu bocznego', + pinTerminal: 'Przypnij Terminal', + pinAgentTerminal: 'Przypnij Terminal Agent', + pinToWorkspace: 'Przypnij do obszaru roboczego', + pinToGlobal: 'Przypnij globalnie', + unpinTerminal: 'Odepnij', + pinnedTerminalTooltip: '{kind} · {scope} · {cwd}', + pinnedTerminalKindUi: 'Terminal interfejsu', + pinnedTerminalKindAgent: 'Terminal Agent', + pinnedTerminalScopeWorkspace: 'Przypięty do obszaru roboczego', + pinnedTerminalScopeGlobal: 'Przypięty globalnie', + pinnedRailLabel: 'Przypięte terminale', + closePinnedTerminal: 'Zamknij terminal', collapse: 'Zwiń panel boczny', expand: 'Rozwiń panel boczny', collapseBottomPanel: 'Zwiń panel dolny', diff --git a/src/client/locales-pt.ts b/src/client/locales-pt.ts index 8804450..cfe6816 100644 --- a/src/client/locales-pt.ts +++ b/src/client/locales-pt.ts @@ -51,6 +51,18 @@ export const pt: Record = { moveToFreeWindow: 'Mover para janela flutuante', floatDropHint: 'Solte para abrir em uma janela flutuante', dockToSidebar: 'Voltar para a barra lateral', + pinTerminal: 'Fixar Terminal', + pinAgentTerminal: 'Fixar Terminal do Agent', + pinToWorkspace: 'Fixar no Workspace', + pinToGlobal: 'Fixar Globalmente', + unpinTerminal: 'Desafixar', + pinnedTerminalTooltip: '{kind} · {scope} · {cwd}', + pinnedTerminalKindUi: 'Terminal de UI', + pinnedTerminalKindAgent: 'Terminal do Agent', + pinnedTerminalScopeWorkspace: 'Fixado no workspace', + pinnedTerminalScopeGlobal: 'Fixado globalmente', + pinnedRailLabel: 'Terminais Fixados', + closePinnedTerminal: 'Fechar Terminal', collapse: 'Recolher barra lateral', expand: 'Expandir barra lateral', collapseBottomPanel: 'Recolher painel inferior', diff --git a/src/client/locales-ru.ts b/src/client/locales-ru.ts index 5afceb9..30ea406 100644 --- a/src/client/locales-ru.ts +++ b/src/client/locales-ru.ts @@ -64,6 +64,18 @@ export const ru: Record = { moveToFreeWindow: 'Переместить в свободное окно', floatDropHint: 'Отпустите, чтобы открыть в свободном окне', dockToSidebar: 'Вернуть на боковую панель', + pinTerminal: 'Закрепить терминал', + pinAgentTerminal: 'Закрепить терминал Agent', + pinToWorkspace: 'Закрепить в рабочей области', + pinToGlobal: 'Закрепить глобально', + unpinTerminal: 'Открепить', + pinnedTerminalTooltip: '{kind} · {scope} · {cwd}', + pinnedTerminalKindUi: 'Терминал интерфейса', + pinnedTerminalKindAgent: 'Терминал Agent', + pinnedTerminalScopeWorkspace: 'Закреплён в рабочей области', + pinnedTerminalScopeGlobal: 'Закреплён глобально', + pinnedRailLabel: 'Закреплённые терминалы', + closePinnedTerminal: 'Закрыть терминал', collapse: 'Свернуть боковую панель', expand: 'Развернуть боковую панель', collapseBottomPanel: 'Свернуть нижнюю панель', diff --git a/src/client/locales-sv.ts b/src/client/locales-sv.ts index cdb7824..b9c94af 100644 --- a/src/client/locales-sv.ts +++ b/src/client/locales-sv.ts @@ -51,6 +51,18 @@ export const sv: Record = { moveToFreeWindow: 'Flytta till fritt fönster', floatDropHint: 'Släpp för att öppna i ett fritt fönster', dockToSidebar: 'Tillbaka till sidopanelen', + pinTerminal: 'Fäst Terminal', + pinAgentTerminal: 'Fäst Agent-terminal', + pinToWorkspace: 'Fäst på arbetsyta', + pinToGlobal: 'Fäst globalt', + unpinTerminal: 'Ta bort fästning', + pinnedTerminalTooltip: '{kind} · {scope} · {cwd}', + pinnedTerminalKindUi: 'UI-terminal', + pinnedTerminalKindAgent: 'Agent-terminal', + pinnedTerminalScopeWorkspace: 'Fäst på arbetsyta', + pinnedTerminalScopeGlobal: 'Fäst globalt', + pinnedRailLabel: 'Fästa terminaler', + closePinnedTerminal: 'Stäng terminal', collapse: 'Komprimera sidopanel', expand: 'Expandera sidopanel', collapseBottomPanel: 'Komprimera bottenpanel', diff --git a/src/client/locales-th.ts b/src/client/locales-th.ts index 937d557..c7ef9a7 100644 --- a/src/client/locales-th.ts +++ b/src/client/locales-th.ts @@ -68,6 +68,18 @@ export const th: Record = { moveToFreeWindow: 'ย้ายไปยังหน้าต่างอิสระ', floatDropHint: 'ปล่อยเพื่อเปิดในหน้าต่างอิสระ', dockToSidebar: 'กลับไปที่แถบข้าง', + pinTerminal: 'ปักหมุดเทอร์มินัล', + pinAgentTerminal: 'ปักหมุดเทอร์มินัล Agent', + pinToWorkspace: 'ปักหมุดไปยังพื้นที่ทำงาน', + pinToGlobal: 'ปักหมุดแบบ Global', + unpinTerminal: 'ยกเลิกการปักหมุด', + pinnedTerminalTooltip: '{kind} · {scope} · {cwd}', + pinnedTerminalKindUi: 'เทอร์มินัล UI', + pinnedTerminalKindAgent: 'เทอร์มินัล Agent', + pinnedTerminalScopeWorkspace: 'ปักหมุดไปยังพื้นที่ทำงาน', + pinnedTerminalScopeGlobal: 'ปักหมุดแบบ Global', + pinnedRailLabel: 'เทอร์มินัลที่ปักหมุด', + closePinnedTerminal: 'ปิดเทอร์มินัล', collapse: 'ย่อแถบด้านข้าง', expand: 'ขยายแถบด้านข้าง', collapseBottomPanel: 'ย่อพาเนลด้านล่าง', diff --git a/src/client/locales-tr.ts b/src/client/locales-tr.ts index 45010eb..d62b092 100644 --- a/src/client/locales-tr.ts +++ b/src/client/locales-tr.ts @@ -68,6 +68,18 @@ export const tr: Record = { moveToFreeWindow: 'Serbest pencereye taşı', floatDropHint: 'Serbest pencerede açmak için bırakın', dockToSidebar: 'Kenar çubuğuna geri dön', + pinTerminal: 'Terminali Sabitle', + pinAgentTerminal: 'Agent Terminalini Sabitle', + pinToWorkspace: 'Çalışma Alanına Sabitle', + pinToGlobal: 'Genel Olarak Sabitle', + unpinTerminal: 'Sabitlemeyi Kaldır', + pinnedTerminalTooltip: '{kind} · {scope} · {cwd}', + pinnedTerminalKindUi: 'UI Terminali', + pinnedTerminalKindAgent: 'Agent Terminali', + pinnedTerminalScopeWorkspace: 'Çalışma alanına sabitlendi', + pinnedTerminalScopeGlobal: 'Genel olarak sabitlendi', + pinnedRailLabel: 'Sabitlenmiş Terminaller', + closePinnedTerminal: 'Terminali Kapat', collapse: 'Kenar çubuğunu daralt', expand: 'Kenar çubuğunu genişlet', collapseBottomPanel: 'Alt paneli daralt', diff --git a/src/client/locales-vi.ts b/src/client/locales-vi.ts index 94c8e6a..166ed24 100644 --- a/src/client/locales-vi.ts +++ b/src/client/locales-vi.ts @@ -68,6 +68,18 @@ export const vi: Record = { moveToFreeWindow: 'Chuyển sang cửa sổ tự do', floatDropHint: 'Thả để mở trong cửa sổ tự do', dockToSidebar: 'Quay lại thanh bên', + pinTerminal: 'Ghim Terminal', + pinAgentTerminal: 'Ghim Terminal Agent', + pinToWorkspace: 'Ghim vào Workspace', + pinToGlobal: 'Ghim Toàn cục', + unpinTerminal: 'Bỏ ghim', + pinnedTerminalTooltip: '{kind} · {scope} · {cwd}', + pinnedTerminalKindUi: 'Terminal UI', + pinnedTerminalKindAgent: 'Terminal Agent', + pinnedTerminalScopeWorkspace: 'Đã ghim vào workspace', + pinnedTerminalScopeGlobal: 'Đã ghim toàn cục', + pinnedRailLabel: 'Terminal đã ghim', + closePinnedTerminal: 'Đóng Terminal', collapse: 'Thu gọn thanh bên', expand: 'Mở rộng thanh bên', collapseBottomPanel: 'Thu gọn panel dưới', diff --git a/src/client/locales-zh-HK.ts b/src/client/locales-zh-HK.ts index 7e8cb94..8d28fa4 100644 --- a/src/client/locales-zh-HK.ts +++ b/src/client/locales-zh-HK.ts @@ -83,6 +83,18 @@ export const zhHK: Record = { moveToFreeWindow: '移動到自由視窗', floatDropHint: '放開以在自由視窗中開啟', dockToSidebar: '返回側邊欄', + pinTerminal: '固定終端', + pinAgentTerminal: '固定 Agent 終端', + pinToWorkspace: '固定到工作區', + pinToGlobal: '固定到全域', + unpinTerminal: '取消固定', + pinnedTerminalTooltip: '{kind} · {scope} · {cwd}', + pinnedTerminalKindUi: 'UI 終端', + pinnedTerminalKindAgent: 'Agent 終端', + pinnedTerminalScopeWorkspace: '固定到工作區', + pinnedTerminalScopeGlobal: '固定到全域', + pinnedRailLabel: '固定終端', + closePinnedTerminal: '關閉終端', collapse: '收起側邊欄', expand: '展開側邊欄', collapseBottomPanel: '收起底部面板', diff --git a/src/client/locales-zh-MO.ts b/src/client/locales-zh-MO.ts index e21f71b..3b2d7b3 100644 --- a/src/client/locales-zh-MO.ts +++ b/src/client/locales-zh-MO.ts @@ -83,6 +83,18 @@ export const zhMO: Record = { moveToFreeWindow: '移動到自由視窗', floatDropHint: '放開以在自由視窗中開啟', dockToSidebar: '返回側邊欄', + pinTerminal: '固定終端', + pinAgentTerminal: '固定 Agent 終端', + pinToWorkspace: '固定到工作區', + pinToGlobal: '固定到全域', + unpinTerminal: '取消固定', + pinnedTerminalTooltip: '{kind} · {scope} · {cwd}', + pinnedTerminalKindUi: 'UI 終端', + pinnedTerminalKindAgent: 'Agent 終端', + pinnedTerminalScopeWorkspace: '固定到工作區', + pinnedTerminalScopeGlobal: '固定到全域', + pinnedRailLabel: '固定終端', + closePinnedTerminal: '關閉終端', collapse: '收起側邊欄', expand: '展開側邊欄', collapseBottomPanel: '收起底部面板', diff --git a/src/client/locales-zh-TW.ts b/src/client/locales-zh-TW.ts index 2d81e3f..1b11fea 100644 --- a/src/client/locales-zh-TW.ts +++ b/src/client/locales-zh-TW.ts @@ -83,6 +83,18 @@ export const zhTW: Record = { moveToFreeWindow: '移動到自由視窗', floatDropHint: '放開以在自由視窗中開啟', dockToSidebar: '返回側邊欄', + pinTerminal: '固定終端', + pinAgentTerminal: '固定 Agent 終端', + pinToWorkspace: '固定到工作區', + pinToGlobal: '固定到全域', + unpinTerminal: '取消固定', + pinnedTerminalTooltip: '{kind} · {scope} · {cwd}', + pinnedTerminalKindUi: 'UI 終端', + pinnedTerminalKindAgent: 'Agent 終端', + pinnedTerminalScopeWorkspace: '固定到工作區', + pinnedTerminalScopeGlobal: '固定到全域', + pinnedRailLabel: '固定終端', + closePinnedTerminal: '關閉終端', collapse: '收起側邊欄', expand: '展開側邊欄', collapseBottomPanel: '收起底部面板', diff --git a/src/client/locales.ts b/src/client/locales.ts index 23ee8f7..6be16fa 100644 --- a/src/client/locales.ts +++ b/src/client/locales.ts @@ -75,6 +75,18 @@ export const zh = { moveToFreeWindow: '移动到自由窗口', floatDropHint: '松开以在自由窗口中打开', dockToSidebar: '回到侧边栏', + pinTerminal: '固定终端', + pinAgentTerminal: '固定 Agent 终端', + pinToWorkspace: '固定到工作区', + pinToGlobal: '固定到全局', + unpinTerminal: '取消固定', + pinnedTerminalTooltip: '{kind} · {scope} · {cwd}', + pinnedTerminalKindUi: 'UI 终端', + pinnedTerminalKindAgent: 'Agent 终端', + pinnedTerminalScopeWorkspace: '固定到工作区', + pinnedTerminalScopeGlobal: '固定到全局', + pinnedRailLabel: '固定终端', + closePinnedTerminal: '关闭终端', collapse: '折叠侧边栏', expand: '展开侧边栏', collapseBottomPanel: '折叠底部面板', @@ -419,6 +431,18 @@ export const en: Record = { moveToFreeWindow: 'Move to Free Window', floatDropHint: 'Release to open in a free window', dockToSidebar: 'Dock Back to Sidebar', + pinTerminal: 'Pin Terminal', + pinAgentTerminal: 'Pin Agent Terminal', + pinToWorkspace: 'Pin to Workspace', + pinToGlobal: 'Pin Globally', + unpinTerminal: 'Unpin', + pinnedTerminalTooltip: '{kind} · {scope} · {cwd}', + pinnedTerminalKindUi: 'UI Terminal', + pinnedTerminalKindAgent: 'Agent Terminal', + pinnedTerminalScopeWorkspace: 'Pinned to workspace', + pinnedTerminalScopeGlobal: 'Pinned globally', + pinnedRailLabel: 'Pinned Terminals', + closePinnedTerminal: 'Close Terminal', collapse: 'Collapse sidebar', expand: 'Expand sidebar', collapseBottomPanel: 'Collapse bottom panel', diff --git a/src/client/split-pane.tsx b/src/client/split-pane.tsx index 56a3d1f..2ed592a 100644 --- a/src/client/split-pane.tsx +++ b/src/client/split-pane.tsx @@ -31,6 +31,12 @@ export interface WorkbenchActions { resizeSplit: (splitId: string, index: number, deltaFrac: number) => void /** Float a docked tab out as a free window (tab context menu entry). */ floatTab: (tabId: string) => void + /** + * Pin/unpin a terminal tab (v0.17.0+). The shell snapshots the home cwd + * at pin time; null clears the pin. Optional: when undefined the tab + * context menu hides the pin entry (legacy callers). + */ + pinTab?: (tabId: string, scope: 'workspace' | 'global' | null) => void } /** One divider: pointer-capture drag translating px deltas into fractions. @@ -211,6 +217,7 @@ function LeafView(props: { else actions.moveTabBefore(payload, leaf.id, before) }} onFloatTab={actions.floatTab} + onPinTab={actions.pinTab} /> {leaf.tabs.length > 0 ? ( /* diff --git a/tests/tab-bar-context-menu.spec.tsx b/tests/tab-bar-context-menu.spec.tsx index 7392e40..b34ca89 100644 --- a/tests/tab-bar-context-menu.spec.tsx +++ b/tests/tab-bar-context-menu.spec.tsx @@ -31,11 +31,12 @@ function stubZh(): void { const MENU_LABELS = ['移动到自由窗口', '关闭', '关闭其他页签', '关闭左侧页签', '关闭右侧页签'] -function mountBar(tabs: SidebarTab[]): { +function mountBar(tabs: SidebarTab[], opts: { onPinTab?: ReturnType } = {}): { tabEls: HTMLElement[] onClose: ReturnType onActivate: ReturnType onFloatTab: ReturnType + onPinTab?: ReturnType unmount: () => void } { const container = document.createElement('div') @@ -43,6 +44,7 @@ function mountBar(tabs: SidebarTab[]): { const onClose = vi.fn() const onActivate = vi.fn() const onFloatTab = vi.fn() + const onPinTab = opts.onPinTab const root: Root = createRoot(container) act(() => { root.render(createElement(TabBar, { @@ -54,6 +56,7 @@ function mountBar(tabs: SidebarTab[]): { onNewTab: () => {}, newTabOptions: [], onFloatTab, + ...(onPinTab !== undefined ? { onPinTab } : {}), onDropTab: () => {}, })) }) @@ -63,6 +66,7 @@ function mountBar(tabs: SidebarTab[]): { onClose, onActivate, onFloatTab, + ...(onPinTab !== undefined ? { onPinTab } : {}), unmount: () => { act(() => { root.unmount() }) container.remove() @@ -208,3 +212,99 @@ describe('TabBar right-click context menu', () => { } }) }) + +describe('TabBar pin submenu (v0.17.0)', () => { + /** The portaled submenu rows (aria-expanded parent + nested menuitem rows). */ + function submenuItems(): HTMLElement[] { + return [...document.querySelectorAll('[role="menuitemradio"], [role="menuitem"]')] + } + + it('hides the pin entry when onPinTab is not provided (legacy callers)', () => { + stubZh() + const { tabEls, unmount } = mountBar(fourTabs()) + try { + act(() => { rightClick(tabEls[2]!) }) // t3 = terminal + // Exactly the legacy 5-item menu, no pin row. + expect(menuItems().map(item => item.textContent)).toEqual(MENU_LABELS) + } finally { + unmount() + } + }) + + it('adds a "Pin Terminal ▸" submenu for an unpinned UI terminal', () => { + stubZh() + const onPinTab = vi.fn() + const { tabEls, unmount } = mountBar(fourTabs(), { onPinTab }) + try { + act(() => { rightClick(tabEls[2]!) }) // t3 = UI terminal + const labels = menuItems().map(item => item.textContent) + // float | pin (with submenu indicator) | close | closeOthers | closeLeft | closeRight + expect(labels[0]).toBe('移动到自由窗口') + expect(labels[1]).toBe('固定终端') + expect(labels[2]).toBe('关闭') + // Hovering the pin row reveals the submenu items. React synthesizes + // onMouseEnter from the bubbling mouseover event, so dispatch that. + act(() => { menuItems()[1]!.dispatchEvent(new MouseEvent('mouseover', { bubbles: true })) }) + const sub = submenuItems().map(item => item.textContent) + expect(sub).toContain('固定到工作区') + expect(sub).toContain('固定到全局') + // Clicking "pin to workspace" fires the callback. + const wsItem = submenuItems().find(item => item.textContent === '固定到工作区')! + act(() => { wsItem.click() }) + expect(onPinTab).toHaveBeenCalledWith('t3', 'workspace') + } finally { + unmount() + } + }) + + it('uses the "Pin Agent Terminal" label for an agent terminal tab', () => { + stubZh() + const onPinTab = vi.fn() + const tabs: SidebarTab[] = [ + { id: 'agent:abc-123', type: 'terminal', title: 'Agent T' }, + ] + const { tabEls, unmount } = mountBar(tabs, { onPinTab }) + try { + act(() => { rightClick(tabEls[0]!) }) + const labels = menuItems().map(item => item.textContent) + expect(labels[1]).toBe('固定 Agent 终端') + } finally { + unmount() + } + }) + + it('shows "Unpin" instead of the submenu for an already-pinned terminal', () => { + stubZh() + const onPinTab = vi.fn() + const tabs: SidebarTab[] = [ + { id: 'terminal:1', type: 'terminal', title: 'T', pin: { scope: 'global' } }, + ] + const { tabEls, unmount } = mountBar(tabs, { onPinTab }) + try { + act(() => { rightClick(tabEls[0]!) }) + const labels = menuItems().map(item => item.textContent) + expect(labels).toContain('取消固定') + expect(labels).not.toContain('固定终端') + const unpinItem = menuItems().find(item => item.textContent === '取消固定')! + act(() => { unpinItem.click() }) + expect(onPinTab).toHaveBeenCalledWith('terminal:1', null) + } finally { + unmount() + } + }) + + it('does not add a pin entry for non-terminal tabs even with onPinTab provided', () => { + stubZh() + const onPinTab = vi.fn() + const { tabEls, unmount } = mountBar(fourTabs(), { onPinTab }) + try { + act(() => { rightClick(tabEls[0]!) }) // t1 = editor + expect(menuItems().map(item => item.textContent)).toEqual(MENU_LABELS) + act(() => { rightClick(tabEls[1]!) }) // t2 = git + expect(menuItems().map(item => item.textContent)).toEqual(MENU_LABELS) + expect(onPinTab).not.toHaveBeenCalled() + } finally { + unmount() + } + }) +})