Files
Agent-System-Frontend/src/workspaces.ts
weichen 17996cf4dd Add document cockpit workspace consuming rdm.cockpit.* (I2)
Why: implement the reviewed I2 increment - statistics band, structure tree
with lazy section pages, panorama center (project overview + node summary),
shared selection/filter, snapshot-consistency banner and honest states, per
the frozen contract (Dev docs/superpowers 2026-10-16, rollout section 7).

Evidence: vitest 314/314 green (14 new pure-logic tests covering capability
gate, true-zero vs unavailable stats, filter toggle, snapshot equality and
update notice, context key, selection survival, honest badges); tsc -b and
vite production build green.

Impact: new workspace entry (rdm-cockpit, gated on the seven cockpit
commands); no changes to existing surfaces; reader view and right panel
land in I3.

Refs: rollout I2
2026-10-16 09:03:48 +00:00

132 lines
5.0 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 体系工具排位:服务端声明的工作区对所有用户可见,按 shortcut 数字占位;
// 内置「代理」补进空位。文件浏览在第三栏「文件」入口(方案:入口收敛),不再占体系工具位。
// 插件管理面板不进这条清单。
// 2026-10-12 增量 2(审计入口能力门控):「追溯审计」不再无条件内置——普通基座
// 没有任何 rdm.inspect.* 命令,入口可达而进入即 400。仅当命令面探测通过
//(rdmAuditReady / store.rdmAuditAvailable)才纳入;代理仍无条件以 ⌘1 补位。
export interface WorkspaceEntry {
key: string
title: string
icon: string
shortcut: string
}
export interface WorkspaceSource {
id: string
title: string
icon?: string
shortcut?: string
}
/** 审计工作区消费的全部 rdm.inspect.* 命令(capabilities 入口 + 四页签取数)。 */
export const RDM_AUDIT_COMMANDS = [
'rdm.inspect.capabilities',
'rdm.inspect.summary',
'rdm.inspect.tree',
'rdm.inspect.object',
'rdm.inspect.relations',
'rdm.inspect.events',
'rdm.inspect.history',
] as const
/** 概览页导出区单独探测。不并入上面的七命令就绪门。 */
export const RDM_EXPORT_COMMANDS = [
'rdm.inspect.export.create',
'rdm.inspect.export.status',
'rdm.inspect.export.download',
] as const
/** 三条导出命令都启用才显示概览导出区。缺一或清单不可用则隐藏。 */
export function rdmExportReady(cmds: { id?: string; enabled?: boolean }[] | null | undefined): boolean {
if (!Array.isArray(cmds)) return false
return RDM_EXPORT_COMMANDS.every(need => cmds.some(c => c?.id === need && c.enabled === true))
}
/** 正式报告被完整性门拒绝时,界面先确认再带 draft 重试。 */
export function exportNeedsDraft(message: string): boolean {
return /不完整|草稿/.test(message)
}
/** GET /api/commands 清单是否足以支撑审计工作区:七命令齐备且 enabled。清单不可用(未登录/请求失败)视为未就绪。 */
export function rdmAuditReady(cmds: { id?: string; enabled?: boolean }[] | null | undefined): boolean {
if (!Array.isArray(cmds)) return false
return RDM_AUDIT_COMMANDS.every(need => cmds.some(c => c?.id === need && c.enabled === true))
}
/** 文档驾驶舱消费的全部 rdm.cockpit.* 命令(I1 只读面;rollout I2)。 */
export const COCKPIT_COMMANDS = [
'rdm.cockpit.overview',
'rdm.cockpit.tree',
'rdm.cockpit.node',
'rdm.cockpit.node.sources',
'rdm.cockpit.node.history',
'rdm.cockpit.node.impacts',
'rdm.cockpit.node.conflicts',
] as const
/** 七命令齐备且 enabled 才展示驾驶舱入口(与 rdmAuditReady 同源语义)。 */
export function cockpitReady(cmds: { id?: string; enabled?: boolean }[] | null | undefined): boolean {
if (!Array.isArray(cmds)) return false
return COCKPIT_COMMANDS.every(need => cmds.some(c => c?.id === need && c.enabled === true))
}
const BUILTINS: Omit<WorkspaceEntry, 'shortcut'>[] = [
{ key: 'agents', title: '代理', icon: '🤖' },
{ key: 'rdm-audit', title: '追溯审计', icon: '🔍' },
{ key: 'rdm-cockpit', title: '文档驾驶舱', icon: '📋' },
]
function shortcutNum(raw?: string): number | null {
if (!raw) return null
const m = raw.match(/(\d)/)
if (!m) return null
const n = Number(m[1])
return n >= 1 && n <= 9 ? n : null
}
interface BuildWorkspaceOptions {
/** rdm.inspect.* 命令面探测通过(store.rdmAuditAvailable)。默认 false:能力未知/缺失不展示审计入口。 */
rdmAudit?: boolean
/** rdm.cockpit.* 命令面探测通过(store.rdmCockpitAvailable)。默认 false:能力未知/缺失不展示驾驶舱入口。 */
rdmCockpit?: boolean
}
export function buildWorkspaceEntries(server: WorkspaceSource[], opts: BuildWorkspaceOptions = {}): WorkspaceEntry[] {
const used = new Set<number>()
const ranked: { rank: number; entry: Omit<WorkspaceEntry, 'shortcut'> }[] = []
const unranked: Omit<WorkspaceEntry, 'shortcut'>[] = []
for (const w of server) {
const entry = { key: `ws:${w.id}`, title: w.title, icon: w.icon || '🗂' }
const n = shortcutNum(w.shortcut)
if (n && !used.has(n)) {
used.add(n)
ranked.push({ rank: n, entry })
} else {
unranked.push(entry)
}
}
const takeFree = (prefer: number): number => {
if (!used.has(prefer) && prefer >= 1 && prefer <= 9) {
used.add(prefer)
return prefer
}
for (let n = 1; n <= 9; n++) {
if (!used.has(n)) {
used.add(n)
return n
}
}
return 99
}
ranked.push({ rank: takeFree(1), entry: BUILTINS[0] })
if (opts.rdmAudit === true) ranked.push({ rank: takeFree(2), entry: BUILTINS[1] })
if (opts.rdmCockpit === true) ranked.push({ rank: takeFree(3), entry: BUILTINS[2] })
for (const entry of unranked) ranked.push({ rank: takeFree(9), entry })
return ranked
.filter(item => item.rank <= 9)
.sort((a, b) => a.rank - b.rank)
.slice(0, 9)
.map((item, i) => ({ ...item.entry, shortcut: `⌘${i + 1}` }))
}