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: {