Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 27 additions & 11 deletions .monkeycode/specs/desktop-background-agent-presence/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,23 @@ sidecar 增加 `background: [{agentId, tcId, summary, startedAt}]`:

派发闭卡帧(driver 生成"已转后台"回执处)附带结构化标记(沿用 `progress {kind:"background_agent"}` 词汇或闭卡 update 上的专用字段),reducer 优先按结构化字段进入"保持运行态"分支;对引擎中文回执的字符串嗅探保留为旧 journal 回放 fallback,注释标注其唯一用途。

### 2.6 停止(P3)

- `session_call` 新增 kind `background_stop {agentId}`:映射 shell sid → engine sid 后调 `session/publishEvent`,`event_id = "bgstop:" + agentId + ":" + uuid`(幂等),`type = "host_request"`,`source = "desktop"`,message 固定模板:点名 `TaskStop(task_id=<agentId>)`、要求仅执行停止并简短确认。`busy`/`not_found` 原样返回给 UI。
- `normalize.rs` 通知轮开轮分支放宽:`source == "notification" || source == "desktop"` 共用同一幂等开轮/收轮逻辑;desktop 事件轮的正文以轻量系统行呈现。
- TaskStop 观察:driver 按事件携带的 `event_id` 关联该轮 `tool_call`,登记 `bgstop` 待决项;见到 `TaskStop` 即确认,超时(30s)未见或轮次结束仍未见 → 经 session-event 通知 UI 回弹。停止成功的终态不需要专门处理——引擎 `manager.Complete` 照常入 `NotificationQueue`,现有 stopped 结果卡与派发卡回填链路原样工作。
### 2.6 停止(P3,已按直通 RPC 落地)

引擎 733df47 提供 `subagentControl` 能力(`subagent/list` / `subagent/cancel`),
§4 预留的「直通切换位」直接生效;原 publishEvent 模型中介方案从未实现,整体作废
(连同 desktop 事件轮、TaskStop 观察与 30s 回弹——直通应答本身就是受理确认):

- `session_call` 新增 kind `background_stop {agent_id}`:映射 shell sid → engine_id
后直调 `subagent/cancel`,应答 `{status:"ok", state, stopped}` 原样透传。
`stopped=false, state="stopping"` = 引擎已发出取消但 5s(shutdownGrace)内未收尾,
子代理稍后退出时通知照常到达;引擎错误(agent/顶层会话不存在)原样上抛给 UI 外显。
- 命令层守卫 `subagentControl` cap(契约 2 的唯一强制点);`driver/mod.rs::Caps`
投影新增 `subagent_control` 供 UI 门控。
- 终态不走应答:引擎 `manager.Complete` 照常入 `NotificationQueue`,现有
`task_notification` 链路(stopped 结果卡 + 派发卡回填 + 第三态收敛)原样工作,
不新增终态路径;通知丢失(引擎更替)由既有孤儿对账兜底。stopped 通知的派发卡
收卡语义细化为「已停止」(`chat.tool.bgStopped`),不再与「执行失败」混词。
- `subagent/list` 暂未消费:pending 派生与对账已由壳侧登记覆盖,留作日后对账校验位。

## 3. UI(ui-next)

Expand All @@ -59,7 +71,11 @@ sidecar 增加 `background: [{agentId, tcId, summary, startedAt}]`:
- 渲染条件:`!presentation.running && presentation.backgroundRunning > 0 && conn.connected`,占 RunBar 的插槽位;
- 内容:脉冲状态点 + `chat.bg.running`(count 插值)。单任务摘要与耗时内联(`rawInput` 的 summary/description 优先,退回工具标题);多任务收起为计数,可展开逐任务一行(摘要 + 耗时 + 各自「查看子会话」,对齐工具卡组「×N 展开」形态)。取材为 items 里全部 `run+background` 卡,ChatView 供给并经签名 memo 稳定引用;
- 交互:任务行点击打开对应子会话回放浮层(`childSessionId`);无主循环停止按钮;
- P3:任务行 hover 出"停止"→ 二段确认(按钮原位变"确认停止?")→ 调 `background_stop` → 行进入"停止中…";busy/超时回弹并 toast。
- P3(已落地):任务行常驻「停止」(与「查看子会话」同形态;实现弃用了 hover 出钮——
同排的查看入口是常驻的,忽隐忽现反而更抓眼)→ 二段确认(按钮原位变「确认停止?」,
4s 未点自动回弹)→ 调 `background_stop` → 行进入「停止中…」直至通知收卡;失败回弹、
原因在条内外显(`chat.bg.stopFailed`),可重试。停止入口按 `backgroundAgentId` 寻址,
无 agentId 的旧 journal 卡与 `engine_caps.subagent_control` 为假时不出入口。

