- tasks→jobs:ctx.jobs / job_output / job_id / session/jobs / jobsBySession / Job* 词汇 - httpServer→webServer(inject + augmentation + register/registerUpgrade) - autoOpenTasks→autoOpenJobs 偏好键 - 右上角开关簇回到角落,注入 CSS 挤开 header 的「Session log」胶囊
212 lines
7.8 KiB
TypeScript
212 lines
7.8 KiB
TypeScript
/**
|
|
* The 7 built-in tab descriptors: the plugin registers its own pages
|
|
* (explorer / git / terminal / browser / subagent / editor / diff) through
|
|
* the same {@link BetterSidebarService} external plugins use — eating its
|
|
* own dogfood. The terminal descriptor owns its quota (`TERMINAL_LIMIT`)
|
|
* and mints `terminal:<n>` ids through `createTab`; the browser mints
|
|
* `browser:<n>` the same way (no quota).
|
|
*/
|
|
import { IconBranchOutline16, IconCodeOutline16, IconFolderOpen16, IconThinkOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
import type { Context } from '../../context-types.ts'
|
|
import { allLeaves, isAgentTabId, type SidebarState } from '../state.ts'
|
|
import { t } from '../locales.ts'
|
|
import { openSidebarFile } from '../intercept.tsx'
|
|
import { ExplorerView } from '../ExplorerView.tsx'
|
|
import { EditorHost } from '../EditorHost.tsx'
|
|
import { lazyChunkComponent } from '../lazy-chunk.tsx'
|
|
import { GitView } from '../GitView.tsx'
|
|
import { DiffTab } from '../DiffTab.tsx'
|
|
import { SubagentView } from '../SubagentView.tsx'
|
|
import { BrowserView } from '../BrowserView.tsx'
|
|
import { IconTerminalOutline16, IconDiffOutline16, IconGlobeOutline16 } from '../icons.tsx'
|
|
import type { ComponentType } from 'react'
|
|
import type { SessionScope } from '../api.ts'
|
|
import type { SidebarStore } from '../state.ts'
|
|
import type { TabDescriptor } from '../service.ts'
|
|
|
|
/**
|
|
* Lazy wrapper over the terminal view: xterm (and its stylesheet) is fetched
|
|
* only when a terminal tab is first opened (see chunk-loader.ts). The
|
|
* wrapper keeps the descriptor contract `(props) => ReactNode` — Sidebar
|
|
* calls it as a plain function.
|
|
*
|
|
* TerminalView's props are { scope, tabId, store } — `tabId` is NOT part of
|
|
* TabComponentProps (it carries `tab: SidebarTab` instead), so the
|
|
* descriptor maps it explicitly; a bare pass-through would leave tabId
|
|
* undefined and TerminalView's isAgentTabId(tabId) would crash on
|
|
* `undefined.startsWith` (regression-pinned in tests/lazy-chunk.spec.tsx).
|
|
*/
|
|
const LazyTerminal = lazyChunkComponent<TerminalViewProps>(
|
|
'terminal',
|
|
(mod) => mod.TerminalView as ComponentType<TerminalViewProps> | undefined,
|
|
)
|
|
|
|
/** The terminal view's props (mirror of TerminalView's own signature). */
|
|
interface TerminalViewProps {
|
|
scope: SessionScope
|
|
tabId: string
|
|
store: SidebarStore
|
|
}
|
|
|
|
/** How many UI-owned terminals may be open at once (agent-owned ones are uncapped). */
|
|
export const TERMINAL_LIMIT = 3
|
|
|
|
/** Count UI-owned terminals (agent:` tabs excluded — they are the model's). */
|
|
function uiTerminalCount(state: SidebarState): number {
|
|
return allLeaves(state.splits)
|
|
.flatMap(leaf => leaf.tabs)
|
|
.filter(tab => tab.type === 'terminal' && !isAgentTabId(tab.id)).length
|
|
}
|
|
|
|
/** The 7 built-in tab descriptors. */
|
|
export function builtinTabs(ctx: Context): readonly TabDescriptor[] {
|
|
return [
|
|
{
|
|
id: 'editor',
|
|
title: () => t('editor'),
|
|
icon: (size: number) => <IconCodeOutline16 size={size} />,
|
|
order: -1,
|
|
hidden: true,
|
|
dedupeKey: (tab) => tab.path,
|
|
component: ({ ctx, store, scope, tab }) => (
|
|
<EditorHost ctx={ctx} store={store} scope={scope} path={tab.path ?? ''} title={tab.title} />
|
|
),
|
|
},
|
|
{
|
|
id: 'explorer',
|
|
title: () => t('explorer'),
|
|
icon: (size: number) => <IconFolderOpen16 size={size} />,
|
|
order: 10,
|
|
single: true,
|
|
component: ({ ctx, store, scope, expanded, onToggleDir, onReferenceFile }) => (
|
|
<ExplorerView
|
|
sessionId={scope.sessionId}
|
|
cwd={scope.cwd}
|
|
expanded={expanded ?? []}
|
|
onToggle={onToggleDir ?? (() => { /* no-op */ })}
|
|
onOpenFile={(path) => { openSidebarFile(ctx, store, scope.sessionId, path) }}
|
|
onReferenceFile={onReferenceFile ?? (() => { /* no-op */ })}
|
|
/>
|
|
),
|
|
},
|
|
{
|
|
id: 'git',
|
|
title: () => t('git'),
|
|
icon: (size: number) => <IconBranchOutline16 size={size} />,
|
|
order: 20,
|
|
single: true,
|
|
component: ({ ctx, store, scope, onOpenDiff }) => (
|
|
<GitView
|
|
scope={scope}
|
|
onOpenFile={(path) => { openSidebarFile(ctx, store, scope.sessionId, path) }}
|
|
onOpenDiff={onOpenDiff ?? (() => { /* no-op */ })}
|
|
/>
|
|
),
|
|
},
|
|
{
|
|
id: 'subagent',
|
|
title: () => t('subagent'),
|
|
icon: (size: number) => <IconThinkOutline16 size={size} />,
|
|
order: 30,
|
|
single: true,
|
|
// Declarative settings: the auto-open switches render under this row in
|
|
// the Side card settings page (the Jobs page's own related settings).
|
|
settings: {
|
|
toggles: [{
|
|
key: 'autoOpenSubagent',
|
|
title: () => t('settingsSubagentTitle'),
|
|
desc: () => t('settingsSubagentDesc'),
|
|
}, {
|
|
key: 'autoOpenJobs',
|
|
title: () => t('settingsJobsTitle'),
|
|
desc: () => t('settingsJobsDesc'),
|
|
}],
|
|
},
|
|
component: ({ ctx, scope, visible, onSubagentJump }) => (
|
|
<SubagentView
|
|
sessionId={scope.sessionId}
|
|
ctx={ctx}
|
|
active={visible}
|
|
onOpenChild={(address) => { onSubagentJump?.(address.childSessionId) }}
|
|
/>
|
|
),
|
|
},
|
|
{
|
|
id: 'terminal',
|
|
title: () => t('terminal'),
|
|
icon: (size: number) => <IconTerminalOutline16 size={size} />,
|
|
order: 40,
|
|
available: (_ctx, _scope, state) => uiTerminalCount(state) < TERMINAL_LIMIT,
|
|
// Declarative settings: the model-facing terminal tools switch and the
|
|
// bottom-panel first-expansion auto-terminal switch render under this
|
|
// row in the Side card settings page (the Terminal page's own related
|
|
// settings; the host gates the toolset on the tools one independently).
|
|
settings: {
|
|
toggles: [{
|
|
key: 'agentTerminalTools',
|
|
title: () => t('settingsToolsTitle'),
|
|
desc: () => t('settingsToolsDesc'),
|
|
}, {
|
|
key: 'bottomPanelAutoTerminal',
|
|
title: () => t('settingsBottomTerminalTitle'),
|
|
desc: () => t('settingsBottomTerminalDesc'),
|
|
}],
|
|
},
|
|
createTab: (state) => {
|
|
const count = uiTerminalCount(state)
|
|
if (count >= TERMINAL_LIMIT) return null
|
|
return {
|
|
tab: {
|
|
id: `terminal:${state.nextTerminal}`,
|
|
type: 'terminal',
|
|
title: `${t('terminal')} ${state.nextTerminal}`,
|
|
},
|
|
patch: { nextTerminal: state.nextTerminal + 1 },
|
|
}
|
|
},
|
|
component: ({ tab, scope, store }) => <LazyTerminal scope={scope} store={store} tabId={tab.id} />,
|
|
},
|
|
{
|
|
id: 'browser',
|
|
title: () => t('browser'),
|
|
icon: (size: number) => <IconGlobeOutline16 size={size} />,
|
|
order: 50,
|
|
// Declarative settings: the sandbox escape hatch and the link
|
|
// takeover render under this tab's row in the Side card settings
|
|
// page (the sandbox one is warned on).
|
|
settings: {
|
|
toggles: [{
|
|
key: 'browserNoSandbox',
|
|
title: () => t('settingsBrowserSandboxTitle'),
|
|
desc: () => t('settingsBrowserSandboxDesc'),
|
|
}, {
|
|
key: 'browserInterceptLinks',
|
|
title: () => t('settingsBrowserLinksTitle'),
|
|
desc: () => t('settingsBrowserLinksDesc'),
|
|
}],
|
|
},
|
|
createTab: (state) => ({
|
|
tab: {
|
|
id: `browser:${state.nextBrowser}`,
|
|
type: 'browser',
|
|
title: t('browser'),
|
|
},
|
|
patch: { nextBrowser: state.nextBrowser + 1 },
|
|
}),
|
|
component: (props) => <BrowserView {...props} />,
|
|
},
|
|
{
|
|
id: 'diff',
|
|
title: () => t('git'),
|
|
icon: (size: number) => <IconDiffOutline16 size={size} />,
|
|
order: -1,
|
|
hidden: true,
|
|
dedupeKey: (tab) => tab.id,
|
|
component: ({ scope, tab }) => (
|
|
tab.diff === undefined ? null
|
|
: <DiffTab sessionId={scope.sessionId} cwd={scope.cwd} diff={tab.diff} />
|
|
),
|
|
},
|
|
]
|
|
}
|