Opening long conversation lists stretched and repositioned the project menu. Give the conversation submenu its own surface and positioning, aligned to the selected project row with independent viewport clamping and scrolling. Use compact navigation when neither side has room. Validation: 13 component tests, typecheck and lint passed; 108 Electron fixture checks passed across 14 light/dark scenarios. Full test run timed out after 60 seconds. Release note: Project activity menus now keep the project list stable when opening long conversation submenus.
19 KiB
应用导航与系统工具入口技术设计
文档信息
| 项目 | 内容 |
|---|---|
| 状态 | 设计中 |
| 版本 | 0.2 |
| 日期 | 2026-09-08 |
| 关联 PRD | 应用导航与系统工具入口 PRD |
| 功能逻辑 | 功能逻辑设计 |
| UI 设计 | UI 设计 |
1. 现状
主页面没有 URL Router,由 App.tsx 中的 WorkspaceView 和 keep-alive 状态切换:
type WorkspaceView =
| 'chat'
| 'magic-notes'
| 'knowledge'
| 'heartbeat'
| 'activity'
| 'settings'
文件、浏览器和终端已经由 RightAssistantSidebar、WorkbarShell 与 workbar-contracts.ts
承载。工作区是不可关闭的单实例应用,浏览器和终端是多实例应用。浏览器多实例的跨进程
实现以浏览器多实例技术设计为准。代码中尚无资源指标采集和资源监控
面板。现有侧栏底部 user-card 整行打开设置,并显示“本地工作区”和平台架构。
本次改动增加入口编排、从主侧栏到工作栏的命令通道,以及本机资源监控的 Main/Preload/ Renderer 最小实现。文件、终端和浏览器后端保持不变。
2. 模块边界
| 模块 | 责任 |
|---|---|
App.tsx |
组合主导航、系统工具栏、设置入口和页面离开检查 |
SidebarSystemTools(新增) |
渲染四个系统工具按钮并发送打开意图 |
RightAssistantSidebar.tsx |
接收打开意图,调用工作栏实例创建或激活逻辑 |
WorkbarShell.tsx |
继续负责实例策略、Tab、目录和焦点行为 |
workbar-contracts.ts |
继续作为工作栏应用 ID 和实例布局的权威契约 |
resource-monitor-contracts.ts(新增) |
定义本机资源快照、可用性和错误码 |
resource-monitor-service.ts(新增) |
在 Main 中采集并规范化本机与 GoodBuddy 指标 |
ResourceMonitorPanel.tsx(新增) |
按需轮询、保留有界历史并渲染资源面板 |
application-settings-contracts.ts |
增加应用导航偏好 schema 和默认值 |
application-settings-store.ts |
迁移并原子保存应用导航偏好 |
PlatformFeaturesSettingsSection 或独立设置 Section |
编辑“显示在侧边栏”偏好 |
不要在 SidebarSystemTools 中读取文件、创建 PTY、操作浏览器或采集系统指标;该组件只发送
有界的工具 ID。
3. Renderer 契约
3.1 系统工具 ID
export const sidebarSystemToolIds = [
'workspace',
'terminal',
'browser',
'resources'
] as const
export type SidebarSystemToolId = (typeof sidebarSystemToolIds)[number]
该类型可以放在 Renderer 内共享模块。首期不暴露 IPC,因为动作只编排已有工作栏 Renderer 状态;实际能力调用继续走现有 Preload API。
3.2 打开意图
App 向 RightAssistantSidebar 传入单调递增的请求或命令对象:
type WorkbarOpenIntent = {
requestId: number
appId: 'workspace' | 'terminal' | 'browser' | 'resources'
reason: 'sidebar-system-tool'
context?: { conversationId?: string; projectId?: string }
}
requestId 使连续点击同一工具仍能被消费。不要仅设置 assistantSidebarTab,因为用户可能已
关闭对应实例,且终端需要按执行空间寻找实例。
建议由 RightAssistantSidebar 在消费意图后通过现有 WorkbarShell 控制接口完成。控制器必须
接收上下文和调用原因,不能只按 appId 查找第一个实例:
type WorkbarController = {
openOrActivate(intent: WorkbarOpenIntent): Promise<WorkbarTabInstance>
}
若当前 WorkbarShell 没有命令式入口,先增加窄的回调或受控请求 Props,不引入全局事件总线。
工作区意图始终激活注册表中的不可关闭单实例;仅在迁移或布局损坏时补回。浏览器意图按
conversationId 激活最近使用的匹配实例,没有匹配实例时创建一个。工作栏“+”不走该去重
路径,选择浏览器时始终创建新实例。
3.3 终端匹配
工作栏“+”目录选择终端时每次创建新实例;侧栏系统工具按钮执行“进入当前终端”,采用:
- 解析当前项目对应的
ExecutionSpaceSelection。 - 在现有终端实例中按目标 identity 查找
starting或running状态。 - 存在多个匹配实例时激活最近使用的实例。
- 没有匹配实例时调用现有终端创建流程。
closing、exited、interrupted 和 failed 实例保留为历史 Tab,不阻止快捷按钮创建新终端。
工作栏“+”继续按原规则创建额外终端,不受快捷按钮去重影响。
3.4 资源监控工作栏应用
在 workbar-contracts.ts 中增加 resources:
{
id: 'resources',
instancePolicy: 'single',
defaultContext: 'application'
}
该应用不加入升级后的默认打开实例。侧栏资源按钮与工作栏“+”目录都通过既有单实例规则创建 或聚焦它。
4. 应用导航偏好
4.1 设置结构
在 ApplicationSettings 增加独立字段:
type BuiltInApplicationId = 'magic-notes' | 'knowledge' | 'heartbeat'
type ApplicationNavigationSettings = {
hiddenBuiltInApplicationIds: BuiltInApplicationId[]
}
默认值为空数组,保持升级前所有可用入口可见。ID 数组可以避免为每个应用增加一个顶层布尔 字段;Zod schema 必须去重、限制数量并拒绝未知内置 ID。
首期不保存排序,因为内置应用顺序固定。插件偏好在插件系统确定稳定 identity 和卸载保留 规则后另行扩展,不提前混入当前 schema。
4.2 与现有开关组合
入口可见条件为:
const visible = featureEnabled && !hiddenBuiltInApplicationIds.includes(id)
magicNotesEnabled 继续控制功能启停。知识库和智能心跳若未来增加业务启停开关,也按同一组合
规则处理。功能停用时设置页保留显隐偏好但禁用显隐 Switch,并提供原功能设置入口。导航设置
不能写回现有开关。
4.3 持久化与 IPC
沿用现有应用设置链路:
Renderer
-> window.goodbuddy.updates.updateSettings
-> settings:application:update
-> application-settings-store
-> application-settings.json
更新继续由 Main 校验可信 sender、Zod 输入和原子写入。设置版本迁移只补充默认空数组,不
修改已有 magicNotesEnabled 或其他平台设置。
5. 侧栏底部重构
现有 user-card 是一个覆盖整行的设置按钮。实现时替换为容器和相邻独立按钮:
<div className="sidebar-footer">
<SidebarSystemTools onOpenTool={openSystemTool} />
<button aria-label={t('navigation.settings')} ...>
<Settings aria-hidden="true" />
</button>
</div>
不能在设置按钮内部嵌套工具按钮。设置按钮继续调用 navigateFromSidebar('settings', trigger),
保留设置预加载、焦点恢复和未保存离开确认。
系统工具按钮调用统一 openSystemTool:
- 设置工作栏打开意图。
- 展开工作栏。
- 由工作栏创建或激活实例。
- 按现有策略把焦点移入目标面板。
工作栏仍由窗口右上方的固定开关关闭,关闭后的焦点返回该开关。
6. 工作栏状态与快捷按钮
RightAssistantSidebar 应向 App 暴露当前活动实例的 appId 和必要的有界状态摘要,或由
现有 onTabChange 扩展得到。主侧栏不读取终端输出、文件内容、URL 或活动详情。
type SidebarSystemToolState = {
activeToolId?: SidebarSystemToolId
statusByToolId?: Partial<Record<SidebarSystemToolId, 'attention' | 'error' | 'running'>>
}
首期可以只实现 activeToolId,没有可靠聚合来源时不显示状态徽标。不能根据局部事件猜测错误
或运行状态。
7. 应用显隐更新
应用显隐 Switch 复用平台功能设置的即时更新模式。请求开始后禁用三个应用显隐 Switch,只 允许一个完整目标集合处于提交中。Main 返回成功结果后更新 App 中的平台设置快照,主导航 随之变化。失败时恢复控件原值,在对应设置行显示可重试错误;主导航继续读取最后一次成功 保存的设置。
8. 插件兼容边界
首期不增加插件注册表、插件 schema 或插件持久化字段,只保留系统 ID 不向插件开放的产品边界。 插件市场设计需要单独确定 Host、权限、签名、生命周期和稳定 identity;不得直接复用 DeepSeek Harness 的 npm 插件市场。
9. 本机资源监控
9.1 采集范围
首期在 Main 中使用经过依赖审查的 systeminformation 获取整机 CPU、内存和磁盘 IO,使用
Electron app.getAppMetrics() 汇总 GoodBuddy 桌面进程 CPU、内存和进程数。采集函数不接收
Renderer 提供的路径、PID、设备名或命令。
首期指标定义:
- CPU:整机忙碌时间占比,规范化为
0至100。 - 内存:总字节数、可用字节数和
总量 - 可用量得到的使用量。 - 磁盘 IO:所有可用本机磁盘的每秒读取与写入字节数。
- GoodBuddy:
app.getAppMetrics()返回的桌面进程聚合 CPU、工作集内存和进程数量。各进程 CPU 先相加,再按逻辑处理器数量归一化为整机容量占比0至100;不能通过截断总和处理 多核数值。
受管 Runtime、MCP 和终端子进程的逐进程指标不在首期范围。磁盘 IO 在平台或权限不足时返回
不可用错误码,不能回退为 0。新增依赖在合入前需要完成许可证、维护状态、打包体积和供应链
检查;检查未通过时应选择提供同等跨平台语义的 Main 侧实现,不能改用 Renderer Shell 命令。
9.2 快照契约
type ResourceMetric<T> =
| { status: 'available'; value: T }
| { status: 'warming-up' }
| { status: 'unavailable'; reason: ResourceMetricUnavailableReason }
type LocalResourceSnapshot = {
sampledAt: string
host: {
cpuPercent: ResourceMetric<number>
memory: ResourceMetric<{
usedBytes: number
totalBytes: number
}>
diskIo: ResourceMetric<{
readBytesPerSecond: number
writeBytesPerSecond: number
}>
}
goodbuddy: {
cpuPercent: ResourceMetric<number>
memoryBytes: ResourceMetric<number>
processCount: ResourceMetric<number>
}
}
所有数字必须有限且非负;百分比限制为 0 至 100,字节数设置跨 IPC 上限。sampledAt 由
Main 生成。不可用原因使用稳定枚举,例如 unsupported-platform、permission-denied、
collector-timeout 和 collector-failed,不向 Renderer 返回原始命令输出。warming-up 只用于
依赖两个采样点的 CPU 与磁盘 IO,不能用于掩盖采集失败。
9.3 Preload 与 IPC
Preload 只暴露:
resourceMonitor.getLocalSnapshot(input: {
resetIntervalBaseline: boolean
}): Promise<LocalResourceSnapshot>
对应 IPC 为 resource-monitor:local-snapshot:get。Main 验证可信 sender;单次采集设置超时,
并合并同一时刻的并发请求,防止多个 Renderer 重复启动系统采集。采集超时后向调用方返回
有界错误,但底层 Promise 仍占用唯一单飞槽位,直到实际结束;在此之前不启动第二个采集
任务。输入只接受布尔值,接口不提供任意进程、磁盘或远程主机查询。
合并规则区分普通请求与基线重置请求:
- 普通请求在途时,新的普通请求复用它。
- 普通请求在途时收到重置请求,Main 最多登记一个待执行重置;当前采集结束后再执行,不能 用普通请求结果满足重置请求。
- 多个重置请求复用同一个在途或待执行重置。
- 重置请求在途时,普通请求复用其
warming-up结果,下一次时钟再取得区间值。
因此服务最多持有一个在途采集和一个待执行重置。调用方超时不取消这个顺序,也不增加队列 长度。
面板每次从非活动状态恢复后的首次请求传入 resetIntervalBaseline: true。Main 消费本次
systeminformation 与 Electron CPU 读数并重置区间基线,CPU 和磁盘 IO 返回 warming-up;
内存等瞬时指标正常返回。后续请求传入 false 并计算当前区间值,避免把面板停用期间计入
速率。
9.4 轮询与历史
ResourceMonitorPanel 仅在工作栏展开、resources 为活动实例且
document.visibilityState === 'visible' 时启动轮询:
- 进入活动状态后立即请求一次,并设置
resetIntervalBaseline: true。 - 此后每 2 秒请求一次;前一请求未结束时跳过本次时钟。
- Tab 失活、工作栏收起、页面变为隐藏或组件卸载时清理定时器,并忽略较晚返回的旧请求。
- Renderer 环形队列最多保留 150 个带真实时间戳的样本。
- 采集失败不追加伪造样本,也不持久化历史。
资源数据不写入应用设置、工作栏布局、运行记录或 SQLite。工作栏布局只保存 resources 实例
及其活动状态。
10. 测试
10.1 Renderer 测试
- 固定入口顺序不受三个应用显隐组合影响。
- 运行记录和设置没有显隐控件。
- 四个系统工具按钮具有名称、工具提示、键盘行为和选中状态。
- 文件复用不可关闭单实例;浏览器和终端按当前上下文聚焦匹配实例,工作栏“+”允许显式多实例。
- 工具按钮展开工作栏但不改变当前主页面。
- 显隐更新成功后主导航立即变化,失败时 Switch 和主导航恢复原状态。
- 窄窗口下工具按钮不重叠,焦点恢复正确。
- 资源面板只在可见活动时轮询,跳过重叠请求并把历史限制为 150 项。
- 窗口隐藏、最小化和恢复时停止轮询并重新建立区间基线。
- 资源部分可用、全部失败和重新进入活动状态均显示正确结果。
10.2 Main 与契约测试
- 新设置字段默认值、合法 ID、重复 ID、数量上限和未知 ID 校验。
- 旧版本设置迁移后默认显示所有既有应用。
- 更新导航偏好不修改魔法笔记启停等无关字段。
- 原子写入失败时保留最后一次有效设置。
- 资源快照拒绝 NaN、Infinity、负数字节数、越界百分比和未知错误码。
- 采集超时、权限不足和磁盘 IO 不支持时返回有界部分结果。
- 超时采集尚未实际结束时保持单飞,不累积新的后台采集任务。
- 普通采集在途时收到基线重置请求,最多排队一个重置且不复用普通结果。
resetIntervalBaseline: true时把 CPU 与磁盘 IO 标记为warming-up。- GoodBuddy CPU 预热时,其内存和进程数仍可独立返回。
- GoodBuddy 多进程 CPU 按逻辑处理器数量归一化,结果保持在
0至100。 - GoodBuddy 摘要不包含 PID、命令行、环境变量或其他进程信息。
10.3 回归范围
- 工作栏布局恢复和已关闭实例重建。
- 本地与托管 SSH 终端目标。
- 浏览器无会话、创建中和 Agent 操作中状态。
- 设置页未保存离开确认。
- 魔法笔记待办数和智能心跳建议数徽标。
- 浅色、深色、
220px与420px侧栏宽度。 - Windows、macOS 和 Linux 的真实 CPU、内存与磁盘 IO 采集;不支持项显示准确原因。
11. 实施顺序
实施范围以 PRD 分期为准。本节替代旧实施顺序;前文关于四个左侧 系统工具按钮、仅持久化隐藏 ID 的契约需在实现前同步,不能直接按旧规格开发。
- 同步 PRD 功能要求、User Stories、逻辑、UI 与技术契约,收敛首期应用中心样式。
- 接入现有内置应用页面与最底部应用中心入口,保留独立设置和固定运行记录入口。
- 为常驻显隐和排序设计最小持久化契约,保留已发布的相关设置与用户数据,按保存结果更新导航。
- 接入应用设置的共同界面与既有设置更新路径,先验证魔法笔记多入口状态一致性。
- 验证右侧现有文件、终端、浏览器 Tab;移除计划中的左下角工具快捷条和专用于该快捷条的意图接口工作。
- 单独实现资源快照契约、Main 采集服务、Preload API 与右侧
resources单实例面板。 - 验证键盘、焦点、浅深主题、窄窗口、设置失败、重启持久化和跨平台行为。
首期使用内置应用清单,不依赖远程商店、插件注册或安装服务。商店目录、推荐、安装与 更新机制在后续独立设计中确定;Demo 中的模拟商店不接入首期产品界面。
12. 项目活动汇总
对应 FR-14,按活动逻辑 派生状态,界面行为见活动界面。
conversation-activity.ts从App.tsx的会话、activeConversationIds、全部可见assistantTasks和项目元数据派生会话行、全局及项目计数,不新增持久化活动副本。ProjectActivity在有活动时渲染汇总与项目、会话级联菜单,ProjectSwitcher复用项目计数。App向ProjectActivity传入项目元数据、活动行、侧栏可见状态与既有精确跳转回调。 菜单按项目 ID 分组并记忆化派生集合;排序仅影响展示,不改聚合状态或持久化数据。- 两层菜单在同一 body Portal 内相接,桌面会话层使用独立 fixed 定位,不参与项目层高度计算。 项目层锚定摘要,会话层锚定选中项目行并独立限制窗口边界。布局在打开、层级变化、实时数据更新、窗口缩放和 滚动时按锚点重算;窄窗口隐藏项目面板并保留返回操作。菜单不调用 Modal 焦点隔离, 捕获阶段消费 Escape,防止触发窄侧栏的文档级监听;Tab 关闭后交还正常焦点顺序。 菜单关闭与焦点恢复在导航回调之前同步完成。
role="menu"使浮层直接进入共享browser-viewport-occlusion.ts相交检测。未增加 浏览器实例的特殊隐藏条件,也不关闭或重建浏览器会话。- 持久化快照刷新时,同一条仍在流式输出的消息保留本地待审批、待回答信息,持久化终态 则清除这些信息。响应成功后对应 Task 立即退出等待状态;响应期间到达的新问题、审批或 终态不能被旧响应覆盖。
AssistantDatabase.listTasks()保留最近历史窗口(默认 100 条),额外包含窗口外当前 运行或等待处理的可见 Task;activeVisibleTaskSelect与toTask的有效状态保持一致, 待调度运行规范化为queued,不因旧 Task 状态被误纳入实时集合。listConversations()保留最近 100 条活动会话,同时包含上述 Task 关联的较早活动会话, 以及含streaming消息的较早活动会话。两类列表去重后仍按原时间顺序返回,结果允许超过 历史窗口大小,避免后台工作被最近记录挤出。openActivityConversation按确切 Conversation ID 导航;内存中缺失时刷新持久化会话 列表并合并,再查找同一 ID。刷新失败或会话已不存在时显示通知,不回退到项目最新会话。 设置离开检查通过后才提交项目、会话及界面清理状态。- 远程恢复成功提示沿用独立的自动消失计时器;活动汇总更新不重置或替代该计时器。
验证记录见项目活动级联实施进度。本次只改 Renderer 展示与交互,远程 Runtime、Agent 协议和桌面到 Agent 的执行路径未改变。