### 3.3 轮末分隔线变体

Expand All @@ -76,16 +92,16 @@ sidecar 增加 `background: [{agentId, tcId, summary, startedAt}]`:

### 3.6 i18n

新增键:`chat.bg.running`、`chat.sys.turnEndBg`、`chat.tool.bgInterrupted`、停止入口与确认文案、系统通知模板。zh/en 同步。
新增键:`chat.bg.running`、`chat.sys.turnEndBg`、`chat.tool.bgInterrupted`、停止入口与确认文案(`chat.bg.stop/stopConfirm/stopping/stopFailed`、`chat.tool.bgStopped`)、系统通知模板。zh/en 同步。

## 4. 兼容与升级策略

- 旧 journal:无结构化标记的历史帧靠保留的嗅探 fallback 正常回放;无 sidecar `background` 字段的旧会话对账视为空集。
- 旧引擎:`publishEvent` 已在 caps 声明,缺失时停止入口整体隐藏(探测 `engine_caps`)。
- 升级位:引擎将来提供 `task/stop`/轮末任务快照 caps 时,`background_stop` 切直通 RPC,publishEvent 降为 fallback;派生逻辑收敛在 driver 一处,UI 无感
- 旧引擎:`subagentControl` 缺失时停止入口整体隐藏(探测 `engine_caps.subagent_control`,命令层同守)。
- 升级位(已兑现):引擎 733df47 落地 `subagentControl` 直通 RPC,`background_stop` 直调 `subagent/cancel`;publishEvent fallback 不再保留(从未实现,直接按直通落地),见 §2.6

## 5. 测试

- Rust(`ohmy_tests.rs`):pending 派生(两条派发路径、多任务并存);turn/stopped complete/interrupted × pending 有无的状态词;通知清理回写;sidecar 持久化写/删;引擎更替对账(合成终态帧、状态归一、正常重开不触发);desktop 事件轮幂等开轮/收轮;TaskStop 观察确认与超时回弹
- UI(Vitest):countItem 拆分与 presentation 全等;BackgroundBar 渲染条件、断连门控、点击定位;turn-end 变体 key;结构化标记优先 + 嗅探 fallback;停止二段确认、停止中、回弹(P3);通知触发条件(P2,mock plugin)。
- Rust(`ohmy_tests.rs`):pending 派生(两条派发路径、多任务并存);turn/stopped complete/interrupted × pending 有无的状态词;通知清理回写;sidecar 持久化写/删;引擎更替对账(合成终态帧、状态归一、正常重开不触发);background_stop cap 守卫/agent_id 校验/RPC 形状与应答透传/引擎错误透传
- UI(Vitest):countItem 拆分与 presentation 全等;BackgroundBar 渲染条件、断连门控、点击定位;turn-end 变体 key;结构化标记优先 + 嗅探 fallback;停止二段确认、停止中、能力门控、失败回弹(P3);stopped 通知按「已停止」收卡;通知触发条件(P2,mock plugin)。
- 门禁:相关 Cargo tests、Vitest、TypeScript typecheck、`git diff --check`。
26 changes: 14 additions & 12 deletions .monkeycode/specs/desktop-background-agent-presence/tasklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,20 @@
- [ ] 6.2 失焦/非当前会话的结构化通知 → 系统通知,点击聚焦并定位结果卡
- [ ] 6.3 权限缺失静默降级;触发条件测试(mock plugin)

## P3 停止(best-effort)

