- SidebarPrefs 新增 tabsEnabled / viewersEnabled 开放 map(缺省 = 启用), PrefsSchema 用 z.dict 校验,旧文档零迁移 - TabDescriptor/FileViewerDescriptor 新增声明式设置面:viewer 补 title/icon, 两者补 settings.toggles(子代理声明 autoOpenSubagent、终端声明 agentTerminalTools) - 服务新增 isTabEnabled/isViewerEnabled;openTab 拒绝禁用类型、matchFileViewer 跳过禁用 viewer - SideCardSection 重写为注册表驱动清单:侧边栏内容(图标+类型id+开关+嵌套开关) + 文件预览(图标+标题+扩展名+开关),开关持久化 + 乐观提交回滚 - 消费点 gating:+ 菜单过滤、子代理自动展开、agent 终端 reconcile、 产出文件拦截(editor 关闭时回落默认行)、新会话 explorer 预置 - 内置 viewer 补 6 个手绘 outline 图标;store 快照携带 prefs 使开关变更即时生效 - 测试:service gating / builtins 声明 / parsePrefs map 校验 / schema 断言 / 新增 SideCardSection renderToString 规格 - 文档:README 设置小节、AGENTS.md 描述符与服务方法、新增设计文档;版本 0.5.0
147 lines
5.3 KiB
TypeScript
147 lines
5.3 KiB
TypeScript
/**
|
|
* The 6 built-in tab descriptors: the plugin registers its own pages
|
|
* (explorer / git / terminal / 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`.
|
|
*/
|
|
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 { TerminalView } from '../TerminalView.tsx'
|
|
import { GitView } from '../GitView.tsx'
|
|
import { DiffTab } from '../DiffTab.tsx'
|
|
import { SubagentView } from '../SubagentView.tsx'
|
|
import { IconTerminalOutline16, IconDiffOutline16 } from '../icons.tsx'
|
|
import type { TabDescriptor } from '../service.ts'
|
|
|
|
/** 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 6 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 switch renders under this row in
|
|
// the Side card settings page (the Subagent page's own related setting).
|
|
settings: {
|
|
toggles: [{
|
|
key: 'autoOpenSubagent',
|
|
title: () => t('settingsSubagentTitle'),
|
|
desc: () => t('settingsSubagentDesc'),
|
|
}],
|
|
},
|
|
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 renders
|
|
// under this row in the Side card settings page (the Terminal page's
|
|
// own related setting; the host gates the toolset on it independently).
|
|
settings: {
|
|
toggles: [{
|
|
key: 'agentTerminalTools',
|
|
title: () => t('settingsToolsTitle'),
|
|
desc: () => t('settingsToolsDesc'),
|
|
}],
|
|
},
|
|
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: ({ scope, tab, store }) => (
|
|
<TerminalView scope={scope} tabId={tab.id} store={store} />
|
|
),
|
|
},
|
|
{
|
|
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} />
|
|
),
|
|
},
|
|
]
|
|
}
|