mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-01 15:30:52 +08:00
Release v1.107.0
This commit is contained in:
@@ -89,6 +89,7 @@ internal/exec-plans/*
|
||||
!internal/exec-plans/project-gate-a-b-acceptance-plan.md
|
||||
!internal/exec-plans/refactor-v2-test-phase-2-plan.md
|
||||
!internal/exec-plans/codex-conversation-compat-refactor-plan.md
|
||||
!internal/exec-plans/codex-app-gui-alignment-plan.md
|
||||
internal/roadmap/**
|
||||
!internal/roadmap/
|
||||
!internal/roadmap/long-term/
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
- 参考运行时主链总计划:`internal/exec-plans/upstream-runtime-alignment-plan.md`
|
||||
- 参考运行时主链进度日志:`internal/exec-plans/upstream-runtime-alignment-progress.md`
|
||||
- Refactor v2 全项目 Gate A/B 验收计划:`internal/exec-plans/project-gate-a-b-acceptance-plan.md`
|
||||
- Codex App GUI 对齐执行计划:`internal/exec-plans/codex-app-gui-alignment-plan.md`
|
||||
- Codex 对话兼容重构:`internal/exec-plans/codex-conversation-compat-refactor-plan.md`
|
||||
- Refactor v2 测试体系第二期计划:`internal/exec-plans/refactor-v2-test-phase-2-plan.md`
|
||||
- Agent Runtime 单一投影入口收口计划:`internal/exec-plans/agent-runtime-single-projection-entry-plan.md`
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
# Codex App GUI 对齐执行计划
|
||||
|
||||
> status: active / G1-in-progress
|
||||
> owner: agent-ui + app-server
|
||||
> started: 2026-07-18
|
||||
> target: Codex App GUI interaction model
|
||||
> backend reference: `/Users/coso/Documents/dev/rust/codex/codex-rs`
|
||||
> architecture baseline: `internal/research/refactor/v2/04-target/architecture.md`
|
||||
> verification baseline: `internal/research/refactor/v2/11-gui/electron-gates.md`
|
||||
|
||||
## 1. 主目标
|
||||
|
||||
把 Lime 的 Agent 主界面从“营销首页 + 聊天消息 + 多套诊断/工作台入口”收敛为以当前 Thread 为中心的桌面工作区:用户持续知道正在处理哪个任务、使用哪个本地环境、当前 Turn 到了哪一步、是否需要响应,以及结果和变更在哪里。
|
||||
|
||||
固定产品链继续为:
|
||||
|
||||
```text
|
||||
Electron Desktop Host
|
||||
-> App Server JSON-RPC
|
||||
-> RuntimeCore / agent-runtime
|
||||
-> Thread / Turn / Item projection
|
||||
-> Codex App-style React GUI
|
||||
```
|
||||
|
||||
Codex App 只作为 GUI 信息架构和交互层级参考;后端 Thread/Turn/Item、审批、队列、恢复、环境和工具生命周期直接对齐 `codex-rs`。不复制 TUI cell、ANSI、终端快捷键或 CLI onboarding。
|
||||
|
||||
## 2. 当前阶段与下一刀
|
||||
|
||||
- 当前阶段:v2 runtime/projection 骨架完成后的 GUI 差距审计。
|
||||
- 已完成:源码结构、三张参考截图、浏览器镜像桌面视口和 Codex App Server/TUI 能力对照。
|
||||
- 下一刀:`G1 Thread workspace shell`,先收敛 active Thread 的桌面壳,不新增协议,不重写 runtime。
|
||||
- 当前写集:`src/hooks/useAppShellLayout.ts`、`src/components/app-sidebar/useAppSidebarConversationActions.ts`、`src/components/agent/chat/AgentChatWorkspace.tsx`、`src/components/agent/chat/workspace/useTaskCenterChromeNavigationRuntime.ts`、`src/components/agent/chat/workspace/useTaskCenterTabChrome.tsx`、`src/components/agent/chat/workspace/taskCenterTabProjection.ts` 及相邻测试。
|
||||
- 避让:`lime-rs/crates/app-server/src/runtime/read_model/tests.rs` 当前存在未知改动,本计划不触碰。
|
||||
|
||||
## 3. 基线证据
|
||||
|
||||
### 3.1 已有但未形成单一产品面
|
||||
|
||||
- `TaskCenterUtilityToolbar.tsx` 已有打开位置、环境信息、Git 分支和 panel 入口。
|
||||
- `InputbarProjectContextBar.tsx` 已有项目、分支和 worktree 控制。
|
||||
- `CanvasWorkbenchLayout` 已有文件、结果、变更和 review surface。
|
||||
- canonical `thread/read`、Item materialization、审批、计划、SubAgent 和恢复投影已有 v2 evidence。
|
||||
|
||||
### 3.2 当前结构性差距
|
||||
|
||||
- `AgentChatWorkspace.tsx` 仍有约 2422 行,超过 v2 目标 `< 800` 行;scene composition 仍承接大量编排。
|
||||
- `TaskCenterUtilityToolbar.tsx` 约 972 行,同时拥有环境读取、任务轨道、App 打开和多个 panel 控制。
|
||||
- 普通 Agent 会话仍默认保留全局 App Sidebar;当前 Thread 缺少稳定、紧凑的页头主对象。
|
||||
- 首页首屏仍是皮肤 Hero、营销文案和技能入口,和 Codex App 的任务工作区心智不一致。
|
||||
- 环境、计划、审批、运行状态同时出现在 Toolbar popover、Inputbar、Timeline、Harness、Canvas session view 等多套 surface。
|
||||
- 主写链仍有大量 `agentSession/*`;Codex-rs current contract 是 `thread/*`、`turn/*`,并包含 `turn/steer`、`thread/fork`、`thread/rollback`、`environment/info`、`environment/status` 等能力。
|
||||
|
||||
### 3.3 本轮 GUI 证据边界
|
||||
|
||||
- `http://127.0.0.1:1420/` 浏览器镜像在 1536x960 下完成首页、空任务和历史会话外壳检查;控制台 error 为 0。
|
||||
- 证据等级为 Gate A,仅证明 Renderer 当前布局;Electron 已运行但未开放 `9223` CDP,未取得 Gate B。
|
||||
|
||||
## 4. 差距优先级
|
||||
|
||||
| 优先级 | 目标 | 当前差距 | 依赖 |
|
||||
| --- | --- | --- | --- |
|
||||
| P0 | 当前 Thread 成为唯一主对象 | 左侧全局导航、项目 tabs、任务 tabs 和消息区竞争上下文;活跃任务页头不稳定 | GUI-only,可先做 |
|
||||
| P0 | 单一 canonical 时间线 | commentary、reasoning、tool、状态摘要存在重复渲染与过量纵向展开 | `thread/read` Item projection |
|
||||
| P0 | 单一 action-required 入口 | Approval/Plan/request-user-input 可同时出现在输入区、时间线和环境任务轨道 | canonical Approval/Plan Item |
|
||||
| P0 | 写链对齐 Codex-rs | `agentSession/turn/start`、cancel、resume 等仍是主写 contract | App Server protocol/runtime |
|
||||
| P1 | Thread 绑定的环境面 | 现有环境浮层只读 project Git,且把任务轨道混入环境面;缺 environment identity/status | `environment/*` + project Git |
|
||||
| P1 | 变更审查闭环 | 已有 diff/workbench,但环境面没有增删行、比较基线和进入 review 的明确动作 | project Git + workbench command |
|
||||
| P1 | Thread 生命周期完整 | list/read 基本存在,fork/rollback/archive/resume/steer 未形成一致 GUI | `thread/*`、`turn/*` |
|
||||
| P1 | Composer 稳定 | 首页与会话输入区形态、装饰和高度变化明显;运行态/排队态入口分散 | command model |
|
||||
| P2 | 产品/诊断分层 | Harness、Trace、Shell、Browser、Workbench 在主 Toolbar 近似等权 | 右侧 surface catalog |
|
||||
| P2 | 首页对齐任务产品 | Hero 和技能陈列压过新任务输入,不像工作型桌面应用 | shell/navigation |
|
||||
|
||||
## 5. 实施切片
|
||||
|
||||
### G1:Thread workspace shell
|
||||
|
||||
目标:active Thread 首屏只保留紧凑任务页头、主时间线、稳定 composer 和一个右侧上下文入口。
|
||||
|
||||
动作:
|
||||
|
||||
- 从 canonical Topic/Thread metadata 派生标题、状态和工作目录。
|
||||
- 普通历史会话进入专注工作区;全局导航改为可恢复的抽屉/返回入口,不长期占用主画布。
|
||||
- “打开位置”使用带文字的明确命令;环境入口和右侧 surface 保持单一。
|
||||
- 不在本切片新增 Git、Thread 或 Environment method。
|
||||
|
||||
退出条件:用户 5 秒内能识别当前任务、当前状态和下一步;桌面宽屏不再出现左侧全局导航与任务 tabs 双导航竞争。
|
||||
|
||||
### G2:Canonical timeline
|
||||
|
||||
目标:一个 Thread Item 只由一个 renderer 呈现。
|
||||
|
||||
动作:
|
||||
|
||||
- User/Agent/Reasoning/Tool/Approval/FileChange/Artifact/SubAgent/Compaction 使用稳定 Item renderer registry。
|
||||
- completed tool 默认压成紧凑语义行;running/failed/pending 才展开必要详情。
|
||||
- commentary 与 final answer 保持连续阅读;删除重复的“先发起这一步/已找到/已处理”二次摘要。
|
||||
- 全局只保留一个 active Turn 状态行。
|
||||
|
||||
退出条件:同一 tool/approval/plan 不会在 Timeline、Harness 和 environment popover 同时成为主操作。
|
||||
|
||||
### G3:Composer 与 action-required
|
||||
|
||||
目标:composer 是 send/steer/interrupt/approval/request-user-input 的单一命令面。
|
||||
|
||||
动作:
|
||||
|
||||
- 空闲态发送,运行态 steer/queue,执行态 interrupt,等待态响应表单。
|
||||
- Approval、Plan decision、request-user-input 互斥占用 action slot;历史结果只在时间线只读回显。
|
||||
- 权限、模型和工作目录显示有效值,不从 Renderer 猜 runtime truth。
|
||||
|
||||
退出条件:任何时刻只有一个主按钮和一个需要用户处理的面。
|
||||
|
||||
### G4:Environment 与 changes
|
||||
|
||||
目标:环境面只回答“任务在哪里运行、代码处于什么状态、下一步如何检查/交付”。
|
||||
|
||||
动作:
|
||||
|
||||
- 后端复制并适配 Codex-rs `environment/info`、`environment/status`;Thread 持有 environment selection。
|
||||
- Git 状态提供 branch、upstream/base、文件数、added/deleted lines 和 refresh 状态。
|
||||
- “比较分支”进入现有 changes workbench;commit/push 未实现时不显示假按钮。
|
||||
- 删除 Toolbar 与 Inputbar 内重复的 Git fetch owner,收敛为 typed projection。
|
||||
|
||||
退出条件:环境状态刷新不依赖组件各自发请求;失败可见且不回退 mock。
|
||||
|
||||
### G5:Thread lifecycle
|
||||
|
||||
目标:GUI 对齐 Codex-rs 的 `thread/start|list|loaded/list|read|resume|fork|archive|unarchive|rollback` 与 `turn/start|steer|interrupt`。
|
||||
|
||||
动作:直接迁移调用并删除对应 `agentSession/*` 主入口,不新增 rename wrapper。
|
||||
|
||||
退出条件:新建、恢复、分叉、回滚、归档、排队、打断都由 canonical notification/read model 回填 GUI。
|
||||
|
||||
### G6:Surface cleanup
|
||||
|
||||
目标:删除对 Codex App 主工作流无贡献的平行产品面。
|
||||
|
||||
候选删除/降级:
|
||||
|
||||
- active Thread 中的营销 Hero 和重复技能陈列。
|
||||
- 主 Toolbar 里的 Harness/Trace 工程词;迁入开发者诊断面。
|
||||
- environment popover 内嵌的完整 Task Rail。
|
||||
- 重复 session overview、runtime strip 和历史状态摘要。
|
||||
- `agentSession/*` 正向 GUI fixture、旧 slash command 占位和无消费 compat surface。
|
||||
|
||||
退出条件:生产 GUI 只消费 current Thread/Turn/Item projection;旧名只留负向 guard 或历史 evidence。
|
||||
|
||||
## 6. 验证门禁
|
||||
|
||||
### 6.1 Agent Verification Contract
|
||||
|
||||
```text
|
||||
改动名称:Codex App GUI 对齐
|
||||
执行计划文件:internal/exec-plans/codex-app-gui-alignment-plan.md
|
||||
负责人:agent-ui + app-server
|
||||
预算标签:budget:normal
|
||||
风险等级:P0
|
||||
影响模块:Agent GUI shell、Thread/Turn/Item projection、App Server typed gateway
|
||||
不做范围:TUI 移植、ANSI/CLI 快捷键、live Provider 质量评估、发布产物
|
||||
```
|
||||
|
||||
Current 主链:
|
||||
|
||||
```text
|
||||
前端入口:AgentChatPage -> AgentChatWorkspace scene composition
|
||||
前端网关:src/lib/api/agentRuntime + packages/app-server-client
|
||||
Electron Desktop Host bridge:app_server_handle_json_lines
|
||||
App Server method:当前 read 为 thread/read;目标写链为 thread/* + turn/*
|
||||
RuntimeCore / service owner:RuntimeCore + agent-runtime + tool-runtime
|
||||
read model:Thread / Turn / Item canonical read model
|
||||
runtime event:accepted/started/queued/running/completed/failed/interrupted + Item lifecycle
|
||||
Evidence Pack 字段:threadId、turnId、itemId、method、transport、status;不记录 secret/完整 prompt
|
||||
GUI surface:Thread workspace header、timeline、composer、environment/changes right surface
|
||||
```
|
||||
|
||||
Happy Path:
|
||||
|
||||
```text
|
||||
用户输入 / Agent 输入:在 active Thread composer 提交任务或运行中 steer
|
||||
预期 runtime events:同一 thread/turn identity 的 accepted -> started -> terminal
|
||||
预期 tool calls:以 canonical Tool Item 在 timeline 单次投影
|
||||
预期 approval / sandbox:action slot 单点响应,历史只读回显
|
||||
预期 artifact:进入 workbench/right surface,不在消息正文伪造状态
|
||||
预期 evidence:Gate B trace + read model + 可见 DOM identity 一致
|
||||
预期 GUI 状态:主对象、当前阶段、阻塞和下一步同时可见
|
||||
失败时应停在哪一层:typed gateway/App Server/runtime 显式失败;禁止生产 mock fallback
|
||||
```
|
||||
|
||||
Evidence Layers:
|
||||
|
||||
| Layer | 本次是否需要 | 证据路径 / 计划路径 | 不需要的原因 |
|
||||
| --- | --- | --- | --- |
|
||||
| deterministic-smoke | 是 | related tests、contracts、current fixture | - |
|
||||
| gui-trace | 是 | 每切片 Gate A + Electron Gate B evidence | - |
|
||||
| runtime-transcript | 是 | current fixture 的 Thread/Turn/Item 摘要 | - |
|
||||
| release-artifact | 否 | - | 本计划不是发版计划 |
|
||||
|
||||
Agent QC 场景映射:
|
||||
|
||||
```text
|
||||
P0:Claw 新建/恢复 Thread、发送、stream、interrupt、approval、历史 hydration
|
||||
P1:environment/changes、fork/rollback/archive、SubAgent activity、窄窗口布局
|
||||
P2:首页和诊断 surface 收口
|
||||
为什么需要:直接改变 Agent GUI 主路径和 action owner
|
||||
为什么不需要其它 P0:媒体/浏览器/live provider 只在对应切片触及时加入
|
||||
是否允许单场景 sidecar:允许,budget:normal 下按受影响场景选择
|
||||
是否允许进入 official evidence:只有 Gate B 与必跑命令全部通过后允许
|
||||
```
|
||||
|
||||
Supervisor:本计划不使用 LLM judge;确定性 contract、DOM、trace 和截图足以判断。失败必须回写到最接近 owner 的 unit/component/contract/Gate B fixture。
|
||||
|
||||
每个用户可见切片至少执行:
|
||||
|
||||
```bash
|
||||
npm run test:related -- <changed-paths...>
|
||||
npm run i18n:check:json
|
||||
npm run verify:gui-smoke
|
||||
```
|
||||
|
||||
协议/后端切片追加:
|
||||
|
||||
```bash
|
||||
npm run test:contracts
|
||||
npm run test:rust:related -- <changed-rust-paths...>
|
||||
npm run smoke:agent-runtime-current-fixture
|
||||
```
|
||||
|
||||
每个 P0/P1 产品切片必须记录:
|
||||
|
||||
- 1536x960、1280x800 和窄窗口 Gate A 截图/DOM 证据。
|
||||
- 真实 Electron Gate B:preload/IPC、`app_server_handle_json_lines`、current method、同一 thread/turn/item identity、mock fallback 为 0。
|
||||
- `zh-CN`、`zh-TW`、`en-US`、`ja-JP`、`ko-KR` 稳定文案回归。
|
||||
|
||||
## 7. 治理分类
|
||||
|
||||
- `current`:Electron -> App Server -> RuntimeCore -> Thread/Turn/Item -> GUI;Codex App-style Thread workspace。
|
||||
- `compat`:本计划不新增。
|
||||
- `deprecated`:迁移期间尚未删除的 `agentSession/*` GUI 写入口,必须逐切片写明退出条件。
|
||||
- `dead / forbidden-to-restore`:重复 Renderer 状态机、生产 mock fallback、与 canonical Item 重复的主操作 surface、仅复刻 TUI 的 UI。
|
||||
|
||||
## 8. 完成度
|
||||
|
||||
- 差距审计:100%。
|
||||
- GUI 对齐实现:0%。
|
||||
- 当前总完成度:10%(已有 v2 runtime/projection 与若干 Codex-like surface,不代表 GUI 对齐完成)。
|
||||
|
||||
## 9. 架构图确认
|
||||
|
||||
```text
|
||||
架构影响:G1-G3 预期为现有 GUI owner 内收口;G4-G5 会修改 protocol、read/write path 和跨层 owner,属于重大架构变更。
|
||||
架构图已更新:当前不适用,尚未实施 G4-G5;实现时必须同步 internal/aiprompts/architecture.md。
|
||||
责任开发者确认:待 G4/G5 实现责任开发者填写,未确认不得标记完成或进入 release evidence。
|
||||
确认内容:待核对目录归属、数据流、依赖方向、协议边界和验证门禁。
|
||||
```
|
||||
|
||||
完成标准:G1-G6 退出条件全部满足,必跑命令通过,五语言回归完成,Gate B 证明同一 Thread/Turn/Item identity 且生产 mock fallback 为 0。当前尚不可进入 release evidence;下一刀仍为 G1。
|
||||
@@ -55,6 +55,7 @@
|
||||
- 本轮工作树追加差异的定向验证:`npm run verify:app-version`、`npm run typecheck`、`git diff --check` 通过;6 个前端 Vitest 文件 61 项通过;`cargo test --manifest-path "lime-rs/Cargo.toml" -p model-provider --lib current_client` 45 项通过。
|
||||
- 远端 Quality `29633271523` 的失败证据:Vitest layer budget 36/30(既有仓库基线)、GUI smoke 与 Rust Full 均因 CI sherpa-onnx 预编译库未准备而链接失败;新 SHA `46f445588` 已验证 GUI smoke 预热成功,Rust Full 仍缺该步骤,已追加同样的目标预热并待下一 SHA 复验。
|
||||
- Quality `29635195772` 已证明 sherpa 预热修复有效:GUI smoke 通过,Rust workspace 编译并通过 1183 项;剩余 Rust 失败来自 `host_boundary_guard` 检出单元测试 fixture 中的 `electron` 字面量,已改为中性 host fixture 并在本地复测 2 项守卫通过。
|
||||
- Release `29635584989` 已越过 Windows 构建、资源校验、staging 和 N-1 下载,真实 N-1 smoke 暴露安装器父进程退出后 `Update.exe` 仍持有 Squirrel 单实例锁;结构化错误为 `AutoUpdater process with arguments --checkForUpdate,... is already running`。RC helper 现按完整 executable path 等待安装器遗留 updater 退出后才启动 N-1 应用,55 项 Electron/Release 定向回归通过。
|
||||
- 真实 Windows packaged L8/N-1 验证:待路径修复后的远端 Release workflow 执行;本机为 macOS,无法代替 Windows 证据。
|
||||
|
||||
## 架构确认
|
||||
@@ -64,4 +65,4 @@
|
||||
- 责任人:release owner(v1.107.0)。
|
||||
- 日期:2026-07-18。
|
||||
|
||||
当前完成度:`97%`。下一刀:提交边界守卫 fixture 修复,待当前 Release 结束后覆盖 `v1.107.0` 到最终 SHA,再监控三平台构建和 Windows N-1 终态。
|
||||
当前完成度:`97%`。下一刀:提交 Squirrel updater quiescence 修复,覆盖 `v1.107.0` 到最终 SHA,再监控三平台构建和 Windows N-1 终态。
|
||||
|
||||
@@ -215,6 +215,48 @@ export async function stopInstalledApp(executable) {
|
||||
return { executable, exitCode: result.exitCode };
|
||||
}
|
||||
|
||||
export function buildWaitForWindowsProcessExitScript() {
|
||||
const matchingProcesses =
|
||||
"@(Get-CimInstance Win32_Process | Where-Object { $_.ExecutablePath -and [String]::Equals([System.IO.Path]::GetFullPath($_.ExecutablePath), $target, [StringComparison]::OrdinalIgnoreCase) })";
|
||||
return [
|
||||
"$ErrorActionPreference = 'Stop'",
|
||||
"$target = [System.IO.Path]::GetFullPath($env:LIME_TARGET_EXECUTABLE)",
|
||||
"$deadline = [DateTime]::UtcNow.AddMilliseconds([double]$env:LIME_PROCESS_WAIT_TIMEOUT_MS)",
|
||||
`while ([DateTime]::UtcNow -lt $deadline) { $processes = ${matchingProcesses}; if ($processes.Count -eq 0) { Write-Output "running=0"; exit 0 }; Start-Sleep -Milliseconds 250 }`,
|
||||
'Write-Error "timed out waiting for process exit: $target"',
|
||||
"exit 1",
|
||||
].join("; ");
|
||||
}
|
||||
|
||||
export async function waitForWindowsProcessExit(
|
||||
executable,
|
||||
{ runProcessImpl = runProcess, timeoutMs = 60_000 } = {},
|
||||
) {
|
||||
const result = await runProcessImpl(
|
||||
"powershell.exe",
|
||||
[
|
||||
"-NoProfile",
|
||||
"-NonInteractive",
|
||||
"-Command",
|
||||
buildWaitForWindowsProcessExitScript(),
|
||||
],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
LIME_PROCESS_WAIT_TIMEOUT_MS: String(timeoutMs),
|
||||
LIME_TARGET_EXECUTABLE: executable,
|
||||
},
|
||||
timeoutMs: timeoutMs + 5_000,
|
||||
},
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(
|
||||
`timed out waiting for process exit at ${executable}: exit ${result.exitCode}`,
|
||||
);
|
||||
}
|
||||
return { executable, exitCode: result.exitCode, timeoutMs };
|
||||
}
|
||||
|
||||
export async function exerciseNMinusOneUpdate({
|
||||
candidateFeedDir,
|
||||
candidateVersion,
|
||||
@@ -236,6 +278,9 @@ export async function exerciseNMinusOneUpdate({
|
||||
);
|
||||
}
|
||||
|
||||
const baselineUpdaterQuiescence = await waitForWindowsProcessExit(
|
||||
installed.updateExecutable,
|
||||
);
|
||||
const staticFeed = await startSquirrelFeed(feed);
|
||||
const cdpPort = await reserveLocalPort();
|
||||
const cdpUrl = `http://127.0.0.1:${cdpPort}`;
|
||||
@@ -371,6 +416,7 @@ export async function exerciseNMinusOneUpdate({
|
||||
);
|
||||
}
|
||||
return {
|
||||
baselineUpdaterQuiescence,
|
||||
candidateFeedServed,
|
||||
candidateInstalledByUpdater: existsSync(candidateInstalled.executable),
|
||||
candidateVersion,
|
||||
|
||||
@@ -18,6 +18,7 @@ import process from "node:process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import {
|
||||
buildNMinusOneLaunchEnv,
|
||||
buildWaitForWindowsProcessExitScript,
|
||||
compareVersions,
|
||||
exerciseNMinusOneUpdate,
|
||||
findReadyElectronUpdaterPage,
|
||||
@@ -27,10 +28,12 @@ import {
|
||||
resolveSquirrelFeed,
|
||||
selectNMinusOneVersion,
|
||||
stopInstalledApp,
|
||||
waitForWindowsProcessExit,
|
||||
} from "./lib/windows-squirrel-n-minus-one.mjs";
|
||||
|
||||
export {
|
||||
buildNMinusOneLaunchEnv,
|
||||
buildWaitForWindowsProcessExitScript,
|
||||
compareVersions,
|
||||
findReadyElectronUpdaterPage,
|
||||
isFinalElectronRendererUrl,
|
||||
@@ -38,6 +41,7 @@ export {
|
||||
resolveInstalledSquirrelPaths,
|
||||
resolveSquirrelFeed,
|
||||
selectNMinusOneVersion,
|
||||
waitForWindowsProcessExit,
|
||||
};
|
||||
|
||||
const SCENARIO_ID = "PLT-02-windows-squirrel-rc";
|
||||
|
||||
@@ -7,6 +7,7 @@ import YAML from "yaml";
|
||||
|
||||
import {
|
||||
buildNMinusOneLaunchEnv,
|
||||
buildWaitForWindowsProcessExitScript,
|
||||
buildWindowsRcSummary,
|
||||
compareVersions,
|
||||
findReadyElectronUpdaterPage,
|
||||
@@ -16,6 +17,7 @@ import {
|
||||
resolveSquirrelFeed,
|
||||
selectNMinusOneVersion,
|
||||
selectSquirrelInstaller,
|
||||
waitForWindowsProcessExit,
|
||||
} from "./windows-squirrel-rc-smoke.mjs";
|
||||
|
||||
describe("Windows Squirrel RC smoke", () => {
|
||||
@@ -76,6 +78,69 @@ describe("Windows Squirrel RC smoke", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("N-1 启动前应等待安装器遗留的 Squirrel Update.exe 退出", async () => {
|
||||
const runProcessImpl = vi.fn().mockResolvedValue({ exitCode: 0 });
|
||||
|
||||
await expect(
|
||||
waitForWindowsProcessExit("C:\\Users\\runner\\AppData\\Local\\lime\\Update.exe", {
|
||||
runProcessImpl,
|
||||
timeoutMs: 12_000,
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
executable: "C:\\Users\\runner\\AppData\\Local\\lime\\Update.exe",
|
||||
exitCode: 0,
|
||||
timeoutMs: 12_000,
|
||||
});
|
||||
|
||||
expect(runProcessImpl).toHaveBeenCalledWith(
|
||||
"powershell.exe",
|
||||
[
|
||||
"-NoProfile",
|
||||
"-NonInteractive",
|
||||
"-Command",
|
||||
buildWaitForWindowsProcessExitScript(),
|
||||
],
|
||||
expect.objectContaining({
|
||||
env: expect.objectContaining({
|
||||
LIME_PROCESS_WAIT_TIMEOUT_MS: "12000",
|
||||
LIME_TARGET_EXECUTABLE:
|
||||
"C:\\Users\\runner\\AppData\\Local\\lime\\Update.exe",
|
||||
}),
|
||||
timeoutMs: 17_000,
|
||||
}),
|
||||
);
|
||||
expect(buildWaitForWindowsProcessExitScript()).toContain(
|
||||
"Get-CimInstance Win32_Process",
|
||||
);
|
||||
expect(buildWaitForWindowsProcessExitScript()).toContain(
|
||||
"Start-Sleep -Milliseconds 250",
|
||||
);
|
||||
|
||||
const source = fs.readFileSync(
|
||||
"scripts/electron/lib/windows-squirrel-n-minus-one.mjs",
|
||||
"utf8",
|
||||
);
|
||||
expect(source.indexOf("await waitForWindowsProcessExit(")).toBeGreaterThan(
|
||||
-1,
|
||||
);
|
||||
expect(source.indexOf("await waitForWindowsProcessExit(")).toBeLessThan(
|
||||
source.indexOf("const child = spawn("),
|
||||
);
|
||||
});
|
||||
|
||||
it("Squirrel Update.exe 未在截止时间退出时应 fail closed", async () => {
|
||||
const runProcessImpl = vi.fn().mockResolvedValue({ exitCode: 1 });
|
||||
|
||||
await expect(
|
||||
waitForWindowsProcessExit("C:\\runner\\Update.exe", {
|
||||
runProcessImpl,
|
||||
timeoutMs: 1_000,
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
"timed out waiting for process exit at C:\\runner\\Update.exe: exit 1",
|
||||
);
|
||||
});
|
||||
|
||||
it("只选择当前候选版本的 Forge Squirrel installer", () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "squirrel-rc-assets-"));
|
||||
fs.mkdirSync(path.join(root, "make", "squirrel.windows", "x64"), {
|
||||
|
||||
@@ -251,6 +251,7 @@ describe("AppSidebar conversations", () => {
|
||||
agentEntry: "claw",
|
||||
projectId: "project-1",
|
||||
initialSessionId: "session-click",
|
||||
initialSessionName: "立即打开历史会话",
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
|
||||
@@ -206,6 +206,7 @@ describe("AppSidebar search", () => {
|
||||
agentEntry: "claw",
|
||||
projectId: "project-1",
|
||||
initialSessionId: "session-target",
|
||||
initialSessionName: "目标历史会话",
|
||||
}),
|
||||
);
|
||||
expect(mockRecordAgentUiPerformanceMetric).toHaveBeenCalledWith(
|
||||
|
||||
@@ -142,7 +142,7 @@ export function AgentChatWorkspace({
|
||||
onWorkflowProgressChange,
|
||||
initialUserPrompt,
|
||||
initialUserImages,
|
||||
initialSessionName: _initialSessionName,
|
||||
initialSessionName,
|
||||
entryBannerMessage,
|
||||
initialPendingServiceSkillLaunch,
|
||||
initialInputCapability,
|
||||
@@ -1420,6 +1420,7 @@ export function AgentChatWorkspace({
|
||||
harnessPanelVisible,
|
||||
homeMountedAt: workspaceRenderT0.current,
|
||||
initialDispatchKey,
|
||||
initialSessionName,
|
||||
initialPendingServiceSkillLaunchSignature,
|
||||
isAutoRestoringSession,
|
||||
isBootstrapDispatchPending,
|
||||
|
||||
@@ -163,6 +163,30 @@ describe("taskCenterTabProjection", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("初始 Thread 标题尚未进入 Topic 投影时应使用路由元数据兜底", () => {
|
||||
const topic = createTopic("thread-active", { title: "" });
|
||||
|
||||
const items = buildTaskCenterTabItems({
|
||||
draftTabs: [],
|
||||
activeDraftTabId: null,
|
||||
isDraftTabActive: false,
|
||||
sessionId: "thread-active",
|
||||
initialSessionId: "thread-active",
|
||||
initialSessionName: "修复恢复链路",
|
||||
previewTopicId: null,
|
||||
visibleTabIds: ["thread-active"],
|
||||
topicById: new Map([[topic.id, topic]]),
|
||||
untitledTaskLabel: "未命名任务",
|
||||
});
|
||||
|
||||
expect(items).toHaveLength(1);
|
||||
expect(items[0]).toMatchObject({
|
||||
id: "thread-active",
|
||||
title: "修复恢复链路",
|
||||
isActive: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("应在 claw 或 new-task 有标签项时渲染任务标签栏", () => {
|
||||
expect(
|
||||
shouldRenderTaskCenterTabStrip({
|
||||
|
||||
@@ -15,6 +15,8 @@ interface BuildTaskCenterTabItemsParams {
|
||||
activeDraftTabId: string | null;
|
||||
isDraftTabActive: boolean;
|
||||
sessionId?: string | null;
|
||||
initialSessionId?: string | null;
|
||||
initialSessionName?: string | null;
|
||||
previewTopicId: string | null;
|
||||
visibleTabIds: string[];
|
||||
topicById: ReadonlyMap<string, Topic>;
|
||||
@@ -63,6 +65,8 @@ export function buildTaskCenterTabItems({
|
||||
activeDraftTabId,
|
||||
isDraftTabActive,
|
||||
sessionId,
|
||||
initialSessionId,
|
||||
initialSessionName,
|
||||
previewTopicId,
|
||||
visibleTabIds,
|
||||
topicById,
|
||||
@@ -82,16 +86,24 @@ export function buildTaskCenterTabItems({
|
||||
const topicItems = visibleTabIds
|
||||
.map((topicId) => topicById.get(topicId))
|
||||
.filter((topic): topic is Topic => Boolean(topic))
|
||||
.map((topic) => ({
|
||||
id: topic.id,
|
||||
title: resolveTaskCenterTopicTitle(topic.title, untitledTaskLabel),
|
||||
status: topic.status ?? "done",
|
||||
updatedAt: resolveTopicUpdatedAt(topic),
|
||||
isActive: !isDraftTabActive && topic.id === (previewTopicId ?? sessionId),
|
||||
hasUnread: Boolean(topic.hasUnread),
|
||||
isPinned: Boolean(topic.isPinned),
|
||||
renamable: true,
|
||||
}));
|
||||
.map((topic) => {
|
||||
const initialTitleFallback =
|
||||
topic.id === initialSessionId ? initialSessionName?.trim() : null;
|
||||
return {
|
||||
id: topic.id,
|
||||
title: resolveTaskCenterTopicTitle(
|
||||
topic.title,
|
||||
initialTitleFallback || untitledTaskLabel,
|
||||
),
|
||||
status: topic.status ?? "done",
|
||||
updatedAt: resolveTopicUpdatedAt(topic),
|
||||
isActive:
|
||||
!isDraftTabActive && topic.id === (previewTopicId ?? sessionId),
|
||||
hasUnread: Boolean(topic.hasUnread),
|
||||
isPinned: Boolean(topic.isPinned),
|
||||
renamable: true,
|
||||
};
|
||||
});
|
||||
|
||||
const orderedItems = [...topicItems, ...draftItems];
|
||||
if (orderedItems.length <= maxCount) {
|
||||
|
||||
@@ -48,6 +48,7 @@ interface UseTaskCenterChromeNavigationRuntimeParams {
|
||||
hasPendingA2UIForm: boolean;
|
||||
homeMountedAt: number;
|
||||
initialDispatchKey: string | null;
|
||||
initialSessionName?: string | null;
|
||||
initialPendingServiceSkillLaunchSignature?: string | null;
|
||||
isAutoRestoringSession: boolean;
|
||||
isBootstrapDispatchPending: boolean;
|
||||
@@ -139,6 +140,7 @@ export function useTaskCenterChromeNavigationRuntime({
|
||||
harnessPanelVisible,
|
||||
homeMountedAt,
|
||||
initialDispatchKey,
|
||||
initialSessionName,
|
||||
initialPendingServiceSkillLaunchSignature,
|
||||
isAutoRestoringSession,
|
||||
isBootstrapDispatchPending,
|
||||
@@ -255,6 +257,7 @@ export function useTaskCenterChromeNavigationRuntime({
|
||||
agentEntry,
|
||||
sessionId,
|
||||
normalizedInitialSessionId,
|
||||
initialSessionName,
|
||||
detachedTopicId,
|
||||
openTabIds,
|
||||
topics,
|
||||
|
||||
@@ -20,6 +20,7 @@ interface UseTaskCenterTabChromeParams {
|
||||
agentEntry: AgentEntry;
|
||||
sessionId?: string | null;
|
||||
normalizedInitialSessionId?: string | null;
|
||||
initialSessionName?: string | null;
|
||||
detachedTopicId?: string | null;
|
||||
openTabIds: string[];
|
||||
topics: Topic[];
|
||||
@@ -47,6 +48,7 @@ export function useTaskCenterTabChrome({
|
||||
agentEntry,
|
||||
sessionId,
|
||||
normalizedInitialSessionId,
|
||||
initialSessionName,
|
||||
detachedTopicId,
|
||||
openTabIds,
|
||||
topics,
|
||||
@@ -97,6 +99,8 @@ export function useTaskCenterTabChrome({
|
||||
activeDraftTabId,
|
||||
isDraftTabActive,
|
||||
sessionId,
|
||||
initialSessionId: normalizedInitialSessionId,
|
||||
initialSessionName,
|
||||
previewTopicId,
|
||||
visibleTabIds,
|
||||
topicById,
|
||||
@@ -106,6 +110,8 @@ export function useTaskCenterTabChrome({
|
||||
activeDraftTabId,
|
||||
draftTabs,
|
||||
isDraftTabActive,
|
||||
initialSessionName,
|
||||
normalizedInitialSessionId,
|
||||
previewTopicId,
|
||||
sessionId,
|
||||
topicById,
|
||||
|
||||
@@ -236,6 +236,7 @@ export function useAppSidebarConversationActions({
|
||||
const targetParams = buildClawAgentParams({
|
||||
...(sessionProjectId ? { projectId: sessionProjectId } : {}),
|
||||
initialSessionId: session.id,
|
||||
initialSessionName: resolveLocalizedSessionTitle(session),
|
||||
});
|
||||
navigateIfNeeded({
|
||||
target: {
|
||||
@@ -252,6 +253,7 @@ export function useAppSidebarConversationActions({
|
||||
deferConversationNavigation,
|
||||
onNavigate,
|
||||
requestedNavigationTargetRef,
|
||||
resolveLocalizedSessionTitle,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -48,6 +48,24 @@ describe("getAppShellLayoutState", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("active Thread 路由应进入专注工作区并隐藏全局 sidebar", () => {
|
||||
expect(
|
||||
getAppShellLayoutState({
|
||||
currentPage: "agent",
|
||||
pageParams: {
|
||||
agentEntry: "claw",
|
||||
initialSessionId: "thread-active",
|
||||
initialSessionName: "修复恢复链路",
|
||||
},
|
||||
agentHasMessages: false,
|
||||
}),
|
||||
).toEqual({
|
||||
shouldHideSidebarForAgent: true,
|
||||
shouldShowAppSidebar: false,
|
||||
shouldAddMainContentGap: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("settings 页应隐藏 sidebar", () => {
|
||||
expect(
|
||||
getAppShellLayoutState({
|
||||
|
||||
@@ -12,10 +12,14 @@ export function getAppShellLayoutState(params: {
|
||||
agentHasMessages: boolean;
|
||||
}): AppShellLayoutState {
|
||||
const currentAgentParams = params.pageParams as AgentPageParams;
|
||||
const hasActiveThreadRoute = Boolean(
|
||||
currentAgentParams.initialSessionId?.trim(),
|
||||
);
|
||||
const shouldHideSidebarForAgent =
|
||||
params.currentPage === "agent" &&
|
||||
(Boolean(currentAgentParams.fromResources) ||
|
||||
Boolean(currentAgentParams.immersiveHome) ||
|
||||
hasActiveThreadRoute ||
|
||||
(params.agentHasMessages && Boolean(currentAgentParams.lockTheme)));
|
||||
|
||||
const shouldShowAppSidebar =
|
||||
|
||||
Reference in New Issue
Block a user