- [ ] 7. Driver 停止通道
- [ ] 7.1 `session_call` kind `background_stop`:publishEvent 组装(幂等 event_id、点名 TaskStop、busy/not_found 透传)
- [ ] 7.2 `normalize.rs` host 事件轮(source=desktop)接入开轮/收轮,轻量系统行呈现
- [ ] 7.3 按 event_id 观察 TaskStop tool_call:确认/超时回弹(session-event 通知 UI)
- [ ] 7.4 Rust 测试:幂等开轮、观察确认、超时回弹、busy 透传
- [ ] 8. UI 停止入口
- [ ] 8.1 BackgroundBar 任务行与派发卡详情弹窗的"停止"入口,二段确认(提示不可恢复)
- [ ] 8.2 停止中态、busy/超时回弹与 toast
- [ ] 8.3 `engine_caps` 探测:无 publishEvent 时隐藏入口;预留 task/stop 直通切换位
- [ ] 8.4 UI 测试:二段确认、停止中、回弹
## P3 停止(已按 subagentControl 直通 RPC 落地;publishEvent 中介方案作废,见 design §2.6)

- [x] 7. Driver 停止通道
- [x] 7.1 `session_call` kind `background_stop {agent_id}`:直调 `subagent/cancel`(cap 守卫、engine_id 寻址、应答/错误原样透传)
- [x] 7.2 `Caps` 投影新增 `subagent_control`(engine_caps → UI 门控)
- [x] 7.3 Rust 测试:cap 缺失拒绝、agent_id 校验、RPC 形状与应答透传、引擎错误透传
- ~~publishEvent 组装 / desktop 事件轮 / TaskStop 观察与超时回弹~~(直通 RPC 后不需要:应答即受理确认,终态归 task_notification 既有链路)
- [x] 8. UI 停止入口
- [x] 8.1 BackgroundBar 任务行常驻「停止」,二段确认(原位变「确认停止?」,4s 自动回弹)
- [x] 8.2 停止中态(等 task_notification 收卡)、失败回弹 + 条内原因外显(chat.bg.stopFailed)
- [x] 8.3 `engine_caps.subagent_control` 门控:无能力/无 agentId(旧 journal 卡)隐藏入口
- [x] 8.4 stopped 终态状态词:通知按「已停止」收卡(chat.tool.bgStopped),不再混用「执行失败」
- [x] 8.5 UI 测试:二段确认、停止中、能力门控、失败回弹;reduce stopped 收卡
- [ ] 8.6 派发卡详情弹窗的停止入口(暂缓:状态条为唯一入口)

## 验证

Expand Down
2 changes: 1 addition & 1 deletion agent
Submodule agent updated from 733df4 to 686dcb
13 changes: 9 additions & 4 deletions desktop/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
| 角色 | 位置 |
|---|---|
| 产帧(壳,引擎事件归一化) | src/driver/frame.rs(唯一入口,禁手拼 JSON) |
| 消费(UI) | ui-next/src/lib/protocol/{types,reduce}.ts(Frame/SessionStatus/PermOutcome/EngineStatus 由 ts-rs 自 frame.rs 生成到 ui/src/gen/ 并同步一份到 ui-next/src/gen/(genSync.test.ts 钉字节一致),types.ts 复用) |
| 消费(UI) | ui-next/src/lib/protocol/{types,reduce}.ts(Frame/SessionStatus/PermOutcome/EngineStatus 由 ts-rs 自 frame.rs 直接生成到 ui-next/src/gen/,types.ts 复用;旧工程 ui/ 已删,genSync 并行期契约随之退役) |

