feat: tab context menu pin submenu

M2 of pinned terminals (v0.17.0):
- TabBar: onPinTab prop (optional, hidden when unset for legacy callers)
- Terminal tabs get a 'Pin ▸' submenu (workspace/global) or 'Unpin' row
- UI vs Agent label distinction via isAgentTabId
- Non-terminal tabs get no pin entry (5-item legacy menu preserved)
- WorkbenchActions.pinTab threaded through split-pane LeafView
- Sidebar.onPinTab wires store.reduce(setTabPin) with cwd snapshot
- 12 new i18n keys synced across all 19 language dictionaries
This commit is contained in:
HuanLinOTO
2026-08-26 18:33:02 +08:00
parent 68f771e65f
commit 5d019d78ac
24 changed files with 412 additions and 10 deletions
+8 -1
View File
@@ -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])
/**
+44 -8
View File
@@ -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: {
<Menu
open={tabMenu !== null && tabMenuIndex >= 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') {
+12
View File
@@ -69,6 +69,18 @@ export const ar: Record<string, string> = {
moveToFreeWindow: 'النقل إلى نافذة حرة',
floatDropHint: 'حرّك للفتح في نافذة حرة',
dockToSidebar: 'العودة إلى الشريط الجانبي',
pinTerminal: 'تثبيت الطرفية',
pinAgentTerminal: 'تثبيت طرفية Agent',
pinToWorkspace: 'تثبيت إلى مساحة العمل',
pinToGlobal: 'تثبيت عام',
unpinTerminal: 'إلغاء التثبيت',
pinnedTerminalTooltip: '{kind} · {scope} · {cwd}',
pinnedTerminalKindUi: 'واجهة طرفية',
pinnedTerminalKindAgent: 'طرفية Agent',
pinnedTerminalScopeWorkspace: 'مثبتة في مساحة العمل',
pinnedTerminalScopeGlobal: 'مثبتة عام',
pinnedRailLabel: 'الطرفيات المثبتة',
closePinnedTerminal: 'إغلاق الطرفية',
collapse: 'طي الشريط الجانبي',
expand: 'توسيع الشريط الجانبي',
collapseBottomPanel: 'طي اللوحة السفلية',
+12
View File
@@ -54,6 +54,18 @@ export const de: Record<string, string> = {
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',
+12
View File
@@ -61,6 +61,18 @@ export const fr: Record<string, string> = {
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',
+12
View File
@@ -68,6 +68,18 @@ export const hi: Record<string, string> = {
moveToFreeWindow: 'फ़्री विंडो में ले जाएँ',
floatDropHint: 'फ़्री विंडो में खोलने के लिए छोड़ें',
dockToSidebar: 'साइडबार पर वापस जाएँ',
pinTerminal: 'टर्मिनल पिन करें',
pinAgentTerminal: 'Agent टर्मिनल पिन करें',
pinToWorkspace: 'वर्कस्पेस पर पिन करें',
pinToGlobal: 'वैश्विक रूप से पिन करें',
unpinTerminal: 'पिन हटाएँ',
pinnedTerminalTooltip: '{kind} · {scope} · {cwd}',
pinnedTerminalKindUi: 'UI टर्मिनल',
pinnedTerminalKindAgent: 'Agent टर्मिनल',
pinnedTerminalScopeWorkspace: 'वर्कस्पेस पर पिन किया गया',
pinnedTerminalScopeGlobal: 'वैश्विक रूप से पिन किया गया',
pinnedRailLabel: 'पिन किए गए टर्मिनल',
closePinnedTerminal: 'टर्मिनल बंद करें',
collapse: 'साइडबार संक्षिप्त करें',
expand: 'साइडबार विस्तृत करें',
collapseBottomPanel: 'निचला पैनल संक्षिप्त करें',
+12
View File
@@ -66,6 +66,18 @@ export const id: Record<string, string> = {
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',
+12
View File
@@ -59,6 +59,18 @@ export const it: Record<string, string> = {
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',
+12
View File
@@ -68,6 +68,18 @@ export const ja: Record<string, string> = {
moveToFreeWindow: 'フリーウィンドウへ移動',
floatDropHint: '離すとフリーウィンドウで開きます',
dockToSidebar: 'サイドバーへ戻す',
pinTerminal: 'ターミナルを固定',
pinAgentTerminal: 'Agent ターミナルを固定',
pinToWorkspace: 'ワークスペースに固定',
pinToGlobal: 'グローバルに固定',
unpinTerminal: '固定を解除',
pinnedTerminalTooltip: '{kind} · {scope} · {cwd}',
pinnedTerminalKindUi: 'UI ターミナル',
pinnedTerminalKindAgent: 'Agent ターミナル',
pinnedTerminalScopeWorkspace: 'ワークスペースに固定',
pinnedTerminalScopeGlobal: 'グローバルに固定',
pinnedRailLabel: '固定されたターミナル',
closePinnedTerminal: 'ターミナルを閉じる',
collapse: 'サイドバーを折りたたむ',
expand: 'サイドバーを展開',
collapseBottomPanel: '下パネルを折りたたむ',
+12
View File
@@ -60,6 +60,18 @@ export const ko: Record<string, string> = {
moveToFreeWindow: '자유 창으로 이동',
floatDropHint: '놓으면 자유 창에서 열립니다',
dockToSidebar: '사이드바로 돌아가기',
pinTerminal: '터미널 고정',
pinAgentTerminal: 'Agent 터미널 고정',
pinToWorkspace: '워크스페이스에 고정',
pinToGlobal: '전역에 고정',
unpinTerminal: '고정 해제',
pinnedTerminalTooltip: '{kind} · {scope} · {cwd}',
pinnedTerminalKindUi: 'UI 터미널',
pinnedTerminalKindAgent: 'Agent 터미널',
pinnedTerminalScopeWorkspace: '워크스페이스에 고정됨',
pinnedTerminalScopeGlobal: '전역에 고정됨',
pinnedRailLabel: '고정된 터미널',
closePinnedTerminal: '터미널 닫기',
collapse: '사이드바 접기',
expand: '사이드바 펼치기',
collapseBottomPanel: '하단 패널 접기',
+12
View File
@@ -66,6 +66,18 @@ export const nl: Record<string, string> = {
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',
+12
View File
@@ -70,6 +70,18 @@ export const pl: Record<string, string> = {
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',
+12
View File
@@ -51,6 +51,18 @@ export const pt: Record<string, string> = {
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',
+12
View File
@@ -64,6 +64,18 @@ export const ru: Record<string, string> = {
moveToFreeWindow: 'Переместить в свободное окно',
floatDropHint: 'Отпустите, чтобы открыть в свободном окне',
dockToSidebar: 'Вернуть на боковую панель',
pinTerminal: 'Закрепить терминал',
pinAgentTerminal: 'Закрепить терминал Agent',
pinToWorkspace: 'Закрепить в рабочей области',
pinToGlobal: 'Закрепить глобально',
unpinTerminal: 'Открепить',
pinnedTerminalTooltip: '{kind} · {scope} · {cwd}',
pinnedTerminalKindUi: 'Терминал интерфейса',
pinnedTerminalKindAgent: 'Терминал Agent',
pinnedTerminalScopeWorkspace: 'Закреплён в рабочей области',
pinnedTerminalScopeGlobal: 'Закреплён глобально',
pinnedRailLabel: 'Закреплённые терминалы',
closePinnedTerminal: 'Закрыть терминал',
collapse: 'Свернуть боковую панель',
expand: 'Развернуть боковую панель',
collapseBottomPanel: 'Свернуть нижнюю панель',
+12
View File
@@ -51,6 +51,18 @@ export const sv: Record<string, string> = {
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',
+12
View File
@@ -68,6 +68,18 @@ export const th: Record<string, string> = {
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: 'ย่อพาเนลด้านล่าง',
+12
View File
@@ -68,6 +68,18 @@ export const tr: Record<string, string> = {
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',
+12
View File
@@ -68,6 +68,18 @@ export const vi: Record<string, string> = {
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',
+12
View File
@@ -83,6 +83,18 @@ export const zhHK: Record<string, string> = {
moveToFreeWindow: '移動到自由視窗',
floatDropHint: '放開以在自由視窗中開啟',
dockToSidebar: '返回側邊欄',
pinTerminal: '固定終端',
pinAgentTerminal: '固定 Agent 終端',
pinToWorkspace: '固定到工作區',
pinToGlobal: '固定到全域',
unpinTerminal: '取消固定',
pinnedTerminalTooltip: '{kind} · {scope} · {cwd}',
pinnedTerminalKindUi: 'UI 終端',
pinnedTerminalKindAgent: 'Agent 終端',
pinnedTerminalScopeWorkspace: '固定到工作區',
pinnedTerminalScopeGlobal: '固定到全域',
pinnedRailLabel: '固定終端',
closePinnedTerminal: '關閉終端',
collapse: '收起側邊欄',
expand: '展開側邊欄',
collapseBottomPanel: '收起底部面板',
+12
View File
@@ -83,6 +83,18 @@ export const zhMO: Record<string, string> = {
moveToFreeWindow: '移動到自由視窗',
floatDropHint: '放開以在自由視窗中開啟',
dockToSidebar: '返回側邊欄',
pinTerminal: '固定終端',
pinAgentTerminal: '固定 Agent 終端',
pinToWorkspace: '固定到工作區',
pinToGlobal: '固定到全域',
unpinTerminal: '取消固定',
pinnedTerminalTooltip: '{kind} · {scope} · {cwd}',
pinnedTerminalKindUi: 'UI 終端',
pinnedTerminalKindAgent: 'Agent 終端',
pinnedTerminalScopeWorkspace: '固定到工作區',
pinnedTerminalScopeGlobal: '固定到全域',
pinnedRailLabel: '固定終端',
closePinnedTerminal: '關閉終端',
collapse: '收起側邊欄',
expand: '展開側邊欄',
collapseBottomPanel: '收起底部面板',
+12
View File
@@ -83,6 +83,18 @@ export const zhTW: Record<string, string> = {
moveToFreeWindow: '移動到自由視窗',
floatDropHint: '放開以在自由視窗中開啟',
dockToSidebar: '返回側邊欄',
pinTerminal: '固定終端',
pinAgentTerminal: '固定 Agent 終端',
pinToWorkspace: '固定到工作區',
pinToGlobal: '固定到全域',
unpinTerminal: '取消固定',
pinnedTerminalTooltip: '{kind} · {scope} · {cwd}',
pinnedTerminalKindUi: 'UI 終端',
pinnedTerminalKindAgent: 'Agent 終端',
pinnedTerminalScopeWorkspace: '固定到工作區',
pinnedTerminalScopeGlobal: '固定到全域',
pinnedRailLabel: '固定終端',
closePinnedTerminal: '關閉終端',
collapse: '收起側邊欄',
expand: '展開側邊欄',
collapseBottomPanel: '收起底部面板',
+24
View File
@@ -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<keyof typeof zh, string> = {
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',
+7
View File
@@ -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 ? (
/*
+101 -1
View File
@@ -31,11 +31,12 @@ function stubZh(): void {
const MENU_LABELS = ['移动到自由窗口', '关闭', '关闭其他页签', '关闭左侧页签', '关闭右侧页签']
function mountBar(tabs: SidebarTab[]): {
function mountBar(tabs: SidebarTab[], opts: { onPinTab?: ReturnType<typeof vi.fn> } = {}): {
tabEls: HTMLElement[]
onClose: ReturnType<typeof vi.fn>
onActivate: ReturnType<typeof vi.fn>
onFloatTab: ReturnType<typeof vi.fn>
onPinTab?: ReturnType<typeof vi.fn>
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<HTMLElement>('[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()
}
})
})