f7a720204a
- 移除 GovAI, nomifun-tauri, 算力盒子 的 submodule 引用 - 添加所有子项目的完整源代码 - 保留原始 .git 为 .git.bak 备份
10 KiB
10 KiB
终端三项改进 设计文档(自动标题 + 回退 Shell + 退出清理)
状态:已与用户确认,进入实现。语言随上下文用中文;代码标识符用英文。
目标
- 自动标题:新建终端会话的侧边栏标题太固定,改为在用户首次交互后,自动总结为与工作内容相关的标题。
- 回退 Shell:claude/codex 终端会话「失去激活态」后出现乱码卡死、无法输入、进程生死不明的情况,改为可靠回退到一个干净的 shell(而非卡死页面),并允许用户持续 Ctrl+C 脱身。
- 退出清理:整个 App 退出时清理所有终端会话(删除全部会话行 + 滚屏),不残留到下次启动。
现状架构(与改动相关)
- 每个会话 =
portable-pty子进程(Windows ConPTY)。TerminalService.live: DashMap<i64, Arc<PtyHandle>>管理活进程,next_epoch区分代际,on_exit回调按 epoch 守卫。子进程退出后last_status='exited',PTY 被销毁,无自动重启。 relaunch(id)(service.rs:837)= kill 旧 PTY → 同一 id、新 epoch 用原命令重启 →clear_scrollback→update_status('running')→emit_updated。- 标题就是
terminal_sessions.name。default_name(command, backend)(service.rs:989)机械决定:backend 标签(Claude/Codex)>Shell($SHELL哨兵)> 原始命令。重命名链路update_meta → repo.update_meta → emit terminal.updated → 前端 onUpdated → 侧边栏重渲染已全链路工作。 - 终端会话无 conversation/provider/model 关联(DB 行、
CreateTerminalParams、API DTO、IPC、UI 均无相关列)。 - 全部输入经
TerminalService::input(id, data_b64)(service.rs:743)汇聚后写入 PTY;pty.rs::write不留记录。input()在无活 handle 时返回NotFound。 - 生命周期事件:
LifecycleKind{TurnEnd, ToolUse, Notification, SessionStart}(lifecycle.rs)。claude 用 Stop→TurnEnd(payload 含last_assistant_message),codex 同有 TurnEnd。subscribe_lifecycle(id)已存在;spawn_pty 内已有一个示例消费者(service.rs:515-529)。 - 既有「prompt→短文本」LLM 路径:
nomifun_ai_agent::one_shot_completion(cfg, system, messages, max_tokens)+resolve_provider_config(provider_repo, encryption_key, provider_id, model, workspace)+user_message(text)。LiveKnowledgeCompleter(knowledge_completer.rs)是「持有 provider_repo + encryption_key + workspace、resolve_default_model()取第一个有效 provider/model」的拷贝模板。provider_repo/encryption_key在AppServices(services.rs:173/176),terminal_service在 services.rs:440-459 接线,但二者未传给TerminalService。 - 前端:
XtermView.tsx的 xterm 网格从不按 status 禁用(onData始终转发输入);仅TerminalSendBox在isExited(last_status!=='running')时 disable(TerminalSessionPage.tsx:408)。term.clear()只清普通缓冲、不能退出 alt-screen;需term.reset()。WebGL 上下文丢失(XtermView.tsx:89-95)是「失去激活态后乱码」的诱因之一。 - WS 重连间隙:
httpBridge.ts的 WS 单例会重连(退避 1s~30s),监听器按事件名存于模块级 map 故重连后存活;但服务端只做无 replay 的broadcast_all,重连 open 回调不重新拉取 scrollback → 断线期间的重绘帧永久丢失 = 持久乱码。 - App 生命周期钩子仅在
apps/desktop/src/main.rs:on_window_event(CloseRequested 隐藏到托盘;Destroyed→exit(0))、tray-quit(设QuitFlag后app.exit(0))、handle_run_event(仅 macOS Reopen)。无任何终端清理;TerminalService/PtyHandle无 Drop。后端运行在独立 tokio runtime 线程,DesktopServer暴露 runtime Handle(desktop.rs:106)但无阻塞 shutdown 方法。
功能① 自动标题
与原始需求的偏差(已确认):终端无 conversation 关联,无法「取对应对话的 provider/model」。改为:用 App 默认有效 provider/model(首个有效 provider 的首个有效 model,复用 knowledge 自动生成模式)做 LLM 总结;模型未配置或调用失败 → 兜底取用户输入内容前 N 字。
触发与范围
- 仅一次,首次交互时填充;含 shell。
- claude/codex:订阅生命周期,首个
TurnEnd事件读payload.last_assistant_message,结合已捕获的首行用户输入,调one_shot_completion总结为短标题(≤ ~30 字)。 - shell / 非 agent / 无模型 / LLM 失败:取首行用户输入前 N 字(默认 N=40,去除控制字符)作为标题。
- 捕获首行输入:在
TerminalService::input()内累积该会话首个输入直到首个\r/\n,存内存。
一次性 + 不覆盖手动改名
- 写入前判定
当前 name == default_name(command, backend);不等说明用户已改名或已自动命名过 → 跳过(自身幂等)。 - 另加内存
titled: DashMap<i64, ()>防止首批按键竞态重复触发。 - 无需 migration。
落库
- 统一走现有
update_meta(id, Some(title), None)(trim/校验/持久化/emitterminal.updated),前端零改动。
后端接线
- 新增
LiveTerminalTitleCompleter { provider_repo, encryption_key, workspace }(仿LiveKnowledgeCompleter),late-wire 注入TerminalService(新增with_title_completer)。None时只走兜底截断,绝不阻塞。 crates/backend/nomifun-terminal/Cargo.toml加nomifun-ai-agent依赖。services.rs:440-459旁注入,复用provider_repo.clone()/encryption_key/data_dir.clone()。
主要文件
crates/backend/nomifun-terminal/src/{service.rs, title.rs(新), Cargo.toml}crates/backend/nomifun-app/src/services.rs
功能② 回退 Shell(修复乱码卡死 + 持续 Ctrl+C 脱身)
三个独立缺陷,一套协同修复:
- 后端
relaunch_as_shell(id):克隆relaunch()逻辑,但用SHELL_SENTINEL+[]替代row.command/row.args来 spawn。先 tree-kill 卡住的 agent → 同一 id、新 epoch 重启干净登录 shell → 持久化command=$SHELL, args=[], backend=None(这样退出/重启后续仍是 shell,且default_name变Shell)→update_status('running')→emit_updated(前端onUpdated自动恢复送信框)。- 新路由
POST /api/terminals/{id}/relaunch-shell(独立于现有 relaunch,语义清晰)。 - IPC:
ipcBridge.terminal.relaunchShell(id)。
- 新路由
- 前端
term.reset():给XtermViewHandle增reset:()=>term.reset()(退出 alt-screen、复位模式、清屏)。在「回退 Shell」与重连重放时调用。 - 常驻「回退 Shell」入口(不受 isExited 限制):会话头部加按钮;点击 =
relaunchShell+term.reset()。即便会话仍running卡死也能脱身。 - 持续 Ctrl+C 升级:
XtermView检测短时间内连续 N 次 Ctrl+C(\x03,默认 3 次 / 1.5s)→ 显示提示条「再次 Ctrl+C 回退到 Shell」并在阈值后触发relaunchShell。Ctrl+C 仍原样转发(不拦截单次)。 - WS 重连重放(乱码主因修复):
httpBridge.tsWS open 时若为重连,通知监听者(新增terminal.__reconnected内部事件或重连回调);XtermView收到后term.reset()然后重新ipcBridge.terminal.get(id)用同一 decoder 重放当前 scrollback。
主要文件
crates/backend/nomifun-terminal/src/{service.rs, routes.rs}ui/src/renderer/pages/terminal/{XtermView.tsx, TerminalSessionPage.tsx, TerminalSendBox.tsx}ui/src/common/adapter/{ipcBridge.ts, httpBridge.ts}- i18n:
ui/.../locales/{zh-CN,en-US}/terminal.json
功能③ 退出清理(删除全部会话行 + 滚屏)
- repo:
ITerminalRepository加delete_all(&self) -> Result<u64>;SQLite 实现DELETE FROM terminal_sessions(terminal_scrollback经 FK CASCADE 自动删)。MemRepo 同步实现(供测试)。 - service:
TerminalService::shutdown_cleanup():遍历self.live逐个kill(),清pending_spawn,再repo.delete_all()。 - desktop:
DesktopServer::shutdown_terminals():把shutdown_cleanup()编排到后端 runtime 上阻塞执行(带超时上限,如 3s),供 Tauri 主线程在app.exit(0)前同步调用。 - main.rs:仅在 real-quit 路径调用:
- tray-quit handler(
QuitFlag已置位,main.rs:697-702)app.exit(0)之前; - 新增
RunEvent::ExitRequested/Exit分支(handle_run_event,仅当QuitFlag置位时)。 - 绝不在 close-to-tray(隐藏窗口)路径调用。
- tray-quit handler(
主要文件
crates/backend/nomifun-terminal/src/service.rscrates/backend/nomifun-db/src/repository/{terminal.rs, sqlite_terminal.rs}crates/backend/nomifun-app/src/desktop.rsapps/desktop/src/main.rs
测试策略
- ①
default_name一致性守卫的幂等(改名后不再触发);无 completer 注入时的截断兜底;注入 fake completer 时 TurnEnd→update_meta 链路;首行输入捕获。 - ②
relaunch_as_shell单测:同一 id、command变为$SHELL、status 回running、发terminal.updated。前端 reset / 重连重放 / 连续 Ctrl+C 升级以手动 + 单元(纯函数)验证。 - ③
delete_all删行 + scrollback(CASCADE);shutdown_cleanupkill 活进程后行清空;close-to-tray 不触发(QuitFlag 守卫,逻辑断言)。
风险与缓解
- 退出清理的破坏性:用户已确认要删全部;严格只在 QuitFlag 真退出路径执行,close-to-tray 绝不删。阻塞清理带 3s 超时上限,避免退出卡住。
- 回退 shell 改变语义:原 agent 退出→可重启项;改 shell 后命令被改写为
$SHELL。这是用户明确要的「回退到 shell」,且仅在用户主动点按/连续 Ctrl+C 触发,不做静默自动改写。 - 标题 LLM 成本/噪声:仅首次、仅一次、
name==default守卫;agent 才调 LLM,shell 走零成本截断。 - Windows 进程组:
kill()在 Windows 无进程组 SIGKILL,孙进程可能残留——沿用既有kill()能力,不在本次扩大范围。