帧结构 `{type, kind?, data?(内联 JSON), timestamp(ms), seq}`——data 的
base64 双重编码已去除,只剩存量 journal 与云端帧两条兼容边界;
Expand All @@ -64,8 +64,9 @@ usage_update/compact_status/task_notification(后台子代理完成通知)/
model_update/think_update(会话思考档变更)/permission_mode_update`。
`llm_call_retry` 仅云端流产出,壳不产,UI 归约兼容。

改词汇的顺序:frame.rs 是唯一权威(ts-rs 重新生成 ui/src/gen/,ui-next/src/gen/ 随之同步),与
types.ts/reduce.ts 同一 PR 内同步,reduce.test.ts 补对应归约断言。
改词汇的顺序:frame.rs 是唯一权威(cargo test 经 ts-rs 重新生成
ui-next/src/gen/),与 types.ts/reduce.ts 同一 PR 内同步,reduce.test.ts
补对应归约断言。

**折叠是等价变换**(driver/fold.rs):journal 是「一 token 一帧」,回放前把
相邻同类流式碎片合并回一帧(usage/plan 每轮只留最后一条,其余原样)。
Expand Down Expand Up @@ -201,7 +202,7 @@ Windows 不得用不能覆盖既有目标的裸 `std::fs::rename`。

## 契约 5:会话状态机

状态词汇(Rust `frame::SessionStatus`,ts-rs 生成 ui/src/gen/SessionStatus.ts,ui-next/src/gen/ 同步一份):
状态词汇(Rust `frame::SessionStatus`,ts-rs 生成 ui-next/src/gen/SessionStatus.ts):
`created → running → idle | finished | interrupted | error`
- `created` = 新建未运行,**不是完成**(否则侧栏/桌宠按完成渲染)。
- `idle` = 当前轮正常结束、会话空闲可继续;`finished` 留给真正结束的
Expand Down Expand Up @@ -386,6 +387,10 @@ interactive:true,引擎给持久 Task 工具族)、会话思考档 session/setTh
直调自动提升)、子代理事件带 parent_session_id/parent_tool_call_id
(dab1b85,壳精确认领)。固定引擎已删除“同步 Agent 超时后自动转后台”,
后台执行必须由 `run_in_background:true` 显式声明。
子代理控制面(733df47,cap `subagentControl`:`subagent/list` +
`subagent/cancel`):壳经 `session_call background_stop` 直通 cancel
(UI 后台状态条的停止入口,Caps.subagent_control 门控);list 暂未消费,
pending 派生/对账仍以壳侧登记为权威。

## 开发与构建产物

Expand Down
12 changes: 6 additions & 6 deletions desktop/src/driver/frame.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Frame 词汇的唯一定义(产帧权威;词汇源自旧 Go 内核的 frame.go,
// git 历史可查)。
//
// 对表:本模块产帧、UI 消费(desktop/ui/src/{types.ts,reduce.ts})——
// 对表:本模块产帧、UI 消费(ui-next/src/lib/protocol/{types,reduce}.ts)——
// 任何新帧类型/字段先改这里与 types.ts,driver 禁止手拼 Frame JSON。
//
// 帧结构:{ type, kind?, data?(内联 JSON 对象), timestamp(ms), seq }
Expand All @@ -17,20 +17,20 @@
// Rust→TS 类型生成:Frame/SessionStatus/PermOutcome 带 ts_rs 导出
// (derive 经 cfg_attr(test) 门控,不进产物二进制)。再生成命令:
// cargo test export_bindings
// 产出 desktop/ui/src/gen/(生成物入库,勿手改);ui/src/types.ts 从
// 产出 desktop/ui-next/src/gen/(生成物入库,勿手改);protocol/types.ts 从
// gen/ 复用这些类型,手写与注释对表自此只剩"跑一次生成"。

use base64::Engine as _;
use serde_json::{json, Value};

/// 会话状态词汇(SessionMeta.status;UI/桌宠按此渲染,勿用裸字符串)。
/// ts-rs 导出 → ui/src/gen/SessionStatus.ts(types.ts 复用);
/// ts-rs 导出 → ui-next/src/gen/SessionStatus.ts(types.ts 复用);
/// rename_all 小写与 as_str 一致(两处同改才算改)。
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[cfg_attr(test, derive(ts_rs::TS))]
#[cfg_attr(
test,
ts(export, export_to = "../ui/src/gen/", rename_all = "lowercase")
ts(export, export_to = "../ui-next/src/gen/", rename_all = "lowercase")
)]
pub enum SessionStatus {
Created,
Expand Down Expand Up @@ -65,7 +65,7 @@ impl SessionStatus {
#[cfg_attr(test, derive(ts_rs::TS))]
#[cfg_attr(
test,
ts(export, export_to = "../ui/src/gen/", rename_all = "lowercase")
ts(export, export_to = "../ui-next/src/gen/", rename_all = "lowercase")
)]
pub enum PermOutcome {
Approved,
Expand Down Expand Up @@ -103,7 +103,7 @@ pub fn b64_text(s: &str) -> String {
/// timestamp 以容存量 journal 与云端帧,见 ui/src/types.ts::Frame)。
#[derive(serde::Serialize)]
#[cfg_attr(test, derive(ts_rs::TS))]
#[cfg_attr(test, ts(export, export_to = "../ui/src/gen/"))]
#[cfg_attr(test, ts(export, export_to = "../ui-next/src/gen/"))]
pub struct Frame {
pub r#type: String,
#[serde(skip_serializing_if = "Option::is_none")]
Expand Down
8 changes: 6 additions & 2 deletions desktop/src/driver/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ pub const ENGINE_STABLE_UPTIME: Duration = Duration::from_secs(60);
#[derive(Clone, Debug, PartialEq, serde::Serialize)]
#[serde(tag = "phase", rename_all = "lowercase")]
#[cfg_attr(test, derive(ts_rs::TS))]
#[cfg_attr(test, ts(export, export_to = "../ui/src/gen/"))]
#[cfg_attr(test, ts(export, export_to = "../ui-next/src/gen/"))]
pub enum EngineStatus {
/// 未启动:冷启动前、或应用退出/重启的中间态。
Stopped,
Expand Down Expand Up @@ -286,7 +286,7 @@ impl DriverHost {
}
}

/// 引擎能力表(对表 desktop/ui/src/types.ts 的 EngineCaps)。
/// 引擎能力表(对表 ui-next/src/lib/ipc/approvals.ts 的 EngineCaps)。
/// 能力仍是渐进的(随上游补齐翻位),由 ready 握手与桌面壳实际能力
/// 共同投影;UI 降级与命令层守卫都读取同一份运行时结果。
#[derive(Clone, Copy, serde::Serialize)]
Expand All @@ -299,6 +299,8 @@ pub struct Caps {
/// 运行中向当前轮追加用户指令。
pub steering: bool,
pub attachments: bool,
/// 定向停止后台子代理(subagent/cancel;UI 后台状态条的停止入口)。
pub subagent_control: bool,
}

pub fn caps(engine: &OhmyDriver, browser_ext: bool) -> Caps {
Expand All @@ -315,6 +317,7 @@ pub fn caps(engine: &OhmyDriver, browser_ext: bool) -> Caps {
steering: engine.has_capability("session/steer"),
// 上传/路径注入由壳实现,不是引擎握手项。
attachments: true,
subagent_control: engine.has_capability("subagentControl"),
}
}

Expand Down Expand Up @@ -527,6 +530,7 @@ pub async fn session_call(
let ctx = RepoCtx {
workdir,
wsl_distro: engine.wsl_distro(),
wsl_mount_root: engine.wsl_mount_root(),
};
// git/fs 是阻塞操作,丢 blocking 池;15s 超时防文件面板永久转圈
let task = tauri::async_runtime::spawn_blocking(move || {
Expand Down
15 changes: 15 additions & 0 deletions desktop/src/driver/ohmy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,21 @@ impl OhmyDriver {
self.0.wsl.as_ref().map(|w| w.distro.clone())
}

/// WSL automount 根;repo 等宿主文件操作用它把 `/mnt/c/...` 精确反解
/// 回 Windows 盘符路径。与 distro 一样随引擎实例更新。
pub fn wsl_mount_root(&self) -> Option<String> {
self.0.wsl.as_ref().map(|w| w.mount_root.clone())
}

/// 会话 guest 路径的宿主文件系统视角。本机模式原样返回;WSL 的盘符
/// automount 反解为 `C:\...`,真正的 Linux 路径才走 `\\wsl$`。
pub fn host_fs_view(&self, path: &str) -> PathBuf {
match &self.0.wsl {
Some(w) => crate::wsl::host_fs_view_with_mount_root(&w.distro, &w.mount_root, path),
None => PathBuf::from(path),
}
}

/// WSL guest 网络模式("mirrored"/"nat";本机模式 None)。
/// 浏览器 MCP 等回连宿主 127.0.0.1 的能力按此降级。
pub fn wsl_networking(&self) -> Option<String> {
Expand Down
Loading
Loading