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:
@@ -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
@@ -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') {
|
||||
|
||||
@@ -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: 'طي اللوحة السفلية',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: 'निचला पैनल संक्षिप्त करें',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: '下パネルを折りたたむ',
|
||||
|
||||
@@ -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: '하단 패널 접기',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: 'Свернуть нижнюю панель',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: 'ย่อพาเนลด้านล่าง',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: '收起底部面板',
|
||||
|
||||
@@ -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: '收起底部面板',
|
||||
|
||||
@@ -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: '收起底部面板',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 ? (
|
||||
/*
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user