Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7756231b3b | ||
|
|
b16fc900bb | ||
|
|
bf8ff7f78e |
@@ -59,6 +59,15 @@ two phases from overlapping. Existing file filters still work, for example
|
|||||||
use `npm test -- --project unit` or `npm test -- --project integration`. To inspect
|
use `npm test -- --project unit` or `npm test -- --project integration`. To inspect
|
||||||
file selection without running tests, use `npx vitest list --filesOnly`.
|
file selection without running tests, use `npx vitest list --filesOnly`.
|
||||||
|
|
||||||
|
For activity-history persistence, run
|
||||||
|
`npx vitest run src/main/assistant/activity-history-io.test.ts`. This checks both
|
||||||
|
WAL growth and repeated full-history reads: consecutive saves compare against
|
||||||
|
the last committed in-memory records, invalidated on close, data clearing or
|
||||||
|
another SQLite connection's commit. It also checks rollback and retry. The cache
|
||||||
|
does not change the database schema or the full-snapshot IPC contract. WAL size
|
||||||
|
and SQL scan counts do not measure physical disk throughput or conversation
|
||||||
|
message persistence; validate those separately when investigating live I/O.
|
||||||
|
|
||||||
监听模式:
|
监听模式:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
+10
-5
@@ -322,7 +322,7 @@ records are listed separately and do not introduce another feature status.
|
|||||||
failure, Agent `SIGKILL`/restart, and recovery from a reopened Desktop SQLite
|
failure, Agent `SIGKILL`/restart, and recovery from a reopened Desktop SQLite
|
||||||
database. Successful tool START/END events appear exactly once, with no
|
database. Successful tool START/END events appear exactly once, with no
|
||||||
Prompt, provider, or tool replay observed. The current Agent source lock is
|
Prompt, provider, or tool replay observed. The current Agent source lock is
|
||||||
`0.13.3`, while the current Desktop release candidate is `0.13.12`; formal
|
`0.13.3`, while the current Desktop release candidate is `0.13.13`; formal
|
||||||
publication status follows the separate Agent and Desktop
|
publication status follows the separate Agent and Desktop
|
||||||
release channels. Previous macOS validation covered native package installation,
|
release channels. Previous macOS validation covered native package installation,
|
||||||
detached lifecycle, Attach, real Ask/Execute, and cancellation of tools in
|
detached lifecycle, Attach, real Ask/Execute, and cancellation of tools in
|
||||||
@@ -748,7 +748,9 @@ records are listed separately and do not introduce another feature status.
|
|||||||
- [x] **Supervisor reviews, story graphs, and activity**: Review a selected scope
|
- [x] **Supervisor reviews, story graphs, and activity**: Review a selected scope
|
||||||
and period, inspect saved result graphs and sources, confirm or revise entities,
|
and period, inspect saved result graphs and sources, confirm or revise entities,
|
||||||
and preview local knowledge entity writes. Sidebar feedback follows or pins a
|
and preview local knowledge entity writes. Sidebar feedback follows or pins a
|
||||||
Conversation or Task and opens its matching result. Activity combines heartbeat
|
Conversation or Task and opens its matching result. It shows target names with
|
||||||
|
compact pin/refresh buttons and preserves long-title readability and keyboard
|
||||||
|
focus in narrow sidebars. Activity combines heartbeat
|
||||||
and downstream review stages, including failures and links to older results.
|
and downstream review stages, including failures and links to older results.
|
||||||
Automatic stages process only new, changed, or unprocessed source portions;
|
Automatic stages process only new, changed, or unprocessed source portions;
|
||||||
no-change checks skip model calls. Manual review reprocesses bounded history.
|
no-change checks skip model calls. Manual review reprocesses bounded history.
|
||||||
@@ -843,9 +845,12 @@ records are listed separately and do not introduce another feature status.
|
|||||||
|
|
||||||
### Open source, builds, and releases
|
### Open source, builds, and releases
|
||||||
|
|
||||||
- Current source candidates are Desktop `0.13.12` and Agent `0.13.3`, with
|
- The current Desktop candidate is `0.13.13`; published Agent `0.13.3` is unchanged, with
|
||||||
OpenCode pinned to `1.18.29` and Continue to `1.5.47`. Publication status follows the independent
|
OpenCode pinned to `1.18.29` and Continue to `1.5.47`. Publication status follows the independent
|
||||||
Desktop and Agent release channels.
|
Desktop and Agent release channels.
|
||||||
|
- Desktop `0.13.13` reduces repeated activity-history reads during streaming saves
|
||||||
|
and fixes supervision feedback sidebar controls and target names. It adds no
|
||||||
|
database format migration over `0.13.12` and requires no Host update.
|
||||||
- Desktop `0.13.12` adds Supervisor reviews, graphs, activity, and automatic
|
- Desktop `0.13.12` adds Supervisor reviews, graphs, activity, and automatic
|
||||||
incremental processing, reduces streaming activity writes, and improves Mermaid,
|
incremental processing, reduces streaming activity writes, and improves Mermaid,
|
||||||
usage totals, conversation duration, Git, notes, and notifications. Database
|
usage totals, conversation duration, Git, notes, and notifications. Database
|
||||||
@@ -857,8 +862,8 @@ records are listed separately and do not introduce another feature status.
|
|||||||
and lint. The full local suite is not rerun at the user's request; full main-branch
|
and lint. The full local suite is not rerun at the user's request; full main-branch
|
||||||
CI, its production build, native packaging, and publication verification remain
|
CI, its production build, native packaging, and publication verification remain
|
||||||
pending. No local production build or packaging is part of this preparation.
|
pending. No local production build or packaging is part of this preparation.
|
||||||
Existing Linux x64 lifecycle evidence used eight real provider requests and
|
Desktop `0.13.12` and Agent `0.13.3` have completed their native release jobs;
|
||||||
incremental-review evidence used three; neither covers the complete native matrix.
|
this does not replace development-time real-Host scenario coverage.
|
||||||
- Validation records remain separate from implementation status. Current-source
|
- Validation records remain separate from implementation status. Current-source
|
||||||
storage validation covers real local and Linux x64 Host tool workloads,
|
storage validation covers real local and Linux x64 Host tool workloads,
|
||||||
concurrent projects/conversations, cancellation, and lossless database migration.
|
concurrent projects/conversations, cancellation, and lossless database migration.
|
||||||
|
|||||||
+4
-3
@@ -50,7 +50,7 @@
|
|||||||
- [x] **高效直连模型工作区工具**:直连模型使用随包 ripgrep 紧凑地发现文件和搜索内容,按行分页读取大型 UTF-8 文件,并在 Execute 中应用多文件补丁。Ask 可以搜索和读取,但不能应用补丁或运行命令;用户无需另行安装 ripgrep。可处理的读取或搜索错误允许模型调整参数后继续,部分搜索结果保留并明确提示覆盖不完整。
|
- [x] **高效直连模型工作区工具**:直连模型使用随包 ripgrep 紧凑地发现文件和搜索内容,按行分页读取大型 UTF-8 文件,并在 Execute 中应用多文件补丁。Ask 可以搜索和读取,但不能应用补丁或运行命令;用户无需另行安装 ripgrep。可处理的读取或搜索错误允许模型调整参数后继续,部分搜索结果保留并明确提示覆盖不完整。
|
||||||
- [x] **长上下文与工具输出分页**:历史消息、长回复和文档解析文本不再采用旧的固定截断限制。直连模型可分页续读已保存的命令和 Subagent 输出;所选模型的上下文窗口及传输边界仍然有效。
|
- [x] **长上下文与工具输出分页**:历史消息、长回复和文档解析文本不再采用旧的固定截断限制。直连模型可分页续读已保存的命令和 Subagent 输出;所选模型的上下文窗口及传输边界仍然有效。
|
||||||
- [x] **OpenCode 与 Continue**:使用隔离子进程、环境变量白名单、统一配置、取消、启动与控制请求时限、有界流式输出和活动记录;共享进程回收逻辑保留 Windows 完整进程树终止,并对采用独立进程组的 POSIX 子进程执行组回收。聊天状态检查使用一次性探测 Runtime 并在返回后立即回收,不进入按模型与项目隔离的执行 Runtime 缓存;本机 GoodBuddy 管理的 OpenCode 在该被动检查中只确认所选路径和模型凭据,不启动用后即毁的 Server,设置中的显式连接测试和原生能力清单仍执行完整启动及健康检查,原生能力清单随后与首次实际请求复用同一个执行 Runtime,不再重复冷启动;执行 Runtime 仍按项目复用,但 OpenCode 配置依赖和按内容摘要生成的 Skill 快照在所有本机项目间全局复用,会话数据、工具输出及请求级 MCP 状态继续隔离。本机 GoodBuddy 管理的 OpenCode 只对同一会话内的请求保持顺序,不同会话即使属于同一项目也可并行,不同项目同样可并行;请求级动态 MCP 通过默认通配禁用和当前请求精确放行保持隔离。托管的本机和远端 OpenCode 跳过界面未使用的自动 Git 快照,避免同步差异计算拖住会话,文件工具、子代理与工作区 Git 差异保留;Continue 持续排空不用展示的宿主 stdout,防止控制台输出填满管道。这些调整随新启动的 Runtime 生效,不改写外部 OpenCode Server 配置。OpenCode 与 Continue 不再按固定工具调用数或活动数中止单次运行,已观察到的工具与 Subagent 活动全部保留在本地会话,不再在原生任务继续执行时丢弃后续详情。交互提问只由前台对话回答,定时任务、远程通道和委派等后台执行遇到提问时会立即失败并提示改为前台运行,避免无限等待。本机 OpenCode 启动时直接使用已选择的绝对路径,不预先检查文件或运行 `--version`,路径失效时由实际启动返回错误;托管 Linux ARM Host 激活 Runtime 时复用准备阶段已验证的 registry 和 manifest,不重新哈希或检查完整 OpenCode 二进制。
|
- [x] **OpenCode 与 Continue**:使用隔离子进程、环境变量白名单、统一配置、取消、启动与控制请求时限、有界流式输出和活动记录;共享进程回收逻辑保留 Windows 完整进程树终止,并对采用独立进程组的 POSIX 子进程执行组回收。聊天状态检查使用一次性探测 Runtime 并在返回后立即回收,不进入按模型与项目隔离的执行 Runtime 缓存;本机 GoodBuddy 管理的 OpenCode 在该被动检查中只确认所选路径和模型凭据,不启动用后即毁的 Server,设置中的显式连接测试和原生能力清单仍执行完整启动及健康检查,原生能力清单随后与首次实际请求复用同一个执行 Runtime,不再重复冷启动;执行 Runtime 仍按项目复用,但 OpenCode 配置依赖和按内容摘要生成的 Skill 快照在所有本机项目间全局复用,会话数据、工具输出及请求级 MCP 状态继续隔离。本机 GoodBuddy 管理的 OpenCode 只对同一会话内的请求保持顺序,不同会话即使属于同一项目也可并行,不同项目同样可并行;请求级动态 MCP 通过默认通配禁用和当前请求精确放行保持隔离。托管的本机和远端 OpenCode 跳过界面未使用的自动 Git 快照,避免同步差异计算拖住会话,文件工具、子代理与工作区 Git 差异保留;Continue 持续排空不用展示的宿主 stdout,防止控制台输出填满管道。这些调整随新启动的 Runtime 生效,不改写外部 OpenCode Server 配置。OpenCode 与 Continue 不再按固定工具调用数或活动数中止单次运行,已观察到的工具与 Subagent 活动全部保留在本地会话,不再在原生任务继续执行时丢弃后续详情。交互提问只由前台对话回答,定时任务、远程通道和委派等后台执行遇到提问时会立即失败并提示改为前台运行,避免无限等待。本机 OpenCode 启动时直接使用已选择的绝对路径,不预先检查文件或运行 `--version`,路径失效时由实际启动返回错误;托管 Linux ARM Host 激活 Runtime 时复用准备阶段已验证的 registry 和 manifest,不重新哈希或检查完整 OpenCode 二进制。
|
||||||
- [x] **托管 SSH OpenCode 与 Continue**:该能力由“设置 > 平台功能”中的独立“远程项目”页签控制并默认关闭;关闭时不影响本地项目、普通桌面功能或桌面发布。支持 Linux x64、Linux arm64 和 macOS arm64 Host,不支持 Intel Mac;通过 SSH 按需启动 detached Agent,不要求开机服务。Ask 在 Runtime 工具边界保持只读,Execute 使用所选 SSH 账号的完整权限。Agent 持有 Prompt、模型和工具轮次、Runtime 进程及语义 transcript,Desktop 断开后重新 Attach 原操作而不重发 Prompt;模型凭据仅在当前操作内存中使用,不写入 Renderer、SSH 参数、环境或磁盘。当前 Agent 可以常驻,更新替换的旧 Agent 排空活动任务后退出。任务完成后回收空闲 Runtime,共享进程仍有任务或待答问题时保留;下一轮重建 Runtime 并恢复对话历史。设置页按平台和架构显示包状态,用户显式下载、更新或导入/导出包含 Agent、固定 Node、OpenCode 和 Continue 的签名 `.gbagent`;三个目标共用一个签名 catalog,继续校验签名和 SHA-256。Continue 需要更新后的联合包与配套 Desktop。首次发布混合 catalog 前需先升级 Desktop,旧版 Linux-only 目录读取器无法解析包含 Darwin 的目录。历史 macOS OpenCode 原生安装、生命周期、Attach、真实 Ask/Execute 和独立进程组工具取消已在真实 Host 验证,不代表 Continue 的同等实机覆盖或新版本已经发布。当前源码候选为 Agent `0.13.3`、Desktop `0.13.12`;详细运行边界和 Linux 历史回归证据见[远程主机技术设计](docs/features/remote-host/technical-design.md),正式发布状态以独立发布渠道为准。
|
- [x] **托管 SSH OpenCode 与 Continue**:该能力由“设置 > 平台功能”中的独立“远程项目”页签控制并默认关闭;关闭时不影响本地项目、普通桌面功能或桌面发布。支持 Linux x64、Linux arm64 和 macOS arm64 Host,不支持 Intel Mac;通过 SSH 按需启动 detached Agent,不要求开机服务。Ask 在 Runtime 工具边界保持只读,Execute 使用所选 SSH 账号的完整权限。Agent 持有 Prompt、模型和工具轮次、Runtime 进程及语义 transcript,Desktop 断开后重新 Attach 原操作而不重发 Prompt;模型凭据仅在当前操作内存中使用,不写入 Renderer、SSH 参数、环境或磁盘。当前 Agent 可以常驻,更新替换的旧 Agent 排空活动任务后退出。任务完成后回收空闲 Runtime,共享进程仍有任务或待答问题时保留;下一轮重建 Runtime 并恢复对话历史。设置页按平台和架构显示包状态,用户显式下载、更新或导入/导出包含 Agent、固定 Node、OpenCode 和 Continue 的签名 `.gbagent`;三个目标共用一个签名 catalog,继续校验签名和 SHA-256。Continue 需要更新后的联合包与配套 Desktop。首次发布混合 catalog 前需先升级 Desktop,旧版 Linux-only 目录读取器无法解析包含 Darwin 的目录。历史 macOS OpenCode 原生安装、生命周期、Attach、真实 Ask/Execute 和独立进程组工具取消已在真实 Host 验证,不代表 Continue 的同等实机覆盖或新版本已经发布。当前 Agent 源码锁定为 `0.13.3`,Desktop 候选为 `0.13.13`;详细运行边界和 Linux 历史回归证据见[远程主机技术设计](docs/features/remote-host/technical-design.md),正式发布状态以独立发布渠道为准。
|
||||||
- [x] **SSH Host 手动环境准备源码链路**:Host Key、认证和系统探针成功后先保存 Host,并只读探测共享 Agent/Runtime;保存 Host、打开项目都不自动安装。Host 卡片只有一个按版本事实显示“安装远程环境”“更新远程环境”或“重新安装”的主按钮,次级 SegmentedControl 选择默认且不持久化的“自动”、Host 下载或 GoodBuddy 传输;“版本匹配”badge 不等同环境健康。自动模式只在 operation/prepare 前探测并择一,显式选择保持有效,任何 prepare、commit 或 adoption 失败都不跨 acquisition 自动 fallback。两种方式把同一签名 compound `.gbagent` 交付到固定 staging 后,共用 control-plane prepare、commit、Agent activate/health、Runtime activate、finalize 与显式 cleanup。GoodBuddy 路径可在同一次操作下载并验证缺失候选、缓存并取得 lease,再有界流式 SFTP 上传一个归档和其中已验证的 bootstrap Node,不把约 294 MiB 整包读入 Main `Buffer`;Host 在解包时完成一次完整 payload 校验。未完成操作只记录暂存 cleanup 所需的 operation ID;下次更新尽力清理旧暂存后重新 prepare,不保存远端 metadata 副本,也不让 cleanup 失败阻塞新更新或回滚健康环境。已有项目在实际使用 Workspace/Runtime 时按需解析 Host current identity 并执行固定 `attach-or-bootstrap`,注册后的 health、capabilities 和 prompt 启动不扫描完整 payload。详见 [设计说明](./docs/features/remote-host/environment-provisioning-technical-design.md)。
|
- [x] **SSH Host 手动环境准备源码链路**:Host Key、认证和系统探针成功后先保存 Host,并只读探测共享 Agent/Runtime;保存 Host、打开项目都不自动安装。Host 卡片只有一个按版本事实显示“安装远程环境”“更新远程环境”或“重新安装”的主按钮,次级 SegmentedControl 选择默认且不持久化的“自动”、Host 下载或 GoodBuddy 传输;“版本匹配”badge 不等同环境健康。自动模式只在 operation/prepare 前探测并择一,显式选择保持有效,任何 prepare、commit 或 adoption 失败都不跨 acquisition 自动 fallback。两种方式把同一签名 compound `.gbagent` 交付到固定 staging 后,共用 control-plane prepare、commit、Agent activate/health、Runtime activate、finalize 与显式 cleanup。GoodBuddy 路径可在同一次操作下载并验证缺失候选、缓存并取得 lease,再有界流式 SFTP 上传一个归档和其中已验证的 bootstrap Node,不把约 294 MiB 整包读入 Main `Buffer`;Host 在解包时完成一次完整 payload 校验。未完成操作只记录暂存 cleanup 所需的 operation ID;下次更新尽力清理旧暂存后重新 prepare,不保存远端 metadata 副本,也不让 cleanup 失败阻塞新更新或回滚健康环境。已有项目在实际使用 Workspace/Runtime 时按需解析 Host current identity 并执行固定 `attach-or-bootstrap`,注册后的 health、capabilities 和 prompt 启动不扫描完整 payload。详见 [设计说明](./docs/features/remote-host/environment-provisioning-technical-design.md)。
|
||||||
- **SSH Host 环境准备验证记录**:当前源码已在隔离 Linux x64 环境通过包安装、Ask/Execute、原生子代理工作区外写入、重连及 stop/bootstrap。现有记录未覆盖 Host 卡片的完整 GitHub、北京镜像、Linux x64/arm64、取消和离线 GoodBuddy 传输矩阵,候选 CI/原生打包与系统休眠唤醒也尚未验证;开发记录不代表版本已经发布。
|
- **SSH Host 环境准备验证记录**:当前源码已在隔离 Linux x64 环境通过包安装、Ask/Execute、原生子代理工作区外写入、重连及 stop/bootstrap。现有记录未覆盖 Host 卡片的完整 GitHub、北京镜像、Linux x64/arm64、取消和离线 GoodBuddy 传输矩阵,候选 CI/原生打包与系统休眠唤醒也尚未验证;开发记录不代表版本已经发布。
|
||||||
- [x] **远程工作区与长任务更新**:Agent `0.11.23` 新增远程文件/Git 管理和可选模型限额,随包 Runtime 不再施加固定十分钟 Prompt 时限;无限请求时长仍保留独立连接超时。请先升级 Desktop 至 `0.12.11`,再下载并更新 Host 上的 Agent。
|
- [x] **远程工作区与长任务更新**:Agent `0.11.23` 新增远程文件/Git 管理和可选模型限额,随包 Runtime 不再施加固定十分钟 Prompt 时限;无限请求时长仍保留独立连接超时。请先升级 Desktop 至 `0.12.11`,再下载并更新 Host 上的 Agent。
|
||||||
@@ -141,10 +141,11 @@
|
|||||||
|
|
||||||
### 开源、构建与发布
|
### 开源、构建与发布
|
||||||
|
|
||||||
- 当前源码候选为 Desktop `0.13.12`、Agent `0.13.3`,OpenCode 固定为 `1.18.29`、Continue 为 `1.5.47`;正式发布状态以独立发布渠道为准。
|
- 当前 Desktop 候选为 `0.13.13`,已发布的 Agent `0.13.3` 保持不变,OpenCode 固定为 `1.18.29`、Continue 为 `1.5.47`;正式发布状态以独立发布渠道为准。
|
||||||
|
- Desktop `0.13.13` 减少流式活动历史保存时的重复读取,并修复监督反馈侧栏的紧凑固定/刷新按钮、目标名称和窄栏长标题显示。相较 `0.13.12` 不新增数据库格式迁移,无需更新 Host 环境。
|
||||||
- Desktop `0.13.12` 增加监督者回顾、图谱、活动和自动增量处理,减少流式活动写入,并改善 Mermaid、用量汇总、会话时长、Git、笔记和通知。数据库升级至 schema 46,回退旧客户端需使用升级前完整备份。
|
- Desktop `0.13.12` 增加监督者回顾、图谱、活动和自动增量处理,减少流式活动写入,并改善 Mermaid、用量汇总、会话时长、Git、笔记和通知。数据库升级至 schema 46,回退旧客户端需使用升级前完整备份。
|
||||||
- Agent `0.13.3` 回收空闲 Runtime,并使被替换的 Agent 等已有工作结束后退出;复合包要求 Desktop `0.13.12`,请先升级 Desktop,再更新 Host 环境。Node 保持 `24.19.0`。
|
- Agent `0.13.3` 回收空闲 Runtime,并使被替换的 Agent 等已有工作结束后退出;复合包要求 Desktop `0.13.12`,请先升级 Desktop,再更新 Host 环境。Node 保持 `24.19.0`。
|
||||||
- 发布准备执行定向回归、发布说明校验、类型检查及 lint。按用户要求不重跑本地全量测试;完整主分支 CI、生产构建、原生打包及公开发布核验待完成,本轮不做本地生产构建或打包。既有 Linux x64 生命周期验证使用 8 次真实请求,增量回顾验证使用 3 次,均不代表完整原生平台矩阵已验收。
|
- 发布准备执行定向回归、发布说明校验、类型检查及 lint。按用户要求不重跑本地全量测试;本轮候选的完整主分支 CI、生产构建、原生打包及公开发布核验待完成,不做本地生产构建或打包。Desktop `0.13.12` 和 Agent `0.13.3` 已完成原生发布流程,但不替代开发期真实 Host 场景覆盖。
|
||||||
- 验证记录与功能状态分开。存储已有本地及 Linux x64 Host 真实工具任务、多项目多会话、取消和无损迁移验证;进程复用有 Windows 完整 App 与 Linux x64 Host 证据,PPTX OCR 与空闲内存释放有 Windows 真实文件证据。图片工具已有真实服务商生成/编辑及 Host 传输证据,这些记录未覆盖完整界面自然语言调用和本地/远程切换。候选仍需主分支 CI 及原生发布打包,详见[Runtime 验证记录](./docs/features/assistant-workbar/progress.md)与[图片工具验证记录](./docs/features/conversation-media-generation/progress.md)。
|
- 验证记录与功能状态分开。存储已有本地及 Linux x64 Host 真实工具任务、多项目多会话、取消和无损迁移验证;进程复用有 Windows 完整 App 与 Linux x64 Host 证据,PPTX OCR 与空闲内存释放有 Windows 真实文件证据。图片工具已有真实服务商生成/编辑及 Host 传输证据,这些记录未覆盖完整界面自然语言调用和本地/远程切换。候选仍需主分支 CI 及原生发布打包,详见[Runtime 验证记录](./docs/features/assistant-workbar/progress.md)与[图片工具验证记录](./docs/features/conversation-media-generation/progress.md)。
|
||||||
- 发布准备前,`9836a4c` 已通过本地 4,858 项测试(67 项跳过)、完整类型检查和 lint;真实服务商画布分析仍未验证。该提交的三个原生 Agent CI 构建通过;桌面 CI `35485936807` 为 4,875 项通过、8 项 Electron 测试失败,均因 Linux 沙箱辅助程序未配置,在页面断言前退出。候选配置该程序并在 Xvfb 下运行测试,不关闭沙箱;候选本地验证、主分支 CI 与原生标签打包分别记录,不在发布准备中运行本地生产构建或打包。
|
- 发布准备前,`9836a4c` 已通过本地 4,858 项测试(67 项跳过)、完整类型检查和 lint;真实服务商画布分析仍未验证。该提交的三个原生 Agent CI 构建通过;桌面 CI `35485936807` 为 4,875 项通过、8 项 Electron 测试失败,均因 Linux 沙箱辅助程序未配置,在页面断言前退出。候选配置该程序并在 Xvfb 下运行测试,不关闭沙箱;候选本地验证、主分支 CI 与原生标签打包分别记录,不在发布准备中运行本地生产构建或打包。
|
||||||
- [x] **0BSD 开源许可**:原创代码可自由使用、复制、修改、分发和商用;第三方组件和资源仍遵循各自许可证。
|
- [x] **0BSD 开源许可**:原创代码可自由使用、复制、修改、分发和商用;第三方组件和资源仍遵循各自许可证。
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# Desktop 0.13.13 Preparation
|
||||||
|
|
||||||
|
Prepared on 2026-09-23 from `b16fc90`, relative to published Desktop `v0.13.12`.
|
||||||
|
This is a Desktop-only release; Agent remains at published `0.13.3`.
|
||||||
|
No LoongArch preview is requested.
|
||||||
|
|
||||||
|
## Scope and isolation
|
||||||
|
|
||||||
|
- `bf8ff7f` reduces repeated activity-history reads during streaming saves.
|
||||||
|
- `b16fc90` restores compact supervision feedback controls and target names.
|
||||||
|
- The candidate is prepared in a separate worktree. The user's uncommitted
|
||||||
|
supervision timeout tests in the original worktree are explicitly excluded.
|
||||||
|
- Desktop and both root lockfile versions are `0.13.13`. Both feature matrices
|
||||||
|
and bilingual release notes were reviewed against this range.
|
||||||
|
- Database schema and Agent/Runtime contracts are unchanged. The activity
|
||||||
|
comparison cache belongs to Desktop persistence; it does not modify the
|
||||||
|
deployed Agent, model bridge, or remote Runtime lifecycle.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
- Six focused files passed 192 tests: activity-history I/O, AssistantDatabase,
|
||||||
|
storage upgrades, sidebar interactions, SupervisionCard, and the Supervisor
|
||||||
|
Electron layout fixture.
|
||||||
|
- Re-running `tests/supervisor-layout.electron.test.ts` with
|
||||||
|
`GOODBUDDY_SUPERVISOR_SIDEBAR=1` passed its additional sidebar scenario:
|
||||||
|
480, 300, and 200 pixel widths in both themes, no horizontal overflow,
|
||||||
|
compact 34px buttons, visible keyboard focus, and no raw target ID in body text.
|
||||||
|
- `npm run release:notes:verify`, `npm run typecheck`, and `npm run lint` passed.
|
||||||
|
- Ten consecutive activity saves performed one full-history scan. WAL and SQL
|
||||||
|
counters are not installed-application physical disk throughput measurements.
|
||||||
|
- No real model requests, local full-suite rerun, production build, packaging,
|
||||||
|
or packaged-app probe were performed. UI validation uses production
|
||||||
|
components with isolated fixtures, not a real user database.
|
||||||
|
|
||||||
|
The candidate must pass complete main-branch CI and its production build before
|
||||||
|
an unused annotated `v0.13.13` tag is pushed to both remotes. Native six-platform
|
||||||
|
packaging and public GitHub/OSS verification remain subsequent release steps.
|
||||||
@@ -33,6 +33,17 @@
|
|||||||
的缓存引用,不影响其他会话。若事务随后回滚或仍收到该任务的后续事件,按已提交记录重建。
|
的缓存引用,不影响其他会话。若事务随后回滚或仍收到该任务的后续事件,按已提交记录重建。
|
||||||
释放引用不等于进程 RSS 立即下降,垃圾回收和 Runtime 自身会话缓存另按生命周期回收。
|
释放引用不等于进程 RSS 立即下降,垃圾回收和 Runtime 自身会话缓存另按生命周期回收。
|
||||||
|
|
||||||
|
## 活动历史连续保存
|
||||||
|
|
||||||
|
活动历史在桌面 `AssistantDatabase` 中按变化记录写入,保留记录顺序和重复 ID 的独立位置。
|
||||||
|
连续保存使用最近一次成功提交的记录作为比较基准,不再逐次读取整份历史。关闭数据库、
|
||||||
|
清空数据或检测到其他 SQLite 连接提交后重新读取;事务失败不更新比较缓存,重试仍基于已提交内容。
|
||||||
|
本次优化不改变数据库 schema 或完整快照 IPC,也不改变 Agent、Runtime 和远程模型桥。
|
||||||
|
本机与远程任务的桌面活动记录共用此保存路径,Host 不需要更新。
|
||||||
|
|
||||||
|
WAL 增长与 SQL 全历史扫描次数的定向验证见 [BUILD.md](../../../BUILD.md);
|
||||||
|
这些指标不等于安装后应用的物理磁盘吞吐测量。
|
||||||
|
|
||||||
## 会话列表读取与前端保留
|
## 会话列表读取与前端保留
|
||||||
|
|
||||||
列表读取与数据库存储是两个不同层次。`messages.metadata_json` 继续保留完整过程,
|
列表读取与数据库存储是两个不同层次。`messages.metadata_json` 继续保留完整过程,
|
||||||
|
|||||||
@@ -18,6 +18,19 @@
|
|||||||
|
|
||||||
## 验证证据
|
## 验证证据
|
||||||
|
|
||||||
|
### 2026-09-23 Desktop 0.13.13 侧栏布局回归
|
||||||
|
|
||||||
|
- 固定目标与刷新改用标题旁的共享图标按钮;目标显示会话/任务名称,缺失名称使用未命名文案。
|
||||||
|
对应 US-S25,行为见[会话侧栏](./ui-design.md#会话侧栏),未改变监督运行或取消逻辑。
|
||||||
|
- `RightAssistantSidebar.resize.test.tsx` 66 项和 `SupervisionCard.test.tsx` 4 项通过。
|
||||||
|
`tests/supervisor-layout.electron.test.ts` 的常规工作区场景通过,额外设置
|
||||||
|
`GOODBUDDY_SUPERVISOR_SIDEBAR=1` 后的侧栏场景也通过:480/300/200px、
|
||||||
|
浅深主题、长标题、34px 按钮、键盘焦点与无横向溢出,共 6 种布局。
|
||||||
|
- 完整 typecheck、lint 通过,真实模型调用 0 次。使用隔离组件 fixture,不代表真实
|
||||||
|
用户数据库端到端验收;发布验证范围见[候选记录](../../development/release-preparation-0.13.13.md)。
|
||||||
|
|
||||||
|
### 既有实现验证
|
||||||
|
|
||||||
- `npx vitest run src/renderer/src/SupervisorWorkspace.test.tsx src/renderer/src/HeartbeatCenter.test.tsx`:17 个测试通过,覆盖统一页签、无计划手动回顾、加载状态、图谱真实连线与来源过滤。
|
- `npx vitest run src/renderer/src/SupervisorWorkspace.test.tsx src/renderer/src/HeartbeatCenter.test.tsx`:17 个测试通过,覆盖统一页签、无计划手动回顾、加载状态、图谱真实连线与来源过滤。
|
||||||
- `npx vitest run src/main/assistant/supervisor-service.test.ts src/main/assistant/assistant-database.test.ts src/main/application-settings-store.test.ts src/renderer/src/ApplicationCenter.test.tsx`:166 个测试通过。
|
- `npx vitest run src/main/assistant/supervisor-service.test.ts src/main/assistant/assistant-database.test.ts src/main/application-settings-store.test.ts src/renderer/src/ApplicationCenter.test.tsx`:166 个测试通过。
|
||||||
- `npm run typecheck`:通过。
|
- `npm run typecheck`:通过。
|
||||||
|
|||||||
@@ -34,6 +34,11 @@
|
|||||||
|
|
||||||
## 会话侧栏
|
## 会话侧栏
|
||||||
|
|
||||||
|
监督反馈标题右侧使用共享紧凑图标按钮固定/取消固定目标与刷新回顾;固定状态通过
|
||||||
|
`aria-pressed` 和按钮高亮表达。目标正文显示会话或任务名称,名称缺失时使用“未命名会话”
|
||||||
|
或“未命名任务”,原始 ID 仅作为悬停补充。无目标时禁用固定和刷新,读取中禁用刷新;
|
||||||
|
窄侧栏中的长标题换行,按钮保留可见键盘焦点。
|
||||||
|
|
||||||
仅在应用已启用时挂载监督面板及固定目标控件;关闭或设置尚未加载时不挂载,也不发起面板查询。任务列表仍按原规则显示。
|
仅在应用已启用时挂载监督面板及固定目标控件;关闭或设置尚未加载时不挂载,也不发起面板查询。任务列表仍按原规则显示。
|
||||||
|
|
||||||
右侧通用助手工作栏中的监督卡片展示“监督反馈”,沿用现有[工作栏设计](../assistant-workbar/prd.md)的布局和执行边界。当前支持 Conversation/Task:有选中任务时跟随任务,否则跟随会话;“固定监督目标”保存当前目标,切换聊天后保持,取消固定恢复跟随。卡片显示目标、结果真实范围、回顾区间和摘要,可刷新并查看匹配来源;无目标或无匹配结果显示空态。Experiment 尚未接入。
|
右侧通用助手工作栏中的监督卡片展示“监督反馈”,沿用现有[工作栏设计](../assistant-workbar/prd.md)的布局和执行边界。当前支持 Conversation/Task:有选中任务时跟随任务,否则跟随会话;“固定监督目标”保存当前目标,切换聊天后保持,取消固定恢复跟随。卡片显示目标、结果真实范围、回顾区间和摘要,可刷新并查看匹配来源;无目标或无匹配结果显示空态。Experiment 尚未接入。
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "goodbuddy",
|
"name": "goodbuddy",
|
||||||
"version": "0.13.12",
|
"version": "0.13.13",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "goodbuddy",
|
"name": "goodbuddy",
|
||||||
"version": "0.13.12",
|
"version": "0.13.13",
|
||||||
"license": "0BSD",
|
"license": "0BSD",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@agentclientprotocol/sdk": "0.25.1",
|
"@agentclientprotocol/sdk": "0.25.1",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "goodbuddy",
|
"name": "goodbuddy",
|
||||||
"version": "0.13.12",
|
"version": "0.13.13",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Secure desktop AI workspace with controlled Agent Runtimes",
|
"description": "Secure desktop AI workspace with controlled Agent Runtimes",
|
||||||
"desktopName": "GoodBuddy",
|
"desktopName": "GoodBuddy",
|
||||||
|
|||||||
@@ -1,6 +1,34 @@
|
|||||||
{
|
{
|
||||||
"formatVersion": 1,
|
"formatVersion": 1,
|
||||||
"releases": [
|
"releases": [
|
||||||
|
{
|
||||||
|
"version": "0.13.13",
|
||||||
|
"releasedAt": "2026-09-23",
|
||||||
|
"notes": {
|
||||||
|
"zh-CN": {
|
||||||
|
"highlights": [],
|
||||||
|
"features": [],
|
||||||
|
"fixes": [
|
||||||
|
"**减少流式活动历史的重复读取**:活动历史较多时,连续保存流式更新不再每次重新读取整份历史。保留已有记录与顺序,保存失败后仍可重试。",
|
||||||
|
"**修复监督反馈侧栏布局**:固定目标与刷新操作使用标题旁的紧凑按钮,目标显示会话或任务名称。长标题和窄侧栏下保持内容可读,并保留键盘焦点提示。"
|
||||||
|
],
|
||||||
|
"notices": [
|
||||||
|
"本次仅更新 Desktop,Agent 保持 **0.13.3**,无需因此更新 Host 环境。从 Desktop 0.13.12 升级不新增数据库格式迁移。"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"en-US": {
|
||||||
|
"highlights": [],
|
||||||
|
"features": [],
|
||||||
|
"fixes": [
|
||||||
|
"**Reduce repeated activity-history reads during streaming**: With a large activity history, consecutive streaming saves no longer reread the entire history on every update. Existing records and ordering are preserved, and failed saves remain retryable.",
|
||||||
|
"**Fix the supervision feedback sidebar layout**: Pin and refresh actions now use compact buttons beside the heading, and targets show conversation or task names. Long titles remain readable in narrow sidebars, with visible keyboard focus."
|
||||||
|
],
|
||||||
|
"notices": [
|
||||||
|
"This release updates Desktop only. Agent remains at **0.13.3**, with no Host update required for these fixes; upgrading from Desktop 0.13.12 adds no database format migration."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"version": "0.13.12",
|
"version": "0.13.12",
|
||||||
"releasedAt": "2026-09-23",
|
"releasedAt": "2026-09-23",
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { randomUUID } from 'node:crypto'
|
|||||||
import { mkdtempSync, rmSync, statSync } from 'node:fs'
|
import { mkdtempSync, rmSync, statSync } from 'node:fs'
|
||||||
import { tmpdir } from 'node:os'
|
import { tmpdir } from 'node:os'
|
||||||
import { join } from 'node:path'
|
import { join } from 'node:path'
|
||||||
import type { DatabaseSync } from 'node:sqlite'
|
import { DatabaseSync } from 'node:sqlite'
|
||||||
import { expect, it } from 'vitest'
|
import { expect, it, vi } from 'vitest'
|
||||||
import { AssistantDatabase } from './assistant-database'
|
import { AssistantDatabase } from './assistant-database'
|
||||||
import { upgradeAssistantStorage } from './assistant-storage-upgrade'
|
import { upgradeAssistantStorage } from './assistant-storage-upgrade'
|
||||||
|
|
||||||
@@ -43,12 +43,18 @@ it('migrates activity history and writes only changed records during streaming',
|
|||||||
expect(database.getActivityHistory()).toEqual({ records, legacyHistoryMayBeIncomplete: true })
|
expect(database.getActivityHistory()).toEqual({ records, legacyHistoryMayBeIncomplete: true })
|
||||||
const migrated = (database as unknown as { database: DatabaseSync }).database
|
const migrated = (database as unknown as { database: DatabaseSync }).database
|
||||||
migrated.exec('PRAGMA wal_checkpoint(TRUNCATE); PRAGMA wal_autocheckpoint=0')
|
migrated.exec('PRAGMA wal_checkpoint(TRUNCATE); PRAGMA wal_autocheckpoint=0')
|
||||||
|
const prepare = vi.spyOn(migrated, 'prepare')
|
||||||
for (let index = 0; index < 10; index++) {
|
for (let index = 0; index < 10; index++) {
|
||||||
records[0]!.detail += 'x'
|
records[0]!.detail += 'x'
|
||||||
database.replaceActivityHistory({ records, legacyHistoryMayBeIncomplete: true })
|
database.replaceActivityHistory({ records, legacyHistoryMayBeIncomplete: true })
|
||||||
}
|
}
|
||||||
const afterBytes = statSync(`${path}-wal`).size
|
const afterBytes = statSync(`${path}-wal`).size
|
||||||
console.log({ beforeBytes, afterBytes })
|
const historyScans = prepare.mock.calls.filter(([sql]) =>
|
||||||
|
/SELECT record_key, record_json FROM activity_history_records/.test(sql)
|
||||||
|
).length
|
||||||
|
prepare.mockRestore()
|
||||||
|
console.log({ beforeBytes, afterBytes, historyScans })
|
||||||
|
expect(historyScans).toBeLessThanOrEqual(1)
|
||||||
expect(afterBytes).toBeLessThan(beforeBytes / 100)
|
expect(afterBytes).toBeLessThan(beforeBytes / 100)
|
||||||
database.replaceActivityHistory({ records, legacyHistoryMayBeIncomplete: true })
|
database.replaceActivityHistory({ records, legacyHistoryMayBeIncomplete: true })
|
||||||
expect(statSync(`${path}-wal`).size).toBe(afterBytes)
|
expect(statSync(`${path}-wal`).size).toBe(afterBytes)
|
||||||
@@ -63,3 +69,46 @@ it('migrates activity history and writes only changed records during streaming',
|
|||||||
rmSync(directory, { recursive: true, force: true })
|
rmSync(directory, { recursive: true, force: true })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('keeps activity comparisons correct after rollback, external writes and clearing', () => {
|
||||||
|
const directory = mkdtempSync(join(tmpdir(), 'goodbuddy-save-io-'))
|
||||||
|
const path = join(directory, 'assistant.sqlite')
|
||||||
|
const database = new AssistantDatabase(path)
|
||||||
|
try {
|
||||||
|
database.initialize(directory)
|
||||||
|
const raw = (database as unknown as { database: DatabaseSync }).database
|
||||||
|
const snapshot = {
|
||||||
|
records: ['first', 'second'].map((id) => ({
|
||||||
|
id, conversationId: randomUUID(), requestId: randomUUID(),
|
||||||
|
scope: { kind: 'global' as const }, kind: 'tool' as const,
|
||||||
|
title: 'Tool', detail: 'initial', status: 'running' as const, createdAt: Date.now()
|
||||||
|
})),
|
||||||
|
legacyHistoryMayBeIncomplete: false
|
||||||
|
}
|
||||||
|
database.replaceActivityHistory(snapshot)
|
||||||
|
raw.exec(`CREATE TRIGGER reject_activity BEFORE UPDATE ON activity_history_records
|
||||||
|
WHEN json_extract(NEW.record_json, '$.id') = 'second'
|
||||||
|
BEGIN SELECT RAISE(ABORT, 'test failure'); END`)
|
||||||
|
for (const record of snapshot.records) record.detail = 'changed'
|
||||||
|
expect(() => database.replaceActivityHistory(snapshot)).toThrow('test failure')
|
||||||
|
expect(database.getActivityHistory().records.map((record) => record.detail)).toEqual(['initial', 'initial'])
|
||||||
|
raw.exec('DROP TRIGGER reject_activity')
|
||||||
|
database.replaceActivityHistory(snapshot)
|
||||||
|
expect(database.getActivityHistory()).toEqual(snapshot)
|
||||||
|
|
||||||
|
const external = new DatabaseSync(path)
|
||||||
|
try {
|
||||||
|
external.exec('DELETE FROM activity_history_records')
|
||||||
|
} finally {
|
||||||
|
external.close()
|
||||||
|
}
|
||||||
|
database.replaceActivityHistory(snapshot)
|
||||||
|
expect(database.getActivityHistory()).toEqual(snapshot)
|
||||||
|
database.clearAssistantData()
|
||||||
|
database.replaceActivityHistory(snapshot)
|
||||||
|
expect(database.getActivityHistory()).toEqual(snapshot)
|
||||||
|
} finally {
|
||||||
|
database.close()
|
||||||
|
rmSync(directory, { recursive: true, force: true })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|||||||
@@ -1799,6 +1799,7 @@ export class AssistantDatabase {
|
|||||||
private static readonly executionStatsCacheTtlMs = 30_000
|
private static readonly executionStatsCacheTtlMs = 30_000
|
||||||
private static readonly executionStatsCacheLimit = 8
|
private static readonly executionStatsCacheLimit = 8
|
||||||
private database?: DatabaseSync
|
private database?: DatabaseSync
|
||||||
|
private activityHistoryCache?: { records: Map<string, string>; dataVersion: number }
|
||||||
private executionStatsReader?: ExecutionStatsReader
|
private executionStatsReader?: ExecutionStatsReader
|
||||||
private readonly noteStorage: MagicNoteStorage
|
private readonly noteStorage: MagicNoteStorage
|
||||||
private readonly dirtyMagicNotes = new Set<string>()
|
private readonly dirtyMagicNotes = new Set<string>()
|
||||||
@@ -2220,6 +2221,7 @@ export class AssistantDatabase {
|
|||||||
this.executionStatsReader?.close()
|
this.executionStatsReader?.close()
|
||||||
this.executionStatsReader = undefined
|
this.executionStatsReader = undefined
|
||||||
this.executionStatsCache.clear()
|
this.executionStatsCache.clear()
|
||||||
|
this.activityHistoryCache = undefined
|
||||||
this.database?.close()
|
this.database?.close()
|
||||||
this.database = undefined
|
this.database = undefined
|
||||||
this.subagentProgress = undefined
|
this.subagentProgress = undefined
|
||||||
@@ -2277,6 +2279,7 @@ export class AssistantDatabase {
|
|||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
this.dirtyMagicNotes.clear()
|
this.dirtyMagicNotes.clear()
|
||||||
|
this.activityHistoryCache = undefined
|
||||||
this.pendingMagicNoteCleanup.clear()
|
this.pendingMagicNoteCleanup.clear()
|
||||||
this.reconcileMagicNoteFiles()
|
this.reconcileMagicNoteFiles()
|
||||||
this.options.onMagicNotesChanged?.()
|
this.options.onMagicNotesChanged?.()
|
||||||
@@ -4877,8 +4880,13 @@ export class AssistantDatabase {
|
|||||||
const database = this.requireDatabase()
|
const database = this.requireDatabase()
|
||||||
database.exec('BEGIN IMMEDIATE')
|
database.exec('BEGIN IMMEDIATE')
|
||||||
try {
|
try {
|
||||||
this.writeActivityHistory(database, snapshot)
|
const { data_version: dataVersion } = database.prepare('PRAGMA data_version').get() as { data_version: number }
|
||||||
|
const previous = this.activityHistoryCache?.dataVersion === dataVersion
|
||||||
|
? this.activityHistoryCache.records : undefined
|
||||||
|
const records = this.writeActivityHistory(database, snapshot, undefined, previous)
|
||||||
database.exec('COMMIT')
|
database.exec('COMMIT')
|
||||||
|
// Publish only committed records; a failed save must remain retryable.
|
||||||
|
this.activityHistoryCache = { records, dataVersion }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
database.exec('ROLLBACK')
|
database.exec('ROLLBACK')
|
||||||
throw error
|
throw error
|
||||||
@@ -4888,12 +4896,16 @@ export class AssistantDatabase {
|
|||||||
private writeActivityHistory(
|
private writeActivityHistory(
|
||||||
database: DatabaseSync,
|
database: DatabaseSync,
|
||||||
snapshot: ActivityHistorySnapshot,
|
snapshot: ActivityHistorySnapshot,
|
||||||
onProgress?: (processed: number) => void
|
onProgress?: (processed: number) => void,
|
||||||
): void {
|
previous?: ReadonlyMap<string, string>
|
||||||
const rows = database.prepare(
|
): Map<string, string> {
|
||||||
'SELECT record_key, record_json FROM activity_history_records'
|
if (!previous) {
|
||||||
).all() as Array<{ record_key: string; record_json: string }>
|
const rows = database.prepare(
|
||||||
const previous = new Map(rows.map((row) => [row.record_key, row.record_json]))
|
'SELECT record_key, record_json FROM activity_history_records'
|
||||||
|
).all() as Array<{ record_key: string; record_json: string }>
|
||||||
|
previous = new Map(rows.map((row) => [row.record_key, row.record_json]))
|
||||||
|
}
|
||||||
|
const records = new Map<string, string>()
|
||||||
const occurrences = new Map<string, number>()
|
const occurrences = new Map<string, number>()
|
||||||
const order: string[] = []
|
const order: string[] = []
|
||||||
const upsert = database.prepare(
|
const upsert = database.prepare(
|
||||||
@@ -4909,10 +4921,12 @@ export class AssistantDatabase {
|
|||||||
const json = JSON.stringify(record)
|
const json = JSON.stringify(record)
|
||||||
order.push(key)
|
order.push(key)
|
||||||
if (previous.get(key) !== json) upsert.run(key, json)
|
if (previous.get(key) !== json) upsert.run(key, json)
|
||||||
previous.delete(key)
|
records.set(key, json)
|
||||||
}
|
}
|
||||||
const remove = database.prepare('DELETE FROM activity_history_records WHERE record_key = ?')
|
const remove = database.prepare('DELETE FROM activity_history_records WHERE record_key = ?')
|
||||||
for (const key of previous.keys()) remove.run(key)
|
for (const key of previous.keys()) {
|
||||||
|
if (!records.has(key)) remove.run(key)
|
||||||
|
}
|
||||||
const orderJson = JSON.stringify(order)
|
const orderJson = JSON.stringify(order)
|
||||||
const incomplete = Number(snapshot.legacyHistoryMayBeIncomplete)
|
const incomplete = Number(snapshot.legacyHistoryMayBeIncomplete)
|
||||||
database.prepare(
|
database.prepare(
|
||||||
@@ -4921,6 +4935,7 @@ export class AssistantDatabase {
|
|||||||
WHERE singleton = 1
|
WHERE singleton = 1
|
||||||
AND (record_order_json != ? OR legacy_history_may_be_incomplete != ?)`
|
AND (record_order_json != ? OR legacy_history_may_be_incomplete != ?)`
|
||||||
).run(orderJson, incomplete, orderJson, incomplete)
|
).run(orderJson, incomplete, orderJson, incomplete)
|
||||||
|
return records
|
||||||
}
|
}
|
||||||
|
|
||||||
createTask(input: {
|
createTask(input: {
|
||||||
|
|||||||
@@ -151,6 +151,7 @@ function sidebarElement({
|
|||||||
workspaceProjectId,
|
workspaceProjectId,
|
||||||
restoreFocusRef,
|
restoreFocusRef,
|
||||||
activeConversationId,
|
activeConversationId,
|
||||||
|
conversationTitles = new Map(),
|
||||||
supervisionEnabled,
|
supervisionEnabled,
|
||||||
supervisionLibraries,
|
supervisionLibraries,
|
||||||
conversationStats,
|
conversationStats,
|
||||||
@@ -191,6 +192,7 @@ function sidebarElement({
|
|||||||
workspaceProjectId?: string
|
workspaceProjectId?: string
|
||||||
restoreFocusRef?: { current: HTMLElement | null }
|
restoreFocusRef?: { current: HTMLElement | null }
|
||||||
activeConversationId?: string
|
activeConversationId?: string
|
||||||
|
conversationTitles?: ReadonlyMap<string, string>
|
||||||
supervisionEnabled?: boolean
|
supervisionEnabled?: boolean
|
||||||
supervisionLibraries?: React.ComponentProps<typeof RightAssistantSidebar>['supervisionLibraries']
|
supervisionLibraries?: React.ComponentProps<typeof RightAssistantSidebar>['supervisionLibraries']
|
||||||
conversationStats?: React.ComponentProps<typeof RightAssistantSidebar>['conversationStats']
|
conversationStats?: React.ComponentProps<typeof RightAssistantSidebar>['conversationStats']
|
||||||
@@ -221,7 +223,7 @@ function sidebarElement({
|
|||||||
browserStates={browserStates}
|
browserStates={browserStates}
|
||||||
schedules={[]}
|
schedules={[]}
|
||||||
tasks={tasks}
|
tasks={tasks}
|
||||||
conversationTitles={new Map()}
|
conversationTitles={conversationTitles}
|
||||||
currentProject={activeProject ?? undefined}
|
currentProject={activeProject ?? undefined}
|
||||||
projectNames={new Map()}
|
projectNames={new Map()}
|
||||||
onCreateCustomTask={onCreateCustomTask}
|
onCreateCustomTask={onCreateCustomTask}
|
||||||
@@ -286,15 +288,37 @@ it.each([false, undefined])('does not mount or query supervision when enabled is
|
|||||||
it('pins the supervision target across conversation switches and restores following when unpinned', async () => {
|
it('pins the supervision target across conversation switches and restores following when unpinned', async () => {
|
||||||
const overview = vi.fn(async () => [])
|
const overview = vi.fn(async () => [])
|
||||||
Object.assign(window.goodbuddy, { supervision: { overview } })
|
Object.assign(window.goodbuddy, { supervision: { overview } })
|
||||||
const view = render(sidebarElement({ activeConversationId: 'A', supervisionEnabled: true }))
|
const conversationTitles = new Map([['A', '会话甲'], ['B', '会话乙']])
|
||||||
|
const view = render(sidebarElement({ activeConversationId: 'A', conversationTitles, supervisionEnabled: true }))
|
||||||
|
expect(screen.getByText('会话:会话甲')).toBeVisible()
|
||||||
|
const card = within(screen.getByRole('region', { name: '监督反馈' }))
|
||||||
|
expect(card.getByRole('button', { name: '固定监督目标' })).toHaveAttribute('aria-pressed', 'false')
|
||||||
await waitFor(() => expect(overview).toHaveBeenLastCalledWith({ target: { type: 'conversation', conversationId: 'A' } }))
|
await waitFor(() => expect(overview).toHaveBeenLastCalledWith({ target: { type: 'conversation', conversationId: 'A' } }))
|
||||||
fireEvent.click(screen.getByRole('button', { name: '固定监督目标' }))
|
fireEvent.click(screen.getByRole('button', { name: '固定监督目标' }))
|
||||||
view.rerender(sidebarElement({ activeConversationId: 'B', supervisionEnabled: true }))
|
view.rerender(sidebarElement({ activeConversationId: 'B', conversationTitles, supervisionEnabled: true }))
|
||||||
|
expect(screen.getByText('已固定 · 会话:会话甲')).toBeVisible()
|
||||||
|
expect(card.getByRole('button', { name: '取消固定监督目标' })).toHaveAttribute('aria-pressed', 'true')
|
||||||
expect(screen.getByRole('button', { name: '取消固定监督目标' })).toBeInTheDocument()
|
expect(screen.getByRole('button', { name: '取消固定监督目标' })).toBeInTheDocument()
|
||||||
expect(overview).toHaveBeenLastCalledWith({ target: { type: 'conversation', conversationId: 'A' } })
|
expect(overview).toHaveBeenLastCalledWith({ target: { type: 'conversation', conversationId: 'A' } })
|
||||||
expect(localStorage.getItem('goodbuddy.workbar-layout.v1')).toContain('"conversationId":"A"')
|
expect(localStorage.getItem('goodbuddy.workbar-layout.v1')).toContain('"conversationId":"A"')
|
||||||
fireEvent.click(screen.getByRole('button', { name: '取消固定监督目标' }))
|
fireEvent.click(screen.getByRole('button', { name: '取消固定监督目标' }))
|
||||||
await waitFor(() => expect(overview).toHaveBeenLastCalledWith({ target: { type: 'conversation', conversationId: 'B' } }))
|
await waitFor(() => expect(overview).toHaveBeenLastCalledWith({ target: { type: 'conversation', conversationId: 'B' } }))
|
||||||
|
expect(screen.getByText('会话:会话乙')).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows the task name while following and pinned, without displaying its raw ID', async () => {
|
||||||
|
const overview = vi.fn(async () => [])
|
||||||
|
Object.assign(window.goodbuddy, { supervision: { overview } })
|
||||||
|
const tasks = [{ id: 'task-uuid', title: '核对交付清单', conversationId: 'A' }] as AssistantTask[]
|
||||||
|
const view = render(sidebarElement({ tasks, selectedTaskId: 'task-uuid', supervisionEnabled: true }))
|
||||||
|
expect(screen.getByText('任务:核对交付清单')).toHaveAttribute('title', 'task-uuid')
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: '固定监督目标' }))
|
||||||
|
view.rerender(sidebarElement({ tasks, activeConversationId: 'B', supervisionEnabled: true }))
|
||||||
|
expect(screen.getByText('已固定 · 任务:核对交付清单')).toBeVisible()
|
||||||
|
const callsBeforeRefresh = overview.mock.calls.length
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: '刷新监督回顾' }))
|
||||||
|
await waitFor(() => expect(overview).toHaveBeenCalledTimes(callsBeforeRefresh + 1))
|
||||||
|
await waitFor(() => expect(overview).toHaveBeenLastCalledWith({ target: { type: 'task', taskId: 'task-uuid' } }))
|
||||||
})
|
})
|
||||||
|
|
||||||
it.each(['cancel', 'failure'] as const)('retries supervision knowledge preview after %s and shows returned content', async (outcome) => {
|
it.each(['cancel', 'failure'] as const)('retries supervision knowledge preview after %s and shows returned content', async (outcome) => {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
FileText,
|
FileText,
|
||||||
Maximize2,
|
Maximize2,
|
||||||
Minimize2,
|
Minimize2,
|
||||||
|
Pin,
|
||||||
Plus,
|
Plus,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
ShieldAlert,
|
ShieldAlert,
|
||||||
@@ -216,6 +217,9 @@ type SupervisionCardProps = {
|
|||||||
target?: SupervisionTarget
|
target?: SupervisionTarget
|
||||||
activeConversationId?: string
|
activeConversationId?: string
|
||||||
conversationTitle?: string
|
conversationTitle?: string
|
||||||
|
taskTitle?: string
|
||||||
|
pinned?: boolean
|
||||||
|
onTogglePinned?: () => void
|
||||||
supervisionLibraries?: KnowledgeLibrary[]
|
supervisionLibraries?: KnowledgeLibrary[]
|
||||||
onContinueSupervision?: (prompt: string, conversationId: string) => Promise<void>
|
onContinueSupervision?: (prompt: string, conversationId: string) => Promise<void>
|
||||||
onOpenSupervisionConversation?: (conversationId: string) => void
|
onOpenSupervisionConversation?: (conversationId: string) => void
|
||||||
@@ -229,6 +233,9 @@ function SupervisionCardContent({
|
|||||||
target,
|
target,
|
||||||
activeConversationId,
|
activeConversationId,
|
||||||
conversationTitle,
|
conversationTitle,
|
||||||
|
taskTitle,
|
||||||
|
pinned = false,
|
||||||
|
onTogglePinned,
|
||||||
supervisionLibraries: libraries,
|
supervisionLibraries: libraries,
|
||||||
onContinueSupervision: onContinue,
|
onContinueSupervision: onContinue,
|
||||||
onOpenSupervisionGraph,
|
onOpenSupervisionGraph,
|
||||||
@@ -310,9 +317,14 @@ function SupervisionCardContent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return <section className="assistant-sidebar__section supervision-card" aria-label="监督反馈">
|
return <section className="assistant-sidebar__section supervision-card" aria-label="监督反馈">
|
||||||
<h3>监督反馈</h3>
|
<div className="supervision-card__header">
|
||||||
{target && <p>{target.type === 'conversation' ? `会话:${conversationTitle ?? target.conversationId}` : `任务:${target.taskId}`}</p>}
|
<h3>监督反馈</h3>
|
||||||
<button type="button" className="link-button" disabled={!target || loading} onClick={() => { setResult(undefined); setSource(undefined); setMessage(''); setRefresh((value) => value + 1) }}>刷新监督回顾</button>
|
<div className="supervision-card__actions">
|
||||||
|
{onTogglePinned && <button type="button" className={`icon-button${pinned ? ' icon-button--active' : ''}`} aria-label={pinned ? '取消固定监督目标' : '固定监督目标'} title={pinned ? '取消固定监督目标' : '固定监督目标'} aria-pressed={pinned} disabled={!target} onClick={onTogglePinned}><Pin aria-hidden="true" size={14} /></button>}
|
||||||
|
<button type="button" className="icon-button" aria-label="刷新监督回顾" title="刷新监督回顾" disabled={!target || loading} onClick={() => { setResult(undefined); setSource(undefined); setMessage(''); setRefresh((value) => value + 1) }}><RefreshCw aria-hidden="true" size={14} /></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{target && <p className="supervision-card__target" title={target.type === 'conversation' ? target.conversationId : target.taskId}>{pinned ? '已固定 · ' : ''}{target.type === 'conversation' ? `会话:${conversationTitle?.trim() || '未命名会话'}` : `任务:${taskTitle?.trim() || '未命名任务'}`}</p>}
|
||||||
{!result ? <p className="assistant-sidebar__empty">{message || '暂无监督回顾'}</p> : <>
|
{!result ? <p className="assistant-sidebar__empty">{message || '暂无监督回顾'}</p> : <>
|
||||||
<p>{result.summary || '监督回顾没有摘要'}</p>
|
<p>{result.summary || '监督回顾没有摘要'}</p>
|
||||||
<p>{result.scope.kind === 'global' ? '全局回顾' : `项目范围:${result.scope.projectIds.join('、')}`} · {new Date(result.timeRange.from).toLocaleString()} – {new Date(result.timeRange.to).toLocaleString()}</p>
|
<p>{result.scope.kind === 'global' ? '全局回顾' : `项目范围:${result.scope.projectIds.join('、')}`} · {new Date(result.timeRange.from).toLocaleString()} – {new Date(result.timeRange.to).toLocaleString()}</p>
|
||||||
@@ -2071,19 +2083,18 @@ export function RightAssistantSidebar({
|
|||||||
{instance.appId === 'tasks' && (
|
{instance.appId === 'tasks' && (
|
||||||
<section className="assistant-sidebar__section task-center">
|
<section className="assistant-sidebar__section task-center">
|
||||||
{supervisionEnabled && <>
|
{supervisionEnabled && <>
|
||||||
<button type="button" className="link-button" disabled={!instance.targetRef && !selectedTaskId && !activeConversationId}
|
|
||||||
onClick={() => setWorkbarInstances((current) => current.map((item) => item.id === instance.id
|
|
||||||
? { ...item, targetRef: item.targetRef ? undefined : selectedTaskId ? { type: 'task', taskId: selectedTaskId } : activeConversationId ? { type: 'conversation', conversationId: activeConversationId } : undefined }
|
|
||||||
: item))}>
|
|
||||||
{instance.targetRef ? '取消固定监督目标' : '固定监督目标'}
|
|
||||||
</button>
|
|
||||||
<SupervisionCard
|
<SupervisionCard
|
||||||
|
pinned={Boolean(instance.targetRef)}
|
||||||
|
onTogglePinned={() => setWorkbarInstances((current) => current.map((item) => item.id === instance.id
|
||||||
|
? { ...item, targetRef: item.targetRef ? undefined : selectedTaskId ? { type: 'task', taskId: selectedTaskId } : activeConversationId ? { type: 'conversation', conversationId: activeConversationId } : undefined }
|
||||||
|
: item))}
|
||||||
target={instance.targetRef?.type === 'conversation' || instance.targetRef?.type === 'task'
|
target={instance.targetRef?.type === 'conversation' || instance.targetRef?.type === 'task'
|
||||||
? instance.targetRef
|
? instance.targetRef
|
||||||
: selectedTaskId ? { type: 'task', taskId: selectedTaskId }
|
: selectedTaskId ? { type: 'task', taskId: selectedTaskId }
|
||||||
: activeConversationId ? { type: 'conversation', conversationId: activeConversationId } : undefined}
|
: activeConversationId ? { type: 'conversation', conversationId: activeConversationId } : undefined}
|
||||||
activeConversationId={instance.targetRef?.type === 'conversation' ? instance.targetRef.conversationId : instance.targetRef?.type === 'task' ? tasks.find((task) => instance.targetRef?.type === 'task' && task.id === instance.targetRef.taskId)?.conversationId : activeConversationId}
|
activeConversationId={instance.targetRef?.type === 'conversation' ? instance.targetRef.conversationId : instance.targetRef?.type === 'task' ? tasks.find((task) => instance.targetRef?.type === 'task' && task.id === instance.targetRef.taskId)?.conversationId : activeConversationId}
|
||||||
conversationTitle={instance.targetRef?.type === 'conversation' ? conversationTitles.get(instance.targetRef.conversationId) : undefined}
|
conversationTitle={conversationTitles.get(instance.targetRef?.type === 'conversation' ? instance.targetRef.conversationId : instance.targetRef?.type === 'task' ? tasks.find((task) => instance.targetRef?.type === 'task' && task.id === instance.targetRef.taskId)?.conversationId ?? '' : activeConversationId ?? '')}
|
||||||
|
taskTitle={tasks.find((task) => task.id === (instance.targetRef?.type === 'task' ? instance.targetRef.taskId : selectedTaskId))?.title}
|
||||||
supervisionLibraries={supervisionLibraries}
|
supervisionLibraries={supervisionLibraries}
|
||||||
onOpenSupervisionGraph={onOpenSupervisionGraph}
|
onOpenSupervisionGraph={onOpenSupervisionGraph}
|
||||||
onContinueSupervision={onContinueSupervision}
|
onContinueSupervision={onContinueSupervision}
|
||||||
|
|||||||
@@ -6,6 +6,17 @@ import i18n from './i18n'
|
|||||||
afterEach(() => { cleanup(); vi.restoreAllMocks(); window.goodbuddy = {} as never })
|
afterEach(() => { cleanup(); vi.restoreAllMocks(); window.goodbuddy = {} as never })
|
||||||
const result = (id: string) => ({ id, storyLineId: id, sourceId: `source-${id}`, summary: `${id} recap`, scope: { kind: 'global' }, timeRange: { from: '2026-09-01T00:00:00Z', to: '2026-09-22T00:00:00Z' } })
|
const result = (id: string) => ({ id, storyLineId: id, sourceId: `source-${id}`, summary: `${id} recap`, scope: { kind: 'global' }, timeRange: { from: '2026-09-01T00:00:00Z', to: '2026-09-22T00:00:00Z' } })
|
||||||
|
|
||||||
|
it('keeps unknown target IDs out of visible copy and disables actions without a target', async () => {
|
||||||
|
window.goodbuddy = { supervision: { overview: async () => [] } } as never
|
||||||
|
const view = render(<SupervisionCard target={{ type: 'conversation', conversationId: 'raw-uuid' }} onTogglePinned={vi.fn()} />)
|
||||||
|
expect(screen.getByText('会话:未命名会话')).toHaveAttribute('title', 'raw-uuid')
|
||||||
|
expect(screen.queryByText(/raw-uuid/)).not.toBeInTheDocument()
|
||||||
|
await act(async () => {})
|
||||||
|
view.rerender(<SupervisionCard onTogglePinned={vi.fn()} />)
|
||||||
|
expect(screen.getByRole('button', { name: '固定监督目标' })).toBeDisabled()
|
||||||
|
expect(screen.getByRole('button', { name: '刷新监督回顾' })).toBeDisabled()
|
||||||
|
})
|
||||||
|
|
||||||
it('graph navigation exposes only the matched result through a native bilingual button', async () => {
|
it('graph navigation exposes only the matched result through a native bilingual button', async () => {
|
||||||
const open = vi.fn()
|
const open = vi.fn()
|
||||||
const overview = vi.fn().mockResolvedValueOnce([result('pinned')]).mockResolvedValueOnce([])
|
const overview = vi.fn().mockResolvedValueOnce([result('pinned')]).mockResolvedValueOnce([])
|
||||||
|
|||||||
@@ -1,3 +1,37 @@
|
|||||||
|
.assistant-sidebar__section.supervision-card {
|
||||||
|
min-width: 0;
|
||||||
|
padding: var(--space-3);
|
||||||
|
border-bottom: 1px solid var(--border-subtle);
|
||||||
|
gap: var(--space-2);
|
||||||
|
font-size: var(--font-body);
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.supervision-card__header,
|
||||||
|
.supervision-card__actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.supervision-card__header { justify-content: space-between; }
|
||||||
|
.supervision-card__actions { flex-wrap: wrap; }
|
||||||
|
.supervision-card__header > .supervision-card__actions { flex: 0 0 auto; }
|
||||||
|
.supervision-card .supervision-card__header h3,
|
||||||
|
.supervision-card p { margin: 0; }
|
||||||
|
.supervision-card__target { color: var(--text-secondary); }
|
||||||
|
.supervision-card__source {
|
||||||
|
padding: var(--space-3);
|
||||||
|
border-radius: var(--radius-control);
|
||||||
|
background: var(--surface-subtle);
|
||||||
|
}
|
||||||
|
.supervision-card__actions select { min-width: 0; max-width: 100%; }
|
||||||
|
.supervision-card .icon-button:disabled {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
.task-center .task-center__scope {
|
.task-center .task-center__scope {
|
||||||
margin: 0 0 var(--space-2);
|
margin: 0 0 var(--space-2);
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import { createServer } from 'vite'
|
|||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
import { expect, it } from 'vitest'
|
import { expect, it } from 'vitest'
|
||||||
|
|
||||||
it('renders the production supervisor at desktop, narrow, and mobile widths', async () => {
|
it(process.env.GOODBUDDY_SUPERVISOR_SIDEBAR
|
||||||
|
? 'renders the production supervision card at narrow widths in both themes'
|
||||||
|
: 'renders the production supervisor at desktop, narrow, and mobile widths', async () => {
|
||||||
const directory = await mkdtemp(join(tmpdir(), 'goodbuddy-supervisor-'))
|
const directory = await mkdtemp(join(tmpdir(), 'goodbuddy-supervisor-'))
|
||||||
const css = await readFile(
|
const css = await readFile(
|
||||||
'src/renderer/src/supervisor-workspace.css',
|
'src/renderer/src/supervisor-workspace.css',
|
||||||
@@ -45,7 +47,7 @@ it('renders the production supervisor at desktop, narrow, and mobile widths', as
|
|||||||
const env: NodeJS.ProcessEnv = {
|
const env: NodeJS.ProcessEnv = {
|
||||||
...process.env,
|
...process.env,
|
||||||
GOODBUDDY_SUPERVISOR_URL:
|
GOODBUDDY_SUPERVISOR_URL:
|
||||||
server.resolvedUrls!.local[0] + 'supervisor.html',
|
server.resolvedUrls!.local[0] + 'supervisor.html' + (process.env.GOODBUDDY_SUPERVISOR_SIDEBAR ? '?sidebar=1' : ''),
|
||||||
GOODBUDDY_SUPERVISOR_DIRECTORY: directory,
|
GOODBUDDY_SUPERVISOR_DIRECTORY: directory,
|
||||||
GOODBUDDY_SUPERVISOR_TOKENS: JSON.stringify([
|
GOODBUDDY_SUPERVISOR_TOKENS: JSON.stringify([
|
||||||
...new Set(
|
...new Set(
|
||||||
|
|||||||
@@ -61,6 +61,51 @@ app
|
|||||||
await settle()
|
await settle()
|
||||||
}
|
}
|
||||||
const reports = []
|
const reports = []
|
||||||
|
if (process.env.GOODBUDDY_SUPERVISOR_SIDEBAR) {
|
||||||
|
await win.loadURL(process.env.GOODBUDDY_SUPERVISOR_URL)
|
||||||
|
await wait('!!document.querySelector(".supervision-card")')
|
||||||
|
await js('document.fonts.ready')
|
||||||
|
win.show()
|
||||||
|
win.focus()
|
||||||
|
for (const theme of ['light', 'dark']) {
|
||||||
|
await js(`document.documentElement.dataset.theme = '${theme}'`)
|
||||||
|
for (const width of [480, 300, 200]) {
|
||||||
|
win.setContentSize(width, 640)
|
||||||
|
await js('document.querySelector(".supervision-card__header button").focus()')
|
||||||
|
win.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Tab' })
|
||||||
|
win.webContents.sendInputEvent({ type: 'keyUp', keyCode: 'Tab' })
|
||||||
|
await settle()
|
||||||
|
const report = await js(`(() => {
|
||||||
|
const card = document.querySelector('.supervision-card');
|
||||||
|
const header = card.querySelector('.supervision-card__header');
|
||||||
|
const buttons = [...header.querySelectorAll('button')];
|
||||||
|
return {
|
||||||
|
width: innerWidth, scrollWidth: document.documentElement.scrollWidth,
|
||||||
|
cardWidth: card.clientWidth, cardScrollWidth: card.scrollWidth,
|
||||||
|
headerHeight: header.getBoundingClientRect().height,
|
||||||
|
buttons: buttons.map(b => ({ width: b.getBoundingClientRect().width,
|
||||||
|
height: b.getBoundingClientRect().height, background: getComputedStyle(b).backgroundColor })),
|
||||||
|
focus: getComputedStyle(document.activeElement).outlineStyle,
|
||||||
|
text: card.textContent
|
||||||
|
};
|
||||||
|
})()`)
|
||||||
|
assert(report.scrollWidth <= report.width, `Page overflow: ${JSON.stringify(report)}`)
|
||||||
|
assert(report.cardScrollWidth <= report.cardWidth, 'Card overflow')
|
||||||
|
assert.equal(report.headerHeight, 34)
|
||||||
|
assert(report.buttons.every(b => b.width === 34 && b.height === 34), 'Compact shared buttons')
|
||||||
|
assert.equal(report.focus, 'solid')
|
||||||
|
assert(!report.text.includes('simulated-conversation-uuid'), 'Raw UUID visible')
|
||||||
|
reports.push({ theme, ...report })
|
||||||
|
await writeFile(join(artifacts, `sidebar-${theme}-${width}.png`), (await win.webContents.capturePage()).toPNG())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.deepEqual(errors, [])
|
||||||
|
await writeFile(join(artifacts, 'sidebar-measurements.json'), JSON.stringify(reports, null, 2))
|
||||||
|
console.log(JSON.stringify({ artifacts, cases: reports.length, errors }))
|
||||||
|
win.destroy()
|
||||||
|
app.quit()
|
||||||
|
return
|
||||||
|
}
|
||||||
await open(false)
|
await open(false)
|
||||||
await js(
|
await js(
|
||||||
'document.querySelector(".supervisor-workspace__legend .link-button").click()'
|
'document.querySelector(".supervisor-workspace__legend .link-button").click()'
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Simulated visual fixture only. No preload, user database, or model is used.
|
// Simulated visual fixture only. No preload, user database, or model is used.
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
import { HeartbeatCenter } from '../../src/renderer/src/HeartbeatCenter'
|
import { HeartbeatCenter } from '../../src/renderer/src/HeartbeatCenter'
|
||||||
|
import { SupervisionCard } from '../../src/renderer/src/RightAssistantSidebar'
|
||||||
import { PageShell } from '../../src/renderer/src/WorkspacePrimitives'
|
import { PageShell } from '../../src/renderer/src/WorkspacePrimitives'
|
||||||
import { UiLocaleProvider } from '../../src/renderer/src/i18n/UiLocaleProvider'
|
import { UiLocaleProvider } from '../../src/renderer/src/i18n/UiLocaleProvider'
|
||||||
import type { SupervisionGraphView } from '../../src/shared/supervision-contracts'
|
import type { SupervisionGraphView } from '../../src/shared/supervision-contracts'
|
||||||
@@ -202,6 +203,7 @@ createRoot(document.getElementById('root')!).render(
|
|||||||
color: 'var(--text-secondary)',
|
color: 'var(--text-secondary)',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexWrap: 'wrap',
|
flexWrap: 'wrap',
|
||||||
|
overflowWrap: 'anywhere',
|
||||||
gap: 'var(--space-3)'
|
gap: 'var(--space-3)'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -212,7 +214,14 @@ createRoot(document.getElementById('root')!).render(
|
|||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<PageShell variant="supervisor">
|
{params.has('sidebar') ? <div className="assistant-sidebar__section task-center">
|
||||||
|
<SupervisionCard
|
||||||
|
target={{ type: 'conversation', conversationId: 'simulated-conversation-uuid' }}
|
||||||
|
conversationTitle={'模拟会话:核对项目交付清单与阶段回顾'.repeat(3)}
|
||||||
|
pinned
|
||||||
|
onTogglePinned={() => {}}
|
||||||
|
/>
|
||||||
|
</div> : <PageShell variant="supervisor">
|
||||||
<HeartbeatCenter
|
<HeartbeatCenter
|
||||||
configs={[]}
|
configs={[]}
|
||||||
runs={[]}
|
runs={[]}
|
||||||
@@ -231,7 +240,7 @@ createRoot(document.getElementById('root')!).render(
|
|||||||
onUseFollowUpTask={() => {}}
|
onUseFollowUpTask={() => {}}
|
||||||
onRetryLoad={noop}
|
onRetryLoad={noop}
|
||||||
/>
|
/>
|
||||||
</PageShell>
|
</PageShell>}
|
||||||
</div>
|
</div>
|
||||||
</UiLocaleProvider>
|
</UiLocaleProvider>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user