# Changelog ## [0.5.0] - 未发布(开发中) > 0.5.0 **尚未发布**:UI 对齐仍在进行(聊天区 / 输入框 / 会话标签页 / 实机验收)。收尾计划见 > `PLAN_UI_PARITY_2026-10-20.md`;验收全部通过前不打 tag、不发布 npm / GitHub release。 ### 重启后的 usage/cache 统计与标签高亮(2026-10-25) - **usage 快照随会话下发**:`session-snapshot` 新增可选 `usage`,host 在首次 mount 与切换会话时先重放 durable `assistant/message` 事件中的 usage,再把 token/cache/context/cost 投影放进 snapshot;renderer 不再在 snapshot 重置时无条件清空 usage,同会话刷新也会保留当前统计。这修复了重启、daemon 重连和标签切换后 cache 信息消失。 - **cache 观测口径**:`UsageFold` / `UsagePayload` 新增 `cacheReadObserved`。会话中只要至少一条采样显式上报了 `cacheReadTokens`,后续/历史省略该可选字段的采样按 0 计入累计命中率;从未上报过 cache-read 的适配器仍显示 `cache —`,不会伪造 0%。只上报 cache-read、不上报 cache-write 的 provider 继续正常显示。 - **持久化来源核实**:dsh 0.2.1 的 `assistant/message.data.usage` 已随 `session.v4.jsonl.zstd` 持久化 cache bucket,因此不需要额外 sidecar;host 的 replay 才是此前丢失统计的断点。 - **激活标签页高亮**:active chip 从 `decrease(background.raised.base)` 改为 `background.raised.high`,编号与标题使用 bold;新增 frame 回归测试对比 active/idle 背景与属性。 ### dsh 0.2.0 / 0.2.1 双版本兼容(2026-10-25) - **session-title**:不再静态导入单侧 API。通过命名空间加载 `@deepseek-ai/dsh-session-title-llm`,运行时识别 0.2.0 的 `generateSessionTitleWithLlm` 或 0.2.1 的 `executeSessionTitleLlm`;新版路径补齐 provider 自有的 system prompt、JSON framing、最小 reasoning effort 和输出解析。 - **终端控制器**:0.2.0 的 `follow/write/resize` 与 0.2.1 的 `attach` Remote stream 由同一适配层承接;新版路径通过 in-process `typertGateway.stream` 构造 uplink,按顺序发送 write/resize,把 snapshot/output/state 投影回原有 IPC 帧格式,并将新版 `writable` 映射为渲染器依赖的 `controllerId`,过滤 ack 帧。 - **依赖与契约**:devDependencies 对齐 `0.2.1-rc.2`,peer 范围同时覆盖 `0.2.0-rc.2` 与 `0.2.1-rc.*`;`UPSTREAM_VALIDATED_VERSIONS` 加入 `0.2.1-rc.2`。 - **验证**:新增 session-title 与 terminal-controller 双 API 兼容单测;宿主 338 项测试通过(337 pass / 1 skipped),`pnpm run build` 通过;已用 dsh 0.2.1-rc.2 启动 tui profile smoke,0.2.0-rc.2 的 legacy 分支由单测覆盖。 ### 核心守护进程、后台退出、子代理侧栏与设置汉化(2026-10-23) - **核心守护进程**:`dpsk` 默认启动/连接本地 headless core,再只启动当前 TTY 的渲染器外壳;Windows 使用命名管道,macOS/Linux 使用 Unix domain socket,令牌写入 `~/.config/.mydpsk/runtime`,不开放 TCP。新增 `dpsk daemon status|start|stop|restart|logs`;`DPSK_EMBEDDED=1` 保留内嵌路径。Windows 上 core 通过 `wscript` 以隐藏窗口启动,不再额外弹出后台终端。 - **退出语义**:`/exit`、空输入框 `Ctrl+C`、leader `q` 和关闭终端都只 detach TUI;核心与任务继续运行,重新 `dpsk` 会重新握手并重放 snapshot。`/shutdown` 与命令面板的全量退出命令单独确认后终止核心。 - **运行中任务提示**:daemon 模式退出且存在 running 会话时,弹窗提供退出界面并在后台继续、打开全部进行中会话、取消;内嵌模式明确警告退出会中断任务。 - **开机启动**:设置页新增核心登录启动开关,Windows 写 HKCU Run,macOS 写 LaunchAgent plist,Linux 写 XDG autostart desktop 文件;行状态从真实 OS 注册读取,注册失败返回错误且不显示为已开启。 - **子代理与后台任务侧栏**:侧栏新增默认折起的 Subagents 和 Background tasks 两节,标题显示数量;子代理行打开子会话标签,可继续的子代理可直接输入,one-shot 子代理显示只读提示和标签切换提示;后台任务行打开对应 shell job 面板。 - **独立 trace 面板**:移除侧栏轨迹,新增 `/trace` 与命令面板 Trace;面板可滚动,保留完整文本、reasoning、工具输入输出和错误,不再受侧栏 24 行上限限制。 - **标签页修复**:新增显式 activeTabId;`+` 新会话草稿与旧会话状态隔离,esc interrupt、read-only、usage、todos、subagents 和 shell jobs 不再串台;同会话切换强制重放 snapshot,关闭标签不影响后台任务。 - **设置页汉化补全**:分类导航、新思考样式、主题模式、路径显示、开机启动和运行时错误全部走 i18n;中英文键保持一一对应,缺失键由门禁测试拦截。 - **协议**:新增上行 `shutdown` 与 `autostart-op`,`session-list` 行增加 `sessionKind` / `canInteract`;握手支持 daemon 重连后的重复 hello/snapshot,宿主在无 TUI 连接时对人类审批与提问 fail-closed。 ### 侧栏宽度、CTX/费用、配置热更新与启动/切换优化(2026-10-23) - **侧栏宽度**:`SIDEBAR_WIDTH` 由 42 收窄到 40,docked/overlay/terminal 默认宽度同步;几何测试改为 40 列。 - **thinking header**:reasoning 流进入首个 text delta 时补 `bodyAt`,compact header 立即显示 `Thought Xs | Used N Tools`,不再等正文结束。 - **cache 命中修复**:usage fold 拆分 `cacheReadComplete` / `cacheWriteComplete`;provider 只报 cache-read 时 sidebar 和 status bar 仍显示命中率。 - **CTX/SUM/$**:侧栏 context 区改为 `CTX 14.5k`、`SUM 14.3k (40% cache)`、`$ 0.0123`,token 单位动态 k/m。 - **本地费用估算**:host 按 assistant sample 的 provider/model/time 逐条计价,支持 cache 分桶、USD 和时区峰谷规则;未匹配到价格时显示 `$ 未配置模型价格`。价目表来自 EasyCLIProxyAPI 0.3.29 的 model_prices,并支持 provider/模型别名自动匹配。 - **配置目录**:program-owned config 迁到 `~/.config/.mydpsk`;首次启动迁移 backend.json、生成 config.json/price.json/custom_price.json,并保留旧 backend 文件。 - **配置热更新**:fs.watch + debounce + 2s polling fallback;invalid/partial 文件保留 last-good;`custom_price.json` 更新会重算当前 usage。 - **/price**:无参数从 `price.json` 选择价格,带参数按 id/别名匹配;把选中价格写入 `custom_price.json` 的当前 provider/model,优先级高于程序公开价目表。 - **/restart**:dpsk supervisor 以专用 exit code 重启 dsh 子进程,重新加载 DSH 侧配置;保留 session、tabs 和本地持久化草稿,普通退出不重启。 - **启动/切换快路径**:backend 文件签名命中跳过 PATH/version 探测(warm `dpsk --version` 约 838ms → 246–283ms);host startup migrations 与 terminal 初始化并行;cold resume 去掉全量 session-list 预检;session-list 刷新 coalesce;transcript projection 按 session 缓存。 - **会话活跃时间修正**:排序改用最近一次真实交互事件(user/assistant/tool/turn/command),忽略 `session/end-seed`、`session/title`、`workspace/changes` 等 replay/bookkeeping;today/yesterday 改为严格本地日历日差,不再使用宽泛的连续时间窗口。 - **模型切换不再重绘整页**:新增 route-only `selection-state` IPC;模型/effort/preset 切换只更新 provider/model/effort/permission/preset/attachment capabilities,不再重发含完整 transcript 的 `session-snapshot`。renderer 先乐观更新 route,避免等待 settings 写入,并消除切换时 `standard` 预置闪回和短暂冻结;image capability 更新也走 route-only。 - **permission 选择器修复**:permission catalog 是进程级数据,不再被 session-snapshot/draft 清空;`/permission` 在 catalog 到达前提供 dsh-base 的 workspace-write / full-access 兜底;host 使用本地化 label,不再显示原始 registry key。 - **启动快路径继续缩短**:首帧不再等待 startup migrations 与 terminal controller 挂载;session rail 先用磁盘 cache 立即绘制,300ms 后后台刷新真实 session-list;workspace files / skills 延后到首帧之后;旧空 session 清理最多每 6 小时一次。dsh 子进程启用 Node compile cache。 - **renderer bytecode**:Bun standalone renderer 启用 `format: 'esm'` + `bytecode: true`,实测本机 sidecar 启动约 150ms → 75ms;同时保留完整 IPC handshake。 - **底部轮/步统计**:`T` 改为真实用户消息数(含被打断消息、排除模型切换提示),`S` 改为有工具调用的唯一 `(turn, step)` 数;不再依赖最后一条 assistant 行,因此不会在流式/中断/无最终回复时消失。并行工具同一模型步只算一步。 - **Jump to latest 定位**:按 textarea 实际宽度计算软换行后的视觉行数(含附件行),提示条始终落在输入框上方空白行,不再覆盖多行/长行草稿。 - **launcher symlink 修复**:全局 npm install 通过包目录 symlink 暴露 `dpsk.js` 时,主模块判断改为 realpath 比较,避免 `dpsk` 静默不执行。 - **输入框路线行重排**:改为 `模式 权限 · 模型 · 思考强度`;模式跟随主题 accent,权限使用思考 warning 色,模型使用正文色,思考强度使用独立 cyan accent。provider 不再出现在输入框,只保留在侧栏;中文权限文案采用 `完全权限`。 - **上次选择持久化**:模型的 provider/model、思考强度和 agent mode 写入 `~/.config/.mydpsk/config.json` 的 `lastSelection`,启动时自动恢复;历史 session 自己的 request header 仍优先。首次启动也会把当前 route/mode seed 进配置,升级后无需先打开选择器。 - **思考块交互修复**:live compact 块从展开收回时返回动态 preview,而不是变成空的 closed 状态;点击展开后的 reasoning 正文也可折叠,不再必须滚回标题;settled 块点击正文折叠为单行 `Thought` header。 - **Write 工具卡片**:`write` 从 inline 行改为 opencode block card:标题只显示 `# Wrote `,文件内容放入受 `maxToolOutputLines` 限制的卡片正文,不再把整段 content 当成标题裸文本铺开。 - **Running 标签补上下文**:pending inline 工具行不再只显示 `Running…`;Read/Glob/Grep/Write/Edit/Web/Subagent 等都会带具体路径、pattern 或参数,未知工具也会显示工具名和第一个参数。 ### 会话清理、紧凑选择器、OpenCode tabs 与 ~ 路径(2026-10-23) - **空白 live session 彻底隐藏**:`session-list` 新增内存事件判定,未物化且无对话事件的 live agent 也标 `blank`; picker/tab catalog 过滤全部 blank(包括 current),Home 仅由独立草稿 tab 表示。 - **会话选择器单行化**:每行只画标题和右对齐 `today` / `yesterday` / `last 7 days` / 日期;移除 id、cwd、父子关系、 status 和 details。current 只保留左侧圆点;隐藏 `searchText` 仍可按 id/workspace 搜索。 - **OpenCode v2 横向 tabs 核心完成**:新增纯 `session-tabs-model`,稳定手动顺序、title snapshot、close/reopen 原位、 close-others、move、cycle、catalog prune 和 v1→v2 `tabs.json` 兼容。顶部 strip 不再被 host lastActivity 顺序重排。 - **草稿 tab 与首条消息提升**:`+` / `leader n` 只创建一个 Home `New Session` 草稿,不创建 host session、不写盘; 首条消息成功后在原 index 提升为真实 tab,失败保留草稿。 - **tab 交互**:active `×`、inactive hover `×`、中键关闭、右键菜单、拖动重排、`Ctrl+1..0`、真实 running 状态点; draft 菜单不提供无效的 rename。 - **运行态 footer 修正**:spinner + `esc interrupt` 改为替换左侧 cwd,右侧继续显示 `shift+tab agent · ctrl+p commands`。 - **用户目录 `~` 显示**:Windows/POSIX 纯函数、大小写/相似前缀边界、窄列保留 `~` 锚点;新增设置 `path display: ~ / full path`, 默认 `~`,仅影响 UI 显示,不影响 workspace/请求/附件/terminal/导出。 - **Compact thinking 分组修正**:assistant 中间正文只结束当前 block;同一 turn 后续 reasoning/tools 重新开启 compact block,不再回落 classic。 - **用户卡片间距**:compact ThinkingHeader 与 classic ReasoningRow 统一 marginTop=1,用户文本与下一条 thinking/正文恰好隔一行。 - **Jump to latest overlay**:改为输入框上方右侧的可点击 absolute overlay,不占 transcript 行;点击回到底部,补全面板打开时隐藏。 - **测试隔离与验证**:测试 Shell 默认注入内存 storage,避免读写真实 `~/.dsh/state`。宿主 `npm run check` 290 项 (289 pass / 1 skipped)、渲染器 380 项全绿;新增 tab model、路径、picker、hover/拖拽、compact 多 block、用户间距与 jump overlay frame 回归。 ### 思考/工具呈现重构、选择高亮与会话生命周期(2026-10-22) - **`newDesigns` 语义改为思考样式开关**:`/settings` 现在显示 `new thinking style`,即时在 classic 与 compact 之间切换; 它不再控制 `opencode2` 的主题候选,也不再移动当前主题。classic 保留原有单行 `Thinking` / `Thought` 与工具卡片。 - **compact transcript**:正文前的多段 reasoning、多个工具调用和状态更新合并成一个块;默认显示 5–10 行自动尾随预览, 工具为 `Running` / `Done` / `Failed` / `Denied` 单行并按显示宽度以 `...` 截断;点击标题展开完整 reasoning Markdown + classic 工具行/卡片,再点收起。手动展开后正文到达不会强制折叠。 - **时长与工具计数**:标题使用 `Thinking 12s | Used 2 Tools` / `Thought 1m 05s | Used 1 Tool`,不足 1 分钟不显示 `m`, 无工具省略 `Used`,无时长不显示 `0s`;共享 1 秒 clock 只重绘标题。host 的流式行补齐 `createdAt`,第一条答案 delta 记录 `bodyAt`。 - **文本选择**:单击产生的零长度 selection 自动清理;提交、切换会话、`turn/start` 清空非空选择;拖选颜色改为主题选择色, Markdown 子 renderable 也会被遍历着色;拖选思考标题不会误触展开。 - **resume Markdown**:host 新增 `joinTextBlocks()`,模型把一个强调/行内代码 span 拆成多个 text block 时用单换行续接, 不再因插入空行而泄漏 `**`。OpenTUI 0.6.6 升级因 `SyntaxStyle` 资源所有权 API 变更被回退;0.5.17 的列表项 inline strong conceal 缺陷用仅列表项去标记的定点兜底处理,段落 strong 与 fenced/inline code 不受影响。 - **空会话不落盘**:新增 `lazySessionPersistence` conversation-gated wrapper,`create()` 与 setup append 先缓冲, 直到第一条 `user/message` / `assistant/message` / `tool/call` 才创建真实后端会话并回放 setup;只有 setup 的 handle 在 flush/close 时丢弃且不调用后端。启动时安全清理历史 4-event `tui-*` 目录,`/resume` 过滤读头确认的空白会话。 - **会话按最后交互排序**:`session-list` 新增 `lastActivityAt`;live 会话取最后事件时间,persisted 会话用 `(id, revision)` 缓存并只读 tail event;host 列表、`/resume` 和 renderer 的 today/yesterday 分组统一使用它,缺失回退 `createdAt`。 - **文档与验证**:README、contracts、实机验收清单已同步;新增 thinking/text/clock/session-activity/session-prune/lazy-persistence 测试、clock 帧回归与 40/80/120 列帧测试;宿主 `npm run check` 288 项(287 pass / 1 skipped)、渲染器 348 项全绿,7 平台渲染器产物构建成功。 ### 思考未结束显示 `Thinking`,结束后才是 `Thought`(2026-10-22) - **症状**:思考块的标题在第一个推理 delta 到达时就变成了 `Thought: …`,可思考其实还在继续。 - **上游形状**:opencode 的 `ReasoningPart` 在 `isDone` 之前一律是 `Spinner + Thinking`(折叠态带 `: 摘要`),`isDone`(**推理部分**的完成,不是整步完成)之后才画 `Thought[: 摘要][ · 时长]`。我们原来 拿「推理文本是否为空」当判据,于是第一个 delta 就翻牌。 - **修法**:流式行新增 `reasoningDone` —— 一步里第一条**答案** delta 到达时置位,交错思考里再来的 `reasoning-delta` 会清掉它(标题退回 `Thinking`)。渲染器 `ReasoningRow` 在 `streaming && !reasoningDone` 时画旋转器 + `Thinking`(有摘要时 `Thinking: 摘要`),之后才画 `Thought` 单行标题;`streaming` 缺省的稳定行不看这个标记,仍按 `Thought` + 该步 `durationMs` 渲染。 - **验证**:`streaming.test.tsx` 新增「第一个答案 delta 把 Thinking 换成 Thought」「稳定行忽略残留标记并 保留时长」,原「推理 delta 后填同一行」改为断言 `Thinking: Plan` 且不出现 `Thought`;宿主 273 项、 渲染器 288 项、双端 `tsc` 与门禁全绿。 ### 连续两次 ctrl+c:一条幂等的退出路径(2026-10-22) - `/exit` 与 `ctrl+c` 现在共用渲染器的 `requestQuit()`:`quitRequested` 保证只放行一次 —— 第一次发 `bye`、写恢复序列、`shutdown()`,第二次落在正在关闭的通道上被挡住,不会再发一条 `bye`、也不会重放 恢复序列(`ctrl+c` 的优先级仍是:清文本选区 → 关最上层对话框 → 清草稿 → 空输入框才退出)。 - 宿主侧 `channel.send` 在渲染器进程已经退出时会抛 `ERR_IPC_CHANNEL_CLOSED`,现在包了 try/catch: 收尾阶段的一次多余发送只是被丢掉,不再在用户回到提示符后打一段栈。 - **验证**:`app.test.tsx` 的「double ctrl+c」用例(两次 `cancel` 只发一条 `bye`、只退出一次)与 `/exit` 恢复序列用例;宿主 `renderer-mount` / `renderer-process` 用例覆盖优雅 `bye` 与恢复顺序。 ### 默认模型跟随上次退出 · 修复不能持久化的设置项(2026-10-22) - **默认模型跟随上次退出**:`commitSelection` 原来只调用 dsh 的 `agentDefaultModel.saveSelection()`, 而那条路径在拿不到 loader 的 `configEditor` 服务时**静默返回** —— 于是选完模型只改了当前会话, profile 的 `agent-default-model` 一直停在 `deepseek-v4.1-flash`,每次重启都回到它。现在改走设置服务 (`settings.update(settingsNamespace('agent-default-model'), { provider, model, reasoningEffort })`), 也就是 `/settings` 已经在用的那条写入路径:落进 profile 的 patch 层,下次启动的新会话就用这个路由。 写失败只发一条 warning,不影响本次选择。 - **`newDesigns`(新界面样式)存不住、也不生效**:宿主发给渲染器的 `settings` 快照里写的是 `newDesigns: config.newDesigns === true` —— 但该字段是 `.volatile()`,`config.newDesigns` 是**活引用对象**, 永远不等于 `true`,所以渲染器每次都收到 `false`(配置里其实早就写进了 `newDesigns: true`)。改成 `resolved.newDesigns`(`liveValue` 解包后的值)。 - **持久化的选择在挂载后才到达**:渲染器在首帧就读取一次 `storage.selection()`,而 `settings` 负载是握手之后 才发出的,于是持久化的主题 / `newDesigns` / 模式可能被默认值覆盖。`ThemeProvider` 现在用 `createEffect` 跟随设置节变化(先应用 `newDesigns` 再应用主题名,否则 `opencode2` 还不在可选列表里)。 - **验证**:新增「`settings` 负载在挂载后到达时切到持久化选择」用例(断言 `newDesigns` 变 true、`opencode2` 进入可选列表并被选中);宿主 267 项、渲染器 286 项、双端 `tsc` 全绿。 ### 退出后正确恢复终端:鼠标模式、备用屏与日志串台(2026-10-22) - **症状**:退出 dpsk 后 shell 把 `[555;109;10M` 当输入回显(X10 鼠标上报)、TUI 最后一帧留在屏上、 `[dsh-wechat-ilink] …` 之类插件收尾日志直接打在提示符上。 - **根因三处**:①渲染器一接手 TTY,宿主就把 `process.stdout.write` 换成写日志的函数(`renderer-log.ts`), 而恢复序列(离屏 / 关鼠标 / 显示光标)正是通过它写的 —— 全部落进了日志文件,一个字节没到终端; ②渲染器 `/exit` 只发 `bye` 等宿主收尸,宿主收到 `bye` 立刻 kill,把恢复写入截断;③`stop()` 先 kill 后写, 写的时候 stdout 还被路由着。 - **修法**:新增 `writeTerminal()`(`fs.writeSync(1, …)`)绕过被替换的 writer 直写终端 fd,`spawnRenderer` 与 mount 的默认 TTY 写入口都改用它;`TERMINAL_RESTORE` 补齐顺序(SGR → 关鼠标/粘贴等模式 → `2J` 清屏 → `?1049l` 离屏 → `?25h` → 复位标题;先清屏再离屏,避免清掉用户的滚动缓冲);`stop()` 先写恢复再 kill; `bye` 走优雅路径不再 kill(只写一次恢复,让渲染器自己收尾),`2s` 看门狗兜底(`byeGraceMs` 可注入); 渲染器 `/exit` 除发 `bye` 外还调用自己的 `shutdown()`(新增 `createAppStore(..., requestExit)` 钩子), 清屏 + `destroy()` + 完整恢复 + 退出;优雅路径下不再 `restoreLog()`,插件收尾日志留在 `$DSH_HOME/logs/tui-.log`,不再打在刚恢复的提示符上。 - **验证**:`bye` 断言从「kill 一次」改为「不 kill、写出恢复序列、退订一次」,新增「渲染器赖着不走时看门狗 2s 后 kill」「`/exit` 触发渲染器自身恢复」「恢复序列先清屏再离屏、关掉 1002/1003/2004」;宿主 267 项、 渲染器 284 项、双端 `tsc` 与门禁全绿。 ### 命令面板去掉会话分组 · 其余对话框文案补汉化(2026-10-22) - **命令面板不再展开会话**:面板里每开过一条会话就多一行,历史一长整张表就被撑开,读起来像第二个 会话选择器。现在面板只列命令与目录(思考强度 / 技能 / 权限 / Agent 模式 / 微信),会话走 `/sessions` 选择器(面板里的 `list sessions` 行照旧打开它)。`paletteOptions` 的 `sessions` 入参一并删掉。 - **补上剩下的一批英文对话框**:标签页右键菜单(`关闭标签页 / 关闭其它标签页 / 标签页左移右移 / 重新打开 / 重命名`)、`导出会话`(Markdown / JSON)、`排队消息`、`后台任务`(含空态与停止确认)、 `终端`(新建 / 关闭确认)、`会话`选择器的两个作用域与计数、`MCP 服务器`、设置页的 `供应商表单` / `自定义模型` —— 共 25 个键,zh/en 各一份。 - **验证**:面板用例改成断言「没有 `session:` 行、没有会话分组、`list sessions` 动作仍在」;宿主 266 项、 渲染器 283 项、双端 `tsc` 与门禁全绿。 ### 界面语言随系统 · 斜杠命令对齐 opencode(2026-10-22) - **为什么之前“汉化了还是英文”**:`tui.locale` 的兜底是写死的 `en`,本机的配置里没有这一行,于是 整套字典(含 `cmdSettings` 这类斜杠命令描述、侧栏分节标题、底栏提示)都取的是英文表。现在 `DEFAULT_LOCALE` 直接是 `zh-CN`(中文优先)。**不嗅 `Intl` 也不嗅 `LANG`**:dsh 把插件宿主跑在 ICU 裁剪过的 Electron 进程里,中文 Windows 上 `Intl.DateTimeFormat().resolvedOptions().locale` 报的是 `en-US` —— 上一版就是被这个坑骗回了英文。要英文就写 `locale: en`,或者用 `/settings` 新增的 「界面语言」一行(写 `update-settings` 落到 patch 层,提示重启生效)。 - **斜杠命令改用 opencode 的名字**:`/models`、`/agents`、`/effort`、`/sessions`、`/themes`、`/status` 取代原来的 `/model`、`/mode`、`/think`、`/resume`、`/theme`、`/details`。旧名字保留为**别名** (`/think` `/variants` `/thinking` 都指向 effort,`/resume` `/continue` 指向 sessions …),词表、 补全、参数补全、命令面板条目 id 一起改,宿主下发的 `command-catalog` 也换成了新名字。 - **没有硬塞 opencode 里 dsh 做不到的命令**:`/undo` `/redo`(无快照层)、`/copy`(无剪贴板)、 `/timeline` `/editor` `/cd` `/worktrees` `/diff` `/stats` `/pair` `/update` `/restart` `/reload` `/connect` `/highlights` `/open` `/debug` 一律不加;`/permission` `/doctor` `/plugins` `/palette` `/rename` `/queue` 这些 dsh 独有能力保留原名。 - **验证**:`tests/i18n.spec.ts` 覆盖 `detectLocale('zh-CN'|'zh-Hans-CN'|'en-US'|'ja-JP')` 与 “显式 locale 优先”;渲染器用例改成断言新名字(`/models`、`/sessions`、`action:slash:/models`)且 旧别名仍可拦截;宿主 266 项、渲染器 282 项、双端 `tsc` 与门禁全绿。 ### ctrl+p 菜单汉化 · 斜线菜单换成 opencode 形状(2026-10-22) - **命令面板全部走宿主字典**:分节标题(`操作` / `思考强度` / `会话` / `技能` / `权限` / `Agent 模式` / `微信`)、16 个动作行(`切换模型` / `重命名会话` …)、四类动态行(排队消息、 后台任务、终端开关)、过滤框占位(`搜索`)与底部提示(`移动` / `运行`)都改成 `store.label(...)`,zh 下不再出现 `switch model` / `rename session` 这类英文单词。 条目 id 与 `searchText` 仍是英文,所以按 `model`、`queue`、`terminal` 照样能搜到。 - **分节标题取代前缀**:行不再写 `reasoning effort: high`、`permission: Auto`、`skill xxx`、 `mode: standard`——标题头已经说了名词,行里只留值(`max`、`Auto`、技能名、预设名)。这也是 opencode 面板的形状,同时把「前缀要翻译」的重复文案消掉了。 - **斜线菜单换成 opencode 的 popup**:行改成 `标签 + 一个空格 + 描述`(描述弱化色),右侧只给 `@` 引用行一个右对齐标记;原来那列 `command` / `argument` / `reference` 是 omp 的 kind 列, 已删除。标签按可用宽度截断,描述可压缩换行(`flexShrink`),空结果走本地化的空状态 (`没有匹配的文件、Agent 或引用`);高亮底色与面板同色(`background.action.primary.$hovered`), 两个菜单读起来是一套。 - **同源文案一起收口**:未命名会话的芯片 / 侧栏标题 / 会话选择器统一显示 `未命名会话` (`rendererUntitled`)。 - **验证**:新增「面板与 popup 显示宿主字典而不是英文 id」用例(推一份 zh 字典,断言 `命令面板`/`搜索`/`移动`/`运行`/`操作`/`重命名会话`/`切换模型` 上屏、且整帧不出现 `rename session` / `switch model`;popup 空状态中文);宿主新增 26 个字典键(zh/en 各一份), 宿主 266 项、渲染器 282 项、双端 `tsc` 与门禁全绿。 ### 显示统一:预设名与模型路由的写法(2026-10-22) - **预设名只有一个拼写**:输入框元信息读的是 `agent-presets` 目录里的**展示名**(`标准模式`), 而回答 footer、Home 输入框贴的是 `preset()` 的 **id**(`standard`)—— 同一屏两个写法。现在统一由 store 的 `presetLabel()` 提供(目录名 → 回落 id):会话与 Home 两条路由的元信息、回答 footer、 `/details` 都读它;footer 在预设未知(目录还没到)时不再留下一个空的 agent 段。 - **模型路由只有一个顺序**:元信息原按上游写成 `model provider`(`deepseek-v4.1-flash cliproxy`), 而侧栏、`Model set to …` 通知、`/model` 选择器的值都是 `provider/model`。现在元信息也写成 `provider/model`(provider 弱化色 · `/` 弱化色 · model 正文色),一屏之内不再有两种顺序; narrow(<44 列)仍然只丢 agent 与 `auto` 徽标。 - **验证**:新增渲染器用例「提示行与 footer 用同一个预设名」(目录给 `标准模式`,断言两处都显示它、 整帧不出现 `standard`),并更新了元信息格式相关的既有断言;渲染器 281 项、宿主 266 项、双端 `tsc` 与门禁全绿。 ### 长轮次里的工具行、思考时长与轮次汇总(2026-10-22) - **工具行卡在 `Running…`**:实时路径把每个事件单独折一次(`transcriptEvents([event])`),而 `tool/result` 靠本次折叠里 `tool/call` 建的 `callId → row` 表找行 —— 单独一个 result 事件时表是空的, 于是直接 `break`,**一行都不发**,界面上那行永远是 pending。快照/重放能看到结果,所以重启就正常。 现在折叠状态(`TranscriptFoldState`)由宿主持有并按前台会话复用:`tool/call` 把行登记进去, `tool/result` 就地改同一个行对象并**把它再发一次**(按 id 原行替换),pending 才真的会变成结果。 - **思考没有时长**:同一个原因 —— `assistant/message` 的 `durationMs` 来自本批次里的 `step/start`, 单独到达时是空的。状态里保存 `step/start` 之后,`Thought` 头恢复 `- Thought: 1.2s`。 - **footer 每个中间轮次都出现**:`assistant/message` 是「一步」而不是「一轮」,之前每一步都画 `agent · model · 时长 · tok/s`,工具还在跑却像是结束了。现在折叠会给**每轮最后一个 assistant 行** 打 `footer: true`,`turn/end` 时再把这行更新一次;渲染端只画带这个标记的行。中断/失败的一轮同样在 `turn/end` 收口(事件里没有持久 assistant 消息时,用流式那一行收口)。 - **轮次级 tok/s**:footer 的数字改成整轮汇总 —— 分母是该轮每一步的模型耗时之和 (`assistant/message` 时间 − `step/start` 时间),工具执行发生在每一步消息落定之后,所以天然被剔除; 分子是该轮所有 step 的 `outputTokens`。每步的 `durationMs` 仍保留给 `Thought` 头。 首字延迟(TTFT)没有持久记录,无法单独剔除。 - **验证**:新增/改写宿主用例覆盖「多步一轮只有一个 footer」「工具时间不进分母(160 tokens / 3s = 53 tok/s,另 5s 工具调用不计)」「单事件增量折叠能把工具行收口、能给 footer、能给出步时长」;渲染器 新增「中间步骤不画 footer、只有最后一step画」;宿主 266 项、渲染器 280 项、双端 `tsc` 全绿。 ### 上下文窗口与推理强度:两处都查到了根因(2026-10-22) - **底栏的 `262.1k` 来自 profile,不是估算 bug**:窗口优先取会话最后一条 `request/context` 事件, 否则取模型元数据(`entry.contextWindow → 目录 → 路由 defaultContextWindow`)。TUI profile 的 `cliproxy` 路由只写了 `defaultContextWindow: 262144`,而 `deepseek-v4.1-flash` 条目没有自己的 `contextWindow`,于是整条路由的模型都按 262144 算。桌面 profile 里同名模型写的是 `contextWindow: 1000000` —— 这就是「记得是 1M」的来源。已在 `profiles/tui/cordis.patch.yml` 里给 该模型补上 `contextWindow: 1000000`,并把桌面端声明的 `input: [text, image]` 一并对齐(否则这条 路由会被判定为不支持图片输入)。 - **恢复会话时窗口不更新**:`refreshContextEstimate` 以前只要 `request/context` 里记录了窗口就直接 `return`。改了 profile 之后,恢复的会话仍按旧窗口算百分比,直到再发一条消息才会写新的 `request/context`。现在仍先显示记录值(同步兜底,避免闪一下 1M 默认),随后异步取一次模型元数据并 让它胜出。 - **`max` 被网关改写成 `high`**:证据链在日志里,两侧都可查。 1. 会话日志 `request/header`:`{"provider":"cliproxy","model":"deepseek-v4.1-flash","effort":"max"}`; 2. 网关(EasyCLIProxyAPI,`oauth/logs/v1-chat-completions-*.log`)的下游请求体: `"reasoning_effort":"max"`、`"max_tokens":32768`; 3. 同一份日志里网关**转发给上游**的请求体:`"reasoning_effort":"high"`,模型 id 被改写为 `deepseek/deepseek-v4.1-flash`。 桌面端(dsh 0.2.1)的请求同样带 `max`、同样被转成 `high`,所以这不是 TUI 或 dsh 的差异,也不是 dsh 适配器在降级:dsh 侧 `reasoningEfforts.max: max` 原样发出,`UNSUPPORTED_REASONING_EFFORT` 也 没有触发。要 `max` 真正到达上游,只能在网关侧配置/反馈;直连时 `max` 会按原样发送。 ### 单会话内切换模型:显示跟随与切换通知(2026-10-22) - **症状**:上一轮修好路径后,点选模型会弹 `Model set to …`,但输入框元信息与侧栏仍显示旧模型。 - **原因**:`snapshotFor()` / 目录推送用 `selectionFor()` 取路由,而它折叠的是会话的 `request/header` —— dsh 在**请求派发时**才追加这个事件,所以刚选完、还没发消息时它仍指向旧模型。 真正决定下一次请求的是 `selectionRef.current`(`installModelSelection` 注入的那个)。 新增 `displaySelection(target)`:前台会话取该 ref(子会话仍读自己的 header),快照、模型目录、 强度目录、图片能力探测都改用它 —— 选完立刻就能看到新模型。 - **切换通知**:dsh 在路由变化时会把一条 `form: 'notice'` 的 user 角色消息写进日志 (`source.kind === 'model-selection'`,`summary` 形如 `mimo-v2.6-pro → cliproxy/deepseek-v4.1-flash`), 供读者确认切换。transcript 之前把所有带 `form` 的注入一律丢弃(那是为了避免把 harness 的提示词 脚手架当成人话显示),于是这条**唯一给读者看的**注入也被吞了 —— 切换在会话里不留痕迹。 现在按 `summary` 渲染成 `↳ from → to` 的系统行(弱化色,不画用户卡片),其它 `form` 注入照旧丢弃。 - **目录跟随**:`onSelectionApplied` 接到整组会话目录(强度、预设、权限、模型)重建,`/effort` 在下一次打开时给的是新模型的档位,模型选择器里的 `current` 也落在新模型上。 - **验证**:新增宿主用例(通知保留 + 无 summary 的同类注入仍丢弃)与渲染器用例(`↳` 行、无 `┃` 卡片,真实用户行仍有卡片);宿主 263 项、渲染器 279 项、双端 `tsc` 全绿。 ### 模型切换:Home 上选的模型没有进请求(2026-10-22) - **症状**:在 Home 用 `/model` 选了 `deepseek-v4.1-flash` 再开始对话,输入框元信息与侧栏都显示 deepseek,但回答 footer(以及会话里记录的 `message.source.model`)仍是旧的 `mimo-v2.6-pro`。 - **原因一(渲染端)**:`applySelection` 在 `home` 路由只写本地 signal 就 `return`,`set-selection` 从来没发出去。Home 展示的是一个**真实(空白)会话**,第一条消息复用它,所以宿主一直拿着自己的旧路由。 现在两个路由都写宿主(没有会话时跳过),本地 signal 仍然先行,观感不变。 - **原因二(宿主)**:`commitSelection` 在 `agent !== target` 时提前 `return`,而 `target = agent ?? liveAgent` —— "渲染端还没切过会话"(每次启动、以及 Home 上的一切选择)恰好满足 这个条件,于是 `selectionRef.current` 从不更新。而 `installModelSelection` 正是把该 ref 的 `current` 注入下一次 `system-prompt/assemble` / `agent/request`:ref 不动,请求就还是旧模型。现在总是更新 ref、 重算上下文估算并推快照。 - **附带**:`saveSelection` 对"选中的就是当前路由"去重(重复点同一个模型不再多一条 `model set` 通知、 也不再重写默认值);`onSelectionApplied` 接到 `rendererCatalogPush`,切换后再次打开 `/model`, `current` 标记落在新模型上(此前快照只带路由、不带目录,标记停在旧模型)。 - **验证**:新增渲染器用例「Home 上的模型选择会写到宿主且保持 home 路由」;渲染器 277 项、宿主 262 项、 双端 `tsc` 全绿。 ### 输入框配色、思考区配色与主题对齐(2026-10-22) - **输入框左侧竖条**:`╹` 收口用的是 `border.base`(灰)而竖条本身是强调色,紫条末端接了一个灰疙瘩; `▀` 阴影又用 `background.raised.base` 而不是输入框自己降一档的底色,接缝处颜色不连续。现在按上游的 形状重排:外层边框盒子不再带背景(强调色竖条直接压在页面上),内层盒子画底色;`╹` 与竖条同色、`▀` 与面板同色。Home 与 Session 两个输入框改成同一形状,`borderColor` 抽成 `promptBorder()` 一处。 - **元信息分色**:`agent auto · model provider · effort` 以前 model 与 provider 是同一串、同一色, effort 用正文色。改成上游 `PromptMetadataRow` 的取色:agent = 强调色(与竖条同色)、model = 正文色、 provider = 弱化色、effort = 警告色**加粗**;`auto` 徽标改弱化色,agent 与 `auto` 之间是空格不是 `·` (上游用 `gap`),narrow 断行仍然只丢 agent/徽标。 - **底部提示亮/浅**:`shift+tab agents · ctrl+p commands` 拆成按键(正文色)与说明(弱化色)两段, 运行中的 `esc interrupt` 保持整段警告色(它本来就是告警而不是提示)。 - **思考区三色**(对标 opencode `ReasoningPart` / `generateThinkingSyntax`):`Thought:` 头以前没有 `fg`,继承默认色后看起来就是正文;思考正文走的是**回答的** syntax style,标题、强调、行内代码全是 回答的配色。现在头部是 `text.feedback.warning.base` 打开时 60%(混到 `background.base`)、折叠时满色, 正文改用新增的 `mutedSyntaxStyle`(把活动主题里每个已注册 token 重染成 `text.muted`,与上游 `generateThinkingSyntax` 等价,按 ink 缓存以免流式时每帧新建原生对象),回答仍是 `markdown.text`。 三者因此是三个可区分的颜色。 - **验证**:新增三个渲染器用例(四色元信息 + 提示两段、`╹`/`▀` 与竖条同色、思考头/正文/回答三色, 含折叠后头部满色),渲染器 276 项全绿;`tsc` 双端通过。 ### 底栏分家、`/resume` 标题与 dsh 后端选择(2026-10-22) - **底栏第二行固定、第一行跟随输入框**:原来两行信息都挂在 shell 根部,Home 上 prompt 浮在屏幕中间, 而 cwd 行被钉在终端最底下,看起来跟输入框没关系。现在只有指标行(`T… S… · tok/s | tokens · cache | context`)固定在最后一行;cwd/hint 行归到 prompt stack 里,Session 下贴在停靠的 prompt 下面(位置 与之前完全相同),Home 下跟着居中的 prompt 一起浮动、宽度跟 prompt 的 75 列上限走,弹窗锚点同步加了 一行(`popupAnchorRows`)。Home 的路由高度随之只预留一行(`STATUS_METRICS_ROWS = 1`),12 行地板那 组用例按新口径更新。 - **`/resume` 的标题与空会话**:dsh 只在标题生成器跑过时写 `session/title`;而每次终端启动都会物化一个 只有 4–5 条 setup 事件的空会话。本机 `list()` 出来 144 条,其中 108 条是这种空壳——它们既没有标题、 也没有内容,在列表里就是一片 "New Session" + id,这正是「和桌面端看到的不一样」的来源。现在宿主读 每条无标题会话的日志头(前 40 个事件),一次判定两件事:有没有 `user/message` / `assistant/message` / `tool/call`(没有就是空壳,随行推 `blank: true`),以及第一条用户消息的正文(压成单行、截到 60 字符 当标签)。渲染端对 `blank` 行只在它就是当前会话时才列出(与桌面端 `sessionVisible` 同规则), `/resume` 与 `ctrl+p` 的会话分类都走这个过滤。判定结果按会话 id 缓存,只缓存已定性的(空会话之后 才会说出第一句);这条路径只读日志头,不额外整份扫描。 - **dsh 后端与会话库统一**:会话库本来就是同一份(`$DSH_HOME/sessions`),但**读日志的是后端自己的 版本**——桌面客户端自带 0.2.1 运行时,而 PATH 上的 CLI 可能是 0.2.0,桌面端用新版本写下的会话 (`job/registered`、`job/settled`、`workspace/changes`)旧后端整条读不出来,`/resume` 里就少了那几条。 启动器现在在启动前枚举所有可启动后端(`DSH_REAL` + `PATH` 上的每一份 `dsh`,按真实路径去重)、读取 桌面端运行时版本(`$DSH_HOME/dsh-runtimes/*/runtime.json` 的 `desktopVersion`):多份时**首次启动 询问一次**并记在 `~/.dsh/dpsk/backend.json`(非 TTY 不阻塞),版本落后于桌面端时给一行提示与升级命令。 新增 `dpsk backend [list|reset]` 随时查看或重选。 - **验证**:`dpsk backend list` 在本机列出两份后端(`%APPDATA%\npm\dsh.cmd` 与 `%LOCALAPPDATA%\deepseek-harness\bin\dsh.cmd`)并给出 0.2.0 → 0.2.1 的差距提示;宿主管线 263 项 (262 pass / 1 skip,含新增 `tests/session-label.spec.ts` 与 `tests/launcher.spec.ts` 的后端选择用例)、 渲染器 271 项、两处 `tsc` 与边界/契约/插件规范/许可门禁全绿。 ### 命令菜单:方向键高亮跟随滚动(2026-10-22) - **症状**:`ctrl+p` 命令菜单里「有时候」上下方向键不好用,看不到高亮跑到哪一行;列表一长就 只能在最初几行里来回,`end` / `pageup` / `pagedown` 同样跳不见。 - **原因**:`DialogSelect` 的 `scrollToSelection` 把「第几行」当成 `scrollbox.getChildren()` 的 下标去取渲染节点。OpenTUI 的 `ScrollBoxRenderable.getChildren()` 返回的是 `content` 的孩子, 也就是这里按分类分组的容器(`actions` / `sessions` / `skills` …),**不是行**;行下标一旦 超过分组数就取不到节点,函数直接 return,滚动一次都不会发生。选中项本身一直在变(回车能选中 正确的行),但窗口不跟,高亮就滚到视野外——列表短、终端高时不需要滚动,所以看起来「偶尔」正常。 - **做法**:改成按**绘制行号**滚动。新增 `selectedLine()`,用与行数统计(`rows()`)完全一致的 口径算出选中行的起始行:分类标题占 1 行(非首个分类上方再有 1 行间距),每行占 1 行标题加 `details` 的行数;再用滚动条自己的 `viewportSize`(真实可视行数,含滚动条占位)把该行放进窗口。 单步 `↑↓` / 翻页只做最小滚动(列表不再无谓抖动),`home` / `end` 与 `alt+↑↓↓` 的跨段跳转才居中; `scrollTop` 由滚动条内部夹取,越界安全。 - **验证**:新增两条渲染器用例——20 行列表在 90×24 终端上连续按 9 次 ↓ 后,画面必须出现 `row10` 且不再显示 `row01`,回车取到 `v10`;`end` / `home` 必须分别露出 `row20` / `row01`。 两条在修复前都失败(画面停在 `row01`–`row05`),修复后通过;渲染器 270 项全绿、`tsc` 通过。 - **同一处收敛**:`dialog-question` 的滚动按行下标取 `getChildren()` 是对的(它的孩子就是行), 未改动;`dialog-shell-output` 的翻页视图同理。 ### 会话归组:终端会话进入 dsh 工作区(2026-10-22) - **症状**:用 dpsk 聊出来的会话,在官方桌面客户端里全部落在「未分组」;桌面端自己建的会话正常归属。 - **原因**:桌面端按 `~/.dsh/storages/workspace.json` 的 workspace 记录归组——只有出现在某个 workspace 的 `sessionIds` 账户里,会话才显示在该工作区下,其余全部进 ungrouped 桶 (`dsh-client-ui-workspace` 的 `groupByWorkspace`:`stray` 就是「没被任何 workspace 记账」的会话)。 工作区注册表 `@deepseek-ai/dsh-workspace` 是**web 层插件**,`dsh-base` 的存储行注释写明 “workspace in web layers”,终端 profile 从来没有它;桌面端的 API session controller 在 `createSession` / fork 之后调用 `workspace.attachSession` 记账,而终端会话由 agent loop 直接创建, 所以只带着 `cwd` 落盘、从未进入任何账户。注册表自身也只在 `initialized === false` 时按 cwd 做**一次性**历史归组,本机早已初始化,之后的终端会话不可能再被自动归组。 - **做法**:新增宿主行 `my-dpsk-harness-tui/workspace-sessions`(`src/workspace-sessions.ts`, 宿主 seam 在 `src/dsh-host/workspace.ts`)。profile 里没有注册表时按需挂载 `@deepseek-ai/dsh-workspace`(可选 peer:装不上就退回到原先的未分组行为,不影响启动), 然后走标准 API 归组——用会话头里的 `cwd` 调 `resolveByPath` 找已登记的工作区,没有就 `create(cwd)` 登记该目录(等同桌面端「打开文件夹」),最后 `attachSession`。 `session/created` 覆盖 `/new`、`/resume`、切换会话等所有新会话;启动时还会把该目录下 **已在磁盘上**的历史会话按创建时间升序回填进账户(归档会话、subagent 子会话跳过), 因为 `attachSession` 是前插,顺序自然保持「最新在前」。 - **验证**:9 个单测之外,`tmp/e2e-workspace.mjs` 用本机真实安装的 dsh 插件跑两段——先让注册表在空 home 里初始化(复现 `initialized: true` 之后不再自动归组的前提),再写入两个历史会话加一个实时 会话后挂载本行,断言三者都进入同一个 workspace 账户、最新在前、后续新会话立即加入。 产物侧:`lib/workspace-sessions.js` 里 `@deepseek-ai/dsh-workspace` 保持运行时动态 import (没有进 bundle),打包后按 `my-dpsk-harness-tui/workspace-sessions` 子路径可解析。 - **已知边界**:与桌面端同时运行时,两侧各自持有 `workspace.json` 的内存副本(dsh 的 JSON 存储 假设单写进程、last-write-wins),后写的一侧会覆盖另一侧的归组结果;被覆盖的归组会在下次 终端启动的回填里自愈。归档的终端会话不会被回填(它们在桌面端「已归档」过滤里自己可见)。 - **升级**:同版本内容变化不会被启动器自动覆盖,本机需要跑一次 `dpsk check`(或等版本号变化)。 ### 旧会话迁移(2026-10-21) - **症状**:启动时提示「Skipped N unreadable legacy session(s)」,这些会话在列表里消失。 - **原因**:它们是旧构建写下的 `version: 0` 会话,而当前 `@deepseek-ai/dsh-session` 的读取校验 写死 `version === 4` 且要求 `isSeeded` 为布尔;事件流里还有 v4 目录已移除的增量事件 (`assistant/chunk` / `text-chunks` / `reasoning-chunks` / `tool-call-chunks`)。 - **做法**:新增 `dpsk sessions [list|migrate]`(`scripts/sessions-migrate.mjs`)。迁移只补 header(`version`、`isSeeded`)、删掉 v4 拒收的 `request/header.system` 与空 `tools`、 把增量事件标成 `ignorable`,并修正 `tool/result` 的 `role` / `toolCallId` / `isError` 与缺失的 message id;事件一条不丢,原文件保留为 `.legacy-<时间戳>`。 - **验证**:迁移后的每一条都过 `validateStoredEvents`(dsh 自己的读取校验),本机 142 个 旧会话全部通过;`tests/sessions-migrate.spec.ts` 用同一套校验守住这些规则。 - **第二阶段(写入方比读取方新)**:另有 9 个会话本身就是 v4,却带着本构建不认识的插件事件 (`job/registered`、`job/settled`、`working-directory/change`)。`dpsk sessions migrate` 现在也会给这类事件补 `ignorable`,原文件保留为 `.replaced-<时间戳>`;修复后 286/286 通过校验。 - **未命名会话不再被当成不可读**:宿主原先把「没有 `session/title` 快照」等同于格式不兼容, 把会话整条剔除。现在按未命名处理——标题为空,会话标签与选择器显示 `New Session`, 有标题的照常显示标题。 ### 视觉层次与间距(2026-10-21) - **两处留白(不改输入框本身)**:回答文本与其页脚(`standard · mimo-v2.6-pro · 1.2s · 42.0 tok/s`)之间加一行;提示栈在面板**外侧**再加一行空行,使整块信息区与输入框之间也有一行空气。输入框的内边距保持原样,补全弹窗覆盖这行空白、仍贴在面板上沿(`PROMPT_CHROME_ROWS` 维持 4)。 - **信息栏分层上色**:侧栏的标签用 muted、数值用强调色、状态分色(MCP `failed` 红、`active` 等 muted,TODO 已完成 dim、进行中强调,轨迹的时间/角色 muted、工具失败红);上下文第二行拆成「总量 base + 缓存占比 muted」,金额行拆成「spent muted + 金额 success」。新增 `fitSegments` 按 cell 宽度逐段裁剪,42 列下仍不换行不溢出。 - **输入框元信息有主次**:`agent` 用强调色、模型与 provider 用 muted、effort 用 base(原先 effort 是警告黄,和 `auto` 抢焦点)。 - **状态栏第一行右侧提示提亮**:cwd 保持 muted,`shift+tab agents · ctrl+p commands` 改用 base,两个信息块不再一个亮度。 ### 底部两行状态栏与侧栏信息归属(2026-10-21) - **底部固定两行**:shell 根节点在两条 route 之外统一挂载状态栏,Home / Session / 停靠侧栏 / 终端面板共用,永远两行、不再有第三条常驻底行。第一行左侧是当前工作目录(`truncateDirectory`:完整路径 → 末级目录 → `...` + 尾部若干字符,按 cell 宽度计算,CJK 不越界),右侧是 `shift+tab agents · ctrl+p commands` 提示;空间不足时先压缩目录、再丢弃提示。第二行居中显示 `T S · tok/s | tokens · cache | / (%)`,缺失的数据整段省略而不是显示 0。 - **token 与缓存口径**:总 token = `inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens`(dsh 的 input 是 uncached,cache 桶独立记账);缓存命中率 = `cacheRead / (input + cacheRead + cacheWrite)`。usage 新增 `cacheBucketsComplete` 标记,宿主累计器确认三个桶都存在时才置真,渲染端拿不到该标记时显示 `cache —`,不会把「未知」当成 0% 命中。 - **turn/step 与 tps 来源**:取最近一条 assistant 行的 `turn` / `step` / `tokPerSec`,全部来自既有 durable 事件投影;流式期间不估算速率,没有数据就整段省略。 - **运行中仍是两行**:原先提示栈里的 spinner + `esc interrupt` 行并入状态栏第一行右侧(运行时替换 Ctrl+P 提示),因此任何时刻底栏都恰好两行。 - **WeChat 与版本移入侧栏**:桥接状态、登录二维码、harness 版本从底部/Home footer 移到侧栏,WeChat 不再独占一行;Home 的 `⊙ n MCP /plugins` 与版本行移除,MCP/插件清单留在侧栏。侧栏可在 Home 打开(overlay),其内容区滚动条启用并绝对定位在预留的右侧 padding 内,长清单不会挤出标题或 route/workspace 页脚。 - **标签高亮跟手**:session strip 的 `active` 改由 renderer 自己的 `currentSession()` 派生,不再依赖宿主稍后推送的 `session-list[].active`;切换会话时首帧就换高亮,旧的 catalog 响应也不会把高亮短暂拉回上一个标签。 - **Home 与 Session 同一停靠规则**:Home 之前只能用 overlay 打开侧栏,宽终端下会和 Session 的停靠行为不一致(一个分栏、一个覆盖)。现在 Home 也按 `sidebarDocked` 在行内停靠,overlay 只在停不下时出现;落地页的 logo 居中宽度随之按内容列收窄。 - **底栏按终端宽度排版**:状态栏在 pane 行之外,横跨整个终端,但之前用「内容列宽」算内边距和居中;停靠侧栏时结果第二行向左偏半个 pane。改为按终端宽度计算,停靠/覆盖两种状态都居中,并新增 frame 回归测试。 - **Home 的水平几何改为内容列**:Home 之前用 `useTerminalDimensions()` 的终端宽度算水平位置,而它挂在收窄后的内容列里;停靠侧栏时提示面板与自动补全弹窗会整体右偏半个 pane(弹窗可能越出内容列)。现在 Home 的宽度、居中与弹窗锚点都用列宽,新增断言“停靠时 prompt 左栏位于列中心而非终端中心”。 - **侧栏滚动有行为测试**:此前只断言长清单被裁剪、标题与页脚不被挤走;新增滚轮测试,在真实 shell 帧里滚 4 个 tick,断言内容确实前进(`plugin-10` 进入视口)而标题与 route 页脚不动。 - **缓存字段的“未知”不再被当成 0**:host 的累计器原先把缺失的 cache bucket 按 0 相加,却无条件发出 `cacheBucketsComplete: true`,等于把“该适配器没上报”显示成“0% 命中”。折叠逻辑抽成纯函数 `src/host/token-totals.ts`:任一采样缺 `cacheReadTokens` / `cacheWriteTokens` 就把完整性置假且不再恢复,渲染端随即显示 `cache —`。新增 `tests/token-totals.spec.ts`(三例:正常求和、缺桶后永久降级、真实 0 仍为完整)。 - **补全弹窗贴回输入框**:`PROMPT_CHROME_ROWS` 原本含“footer 行”,而 footer 已移出提示栈,常量多算 1 导致弹窗与面板之间留了一行空白。常量改为 4,并把“运行中的 `esc interrupt` 行”按实际存在与否计入锚点(shell 与 `AutocompletePopup` 的 fallback 同步)。新增回归测试:弹窗最后一行必须紧邻面板首行。 - **Home 弹窗锚点也修正**:与 Session 同一类错误——`panelTop()` 少算提示框外层的 `paddingTop`,Home 的补全弹窗与面板之间同样空一行。补上后两者都紧贴面板,并各有一条回归测试(Session 在 `app.test.tsx`,Home 在 `layout.test.tsx`)。 - **文档与设置文案对齐**:README 的 Home 段落不再描述已删除的 `⊙ N MCP /plugins` 与版本行,改为说明固定两行状态栏和“额外信息都在侧栏”;settings 页的 `left prompt` / `right prompt` 两行标题标注「底栏固定后不再绘制」,避免用户改了没反应却不知道原因(值仍可编辑、仍写入 `dpsk-tui`,pi-tui 路径继续读取)。 - **新增实机验收清单**:`docs/ACCEPTANCE_TUI_SHELL.md` 把“必须人工确认”的项目逐条列出(两行底栏、宽度降级、侧栏滚动与停靠、标签高亮、补全框贴边、中文路径、CJK/输入法),并把每项对应的自动化测试与脚本证据标在旁边,便于按项勾验;README 的界面预览一节指向它,并说明截图可能早于本次底栏改动。 - **门禁**:host 237(236 pass / 1 skip)、renderer 255 pass;两个真实二进制驱动:`tmp/check-frame.ts` 校验 24 行网格、底栏两行、无第三条 footer、唇边与 transcript 顺序,`tmp/check-status.ts` 在 120/50/30 列下校验 usage→第二行链路(turn/step、tps、23.6k tokens、cache 40%、context、居中)与 cwd 的「先隐藏提示、再退化为末级目录、最后 `...`+尾部」降级。 ### 会话流流式、Home 标签与模型菜单(2026-10-21) - **流式回答不再每字重建**:转写行从 `createSignal` 改为按 id 键控的 Solid store + `reconcile`,`session-frame` 走 copy-on-write 合并。此前每条 delta 都替换整行对象,`` 因此卸载并重建该行及其 `` renderable;现在同一行的对象身份保持不变,正文只在原 renderable 内增长。新增 `transcriptRevision` 信号:`transcript()` 返回的 store proxy 不读取行字段,原地 delta 不会触发依赖它的吸底 effect,因此"内容变化"由一个显式 revision 表达。新增 9 个流式不变量测试(60 次 delta 单行且 revision 递增、settle 不重复、Markdown 原地增长、逐 delta 帧扫描不丢字、settle 后不留幽灵 Thought 行)。 - **Markdown streaming**:`MarkdownText` 显式接收 `streaming` 并传给 OpenTUI 原生 ``,流式期间不反复 `trim()`,settle 后才 finalize;未闭合代码块 / 列表 / 表格不再逐字跳变。 - **思考也用 Markdown**:reasoning body 从纯文本改为同一套 Markdown renderer(muted thinking ink),`**Summary**` 头部、列表、围栏代码都能正确解析;修正"空 reasoning 先 return null"导致 `Thinking` spinner 永不可达的问题;`Thought: 标题 · 0.8s` 的间隔符与上游一致。另修复该早退在流式行上的一次性求值缺陷:没有 reasoning 的回答在 settle 后会残留 `Thought: 1.8s` 空行(源构建静态用例覆盖不到,由真实二进制端到端驱动发现)。 - **assistant footer 只在终态出现**:流式行不再绘制 footer,delta 不会触发 footer 重排;`turn/end` 时若流式行仍未 settle(中断/异常),宿主 TUI 适配层补发一条 `streaming: false`,避免 spinner 卡死。 - **Home 保留会话标签**:`SessionStrip` 从 session-only 分支提升到 Home/session 共用层;已有打开的会话时 Home 顶部仍显示标签条(Home 的高度计算同步减去该行),没有 tab 时保持干净首屏。 - **`Switch model` 变成专用选择器**:`/model`、`/models`、`ctrl+x m` 与命令面板的 "switch model" 都打开按 provider 分组的模型对话框(当前项标记、搜索、取消不写入);命令面板不再把模型目录混进命令列表;F2 / Shift+F2 仍是独立的 cycle,并按 `provider + model` 定位当前项(修复同名模型跨 provider 从 index -1 起跳)。新增 i18n `rendererSwitchModel`;`/help` 的 Ctrl+P / Ctrl+X M / F2 文案与实际键位对齐。 - **门禁**:235 host(234 pass / 1 skip)、243 renderer 全绿;未修改 dsh 上游包与事件流。 ### UI 对齐第二批(2026-10-20) - **旧会话标题读取降级**:`readTitleSnapshots` 失败的会话不再逐条弹 warning(此前会叠成 `+60 more` 的 toast),改为进程内缓存、整批只报一条 info「已跳过 N 个无法读取的旧会话」,并从 `session-list` 与标签页中剔除。dsh `0.2.0-rc.2` 的 v0→v1 迁移要求 `subagent/descriptor` 版本 3,旧日志里的版本 2 属于 dsh 侧不兼容,插件只做降级。 - **toast 去重**:相同 level + 文本的 notice 不再重复入队,队列上限 20 条;重试型子系统只留一条。 - **输入框 metadata**:改为 `agent · auto · model provider · effort`;`auto` 在 Auto 权限预设下显示;agent 用 preset 展示名而非 id;窄屏(<44)继续丢弃 agent/auto/provider。 - **footer 去重**:默认 `rightPrompt` 从 `${model} · ${effort} · ${permission} · ${mode}…` 改为 `${context}${queued}${subagents}`,右侧只留上下文用量 / 队列 / 子代理,末尾提示改为 `ctrl+p commands`;路由只出现在输入框 metadata,不再重复。 - **会话标签页管理**:active chip 右侧 `×` 关闭、中键关闭、右键菜单(Close / Close others / Move left / Move right / Reopen / Rename)、`leader 1..9` 改为按**打开顺序**选标签(此前错用会话目录)、关闭后邻居选择与重开栈保持原位、`session-list` 到达时清理指向不可读会话的 tab 与重开项。 - **测试**:新增 toast 去重、tab 菜单(close others / move / reopen)用例;`app.test` 的 footer/metadata 断言更新为新口径。235 host / 230 renderer 全绿。 ### 交付收尾(本次) - **渲染器成为默认路径、基座退出**:`src/index.ts` 删除 `DSH_RENDERER` opt-in 与整套宿主终端 UI 装配;`patches/`、`verify:patch-surface`、`src/components/{transcript,dialogs,settings-screen,custom-model-form}.ts`、`src/prompt.ts` 删除;`src/index.ts` 从 4.3k 行降到 3.4k 行。 - **opencode v2 布局落地**:Home 路由(字标 + 居中 prompt + footer)、会话 tab 条、右侧栏(context / MCP / 插件)、右 pane + resize 手柄、toast、spinner、markdown、转写行节奏、prompt 左边界/阴影唇边/metadata/footer 模板、`ui/layout.ts` 断点常量与 `layout.test.tsx` 几何断言。 - **修复 `cli.json` 一次性导入永远失败**:`TuiConfigSchema` 补上导入会写的字段(`leaderTimeout` / `keybinds` / `scrollSpeed` / `scrollAcceleration` / `diffStyle` / `cursor` / `mouse` / `cliImportedAt`);此前 dsh settings 服务以 `Config field "cliImportedAt" is not volatile` 拒绝整批写入,导入每次启动都重试并弹告警。新增 `tests/config-volatile.spec.ts` 断言字段与 schema 同步。 - **修复渲染器存储锁在 Windows 上的 EPERM**:锁获取把 `EPERM` / `EACCES` / `EBUSY`(delete-pending 目录)按竞争重试,而不再直接失败;此前两个快速 tab 保存会弹 `Could not save the tab set: EPERM … tabs.json.lock`。新增 `storage.test.ts` 的并发与重试用例。 - **CI 与门禁对齐**:renderer job 加 `--conditions browser`(缺它会解析到 Solid SSR 构建,10 个对话框/选择测试失败)与 `bun run typecheck`;`renderer/bun.lock` 与 `renderer/patches/solid-js@1.9.15.patch` 入库,干净 checkout 可安装;release job 同样先跑渲染器 typecheck/test。 - **清理**:删除已退出的宿主主题引擎(`src/theme.ts` / `src/theme-data.ts` / `tests/theme.spec.ts`)与不再引用的 `src/autocomplete.ts`;新增 `src/tui-types.ts`(本地完成类型)与 `src/truecolor.ts`;移除 `@earendil-works/pi-tui` devDependency,`lib/` 不再打包该库代码;删除 `scripts/generate-omp-themes.mjs` 与 `generate:themes`。 - **文档**:README 主题数量(33 个原生 opencode 主题)、门禁表、渲染器缺口清单、release 链接、协议版本与许可表更新;`docs/contracts.md` 标题改为渲染协议 v2;`renderer/src/app.tsx` 头注释改为实际布局;新增 `tests/theme-catalog.spec.ts` 校验主题 id 与渲染器资产一致。 ### 改名与启动器 - 包改名 `dsh-omp-tui` → **`my-dpsk-harness-tui`**;启动器 bin `omdsh` → **`dpsk`**(`scripts/dpsk.js` + `scripts/dpsk` + `scripts/dpsk.cmd`)。`dpsk check` / `dpsk update` 语义与 `omdsh check` / `omdsh update` 一致。环境变量 `OMDSH_NO_BOOTSTRAP` → `DPSK_NO_BOOTSTRAP`,profile 内的同步标记 `.omdsh-source` → `.dpsk-source`,旧会话文件让位标记 `*.omdsh-legacy-` → `*.dpsk-legacy-`。 - `dsh-plugin.json` 的 `id` / `name` / `version` 与 `cordis.patch.yml` 的行 `name` 同步改名。GitHub 仓库改名是手工步骤:未改名前 `dpsk update` 取不到新仓库的 release 产物,`dpsk` 启动与 `dpsk check` 不受影响。 ### 原生主题(渲染器 ThemeProvider) - 移植 opencode v2.0.32 的主题核心与内置主题到 `renderer/src/theme/{core,native}`,`renderer/src/theme/provider.tsx` 是唯一主题上下文:内置 `opencode`、custom 主题文件(`(DSH_HOME ?? ~/.dsh)/themes`、项目祖先 `.deepseek/themes`)、由终端调色板生成的 `system`,以及 `experimental["new-designs"]` 下的 `opencode2`。 - 选择持久化在宿主 `dpsk-tui` 设置节:`themeName`(默认 `opencode`)、`themeMode`(默认 `system`,另可 `dark` / `light`)、`newDesigns`(默认 false)。宿主设置节同时发送 resolved 默认值与持久化覆盖值。 - 删除渲染器合成配色链 `renderer/src/theme/tokens.ts` 与其测试;所有组件改经 `store.color()` / `store.decrease()`,由 provider 解析的 token 树供给,未定义的 token 由组件自己的字面色兜底(不再从别的颜色合成)。 - `/theme` 与 `Ctrl+X t` 打开原生主题选择器:移动即预览,取消恢复原主题,按名字排序并显示当前项。 - 0.4.0 的 `themeDark` / `themeLight` / `themeSelected` / `themeCustom` / `themeSchema` 只保留为迁移输入,不再写入新节,也不再影响运行期主题。 ### 渲染器接线(IPC v2 收尾) - `session-snapshot` 现在消费 `availableActions`:终端与 Shell 作业入口按宿主真实能力出现,不再是永远为空的动作表。 - MCP、Plugins、Shell 作业与交互终端闭环:`/mcps` 只列真实 server 名,`/plugins` 走宿主报告,`/shell` 分页读取真实字节游标并可停止作业;`terminal-op` 的 list/create/retain/release/follow/detach/write/resize/close 全接通,宿主只关闭自己创建的终端,退出时释放全部 follow/retain。 - 附件:`attachment-read` 端到端接通(图片走原生 `` 覆盖层,文件走分页文本对话框),命令附件与 20 MiB 上限由宿主校验;用户卡渲染附件引用。 - 一次性迁移真正启动:`cli.json` 导入、`tui`→`dpsk-tui` 命名空间迁移、旧主题三元组→`themeName`/`themeMode`(未知 id 回退 `opencode` 并报告)在首个 Agent 挂载时执行一次。 - 转写改为**完整投影**:宿主不再按显示策略截断推理、工具输出或 diff;行携带 `sourceSeq`/`createdAt`/`step`/`callId`/完整 `toolArguments`/JSON-safe `content`。折叠、verbosity、`session.thinking`、TPS 与 diff 换行都在渲染期决定。 - 助理正文改用 OpenTUI 原生 ``:主题 `generateSyntax` 的 `SyntaxStyle` 由 provider 提供(渲染器 idle 后才销毁旧样式),conceal、grid 表格、围栏代码与上游同源。 - toast 改为上游语义:同一时刻一条 + 队列、`+N more`、hover 暂停、点击关闭、5s 超时;spinner 采用 80ms 节拍,`animations=false` 画 `⋯`。 - 会话 tab 由渲染器自己持有并持久化到 `(DSH_HOME ?? ~/.dsh)/state/deepseek/tui`:快照开 tab,`w` 关闭、`ctrl+shift+t` 重开、`alt+↑/↓` 切换,关闭最后一个 tab 回 Home;`session-list` 只是目录。 - 渲染器本地能力补齐:`createTuiClipboard`(host+terminal、NUL 处理、真实失败)、`openEditor`(`$VISUAL`/`$EDITOR`、suspend/resume)、attention 通知与五种内置音效(随二进制内嵌),`ctrl+c` 的 cancel 优先级(选区→草稿→对话框→退出)。 - 对话框:`initialFilter` 生效,`Alt+↑/↓` 只在声明 `sectionNavigation` 的列表切换分区;补全弹窗保持 10 行窗口并让选中行始终可见,零匹配的 mention 保留原生空态。 - 启动/退出边界:`spawn` 失败通过 `child.on('error')` 一次性报告;渲染器只有一条幂等 shutdown 路径(flush 本地状态 → dispose 服务 → destroy → exit),host 断连也会恢复终端。 - 修复 host 快照路径仍向 `transcriptEvents` 传显示策略参数导致的类型/行为漂移,`docs/contracts.md` 的 kind 清单、能力表与 payload 说明同批更新到 v2。 ### settings 命名空间迁移 - 0.5.0 重命名拥有本 bundle 设置节的两个 profile 行:`tui` → `dpsk-tui`,`session-title-llm-tui` → `dpsk-session-title`。设置节按行 id 寻址,旧行 id 下写入的值(`/settings`、`/theme`、标题模型页)在行改名后失效。 - `src/settings-migrate.ts` 在激活时同步读取 profile patch 层(`ctx.settings.documentPath`),把旧行值作为回退喂给首屏;启动后异步写入新命名空间,**跳过新命名空间已有值的字段**,因此重启后不再写入(幂等)。迁移清单以启动通知打印。 - 新增 `js-yaml` 运行时依赖用于解析 patch 层;`src/js-yaml.d.ts` 提供最小声明。 ### 许可合规 - 新增顶层 `NOTICE.md`:机器可读的上游登记表(上游、SPDX、版权行、版本、派生位置、逐文件 SHA-256)+ 完整许可文本 + 命名与背书边界。 - 补齐此前缺失的署名:**OMP(`can1357/oh-my-pi`,MIT)** —— `src/theme-data.ts` 的 98 个主题与 `src/wechat/**` 的微信桥迁移;**dsh-ecosystem-spec(`T-Auto`,MIT)** —— `vendor/dsh-ecosystem-spec/` 的 16 个资产与 `src/plugin-spec/` 随包副本;**Tencent openclaw-weixin(MIT)** —— `src/wechat/core/**` 的 iLink 协议层。 - 派生文件逐文件加 provenance 头与 `SPDX-License-Identifier`。`LICENSE` 的 vendored 章节重写并指向 `NOTICE.md`;`package.json` 的 `files` 随包分发 `LICENSE` + `NOTICE.md`。 - 新增门禁 `pnpm run verify-license`(纳入 `pnpm run check`):登记表许可文本齐全、`derived` 路径存在、`vendored` 摘要一致、`vendor/**` 全量登记、声明派生的文件带 SPDX、随包分发声明。规则限定为**头部声明派生措辞**才要求 SPDX —— 引用 MIT 依赖(`import`)不算派生。 ### 渲染协议 v1(宿主 ↔ 渲染器) - 新增 `src/ipc/`:信封 `{protocolVersion, seq, kind, payload}` + NDJSON 编解码(`encodeLine` / `decodeLine` / `splitLines`)、`IpcTransport`(seq 自增、畸形消息丢弃并报告原因)、宿主侧 `childChannel` 与渲染器侧 `processChannel` 两个通道适配。 - 17 个下行 kind(宿主 → 渲染器)与 9 个上行 kind(渲染器 → 宿主)在 `src/ipc/kinds.ts` 登记,每个 kind 配一个 payload interface 并在 `KIND_PAYLOAD_NAMES` 登记。渲染器不直接触碰 dsh 服务,一切会话/模型/权限/设置/微信桥操作都以 kind 请求宿主执行。 - 五态协商(与 plugin-spec 准入同一套词表):`compatible` / `compatible_degraded` / `waiting_authorization` / `rejected` / `unknown`。必需能力 `transcript`、`composer`、`theme-tokens`;协议版本不一致或缺必需能力 → `rejected`(渲染器退出);终端未交接 → `waiting_authorization`(不交接终端)。 - `docs/contracts.md` 新增「渲染协议 v1」章节(信封与分帧、方向与 kind 清单、五态协商、通道与终端交接、门禁与 fixtures)。 - 新增门禁 `pnpm run verify:ipc-protocol`(挂入 `pnpm run check`):kind 清单无重复、可选子集在全集内、每个 kind 的 payload interface 已声明、fixtures 每行是合法信封且方向正确、`handshake.ndjson` 恰为 `hello` → `hello-ack`;`renderer/src/protocol/kinds.ts` 存在时校验渲染器镜像。 - `pnpm run verify:boundary` 扩展:`@opentui/*` 与 `solid-js` 只允许在 `renderer/` 内 import;`renderer/**` 不得 import `@deepseek-ai/*`。 - fixtures:`tests/ipc/fixtures/{handshake,downstream,upstream}.ndjson`;`tests/ipc.spec.ts` 21 例覆盖分帧、协商五态、transport 与通道适配。 ### 渲染器子进程(Bun + OpenTUI) - 新增 `renderer/` 工作区包:`@opentui/core` / `@opentui/solid` / `@opentui/keymap` 0.6.6 + `solid-js`,Solid JSX 走 `jsx: "preserve"` + `jsxImportSource: "@opentui/solid"`,`bunfig.toml` 挂 `@opentui/solid/preload`。`renderer/build.ts` 用 `bun build --compile` 产出 7 平台单文件产物(`win32-x64`、`win32-arm64`、`linux-x64`、`linux-x64-musl`、`linux-arm64`、`darwin-x64`、`darwin-arm64`),产物名与 `resolveRendererPath` 的平台键一致。跨平台编译需要把 8 个 `@opentui/core-` 原生包显式写进 devDependencies:可选依赖默认只装当前平台,缺目标平台时 `bun build` 报 `Could not resolve`。 - `renderer/src/protocol/kinds.ts` 是 kind 清单的渲染器镜像,`verify:ipc-protocol` 校验两侧 kind 集合一致。 - `renderer/src/main.tsx` 的顺序是刻意的:**先握手、后创建 CLI renderer**。协议版本不匹配或缺必需能力的渲染器绝不碰终端;宿主回答 `compatible` / `compatible_degraded` 之后渲染器才接管 TTY。单一消息分发入口在握手之前注册,握手应答与握手期间到达的下行信封都进 store,不存在「握手完成到监听器注册之间」的丢消息窗口。 - `src/host/renderer-process.ts`:spawn(`stdio: ['inherit','inherit','ipc']`)、握手与协商、`send` / `bye` / `stop`、`TERMINAL_RESTORE` 恢复序列、崩溃报告。终端恢复三条路径:正常退出走渲染器自己的 `destroy()`(宿主不补发);崩溃或被 kill 时宿主在子进程退出后补发恢复序列并打印报告;Ctrl+C 走 `exitOnCtrlC` 的正常退出。`spawn` 可注入,测试用假子进程覆盖五态与三条恢复路径。 - `src/host/renderer-log.ts`:渲染器接管 TTY 后,dsh 的 stdout / stderr 改道追加写入 `$DSH_HOME/logs/tui-.log`,并以 `log` kind 投渲染器的 console overlay;`restore()` 把原始 write 函数装回。 - `resolveRendererPath` 查找顺序:`DSH_RENDERER_PATH` → 启动器缓存 `~/.dsh/dpsk/renderer/-/` → 仓库开发构建 `renderer/dist/`;都不存在时返回 `undefined`,宿主报告缺产物而不是 spawn 不存在的路径。 - `NOTICE.md` 新增 `opentui`(MIT,`Copyright (c) 2025 opentui`,0.6.6)与 `opencode`(MIT,`Copyright (c) 2025 opencode`,2.0.30)条目与完整许可文本,`LICENSE` 的上游清单同步。`opentui` 条目 `derived` 为空——import 一个 MIT 依赖不构成派生,但编译产物把 OpenTUI 代码打进分发包,所以许可文本必须随包分发。`opencode` 条目的 `derived` 在逐文件移植时填充,每个移植文件带 `SPDX-License-Identifier: MIT` 与 upstream commit。 - 测试:`renderer/src/app.test.tsx` 4 例(`testRender` + `captureCharFrame` 断言 shell 画出 header / session / transcript / composer / notices / status);`tests/renderer-process.spec.ts` 18 例(平台键、可执行后缀、缓存路径、`DSH_RENDERER_PATH`、开发构建、缺产物、恢复序列内容、`defaultHostView`、握手五态、上游转发、崩溃补发、正常退出不补发、`stop` 杀进程并恢复、日志改道与还原、`tui-.log` 命名)。 - 实机验收(Bun 1.4.2 / Windows x64):握手 `compatible` → 渲染器接管 TTY 画出 shell;`bye` 后 `code 0` / `abnormal false`、宿主不补发恢复序列;`stop()` 后 `abnormal true`、宿主补发恢复序列与崩溃报告。 ### token 主题引擎与 98 主题 - `renderer/src/theme/tokens.ts`:opencode v2 的 token 模型(`hue.` 九槽色阶 100–900、语义 token 树、`$hovered`/`$focused`/`$selected`/`$disabled`/`$pressed` 状态修饰键、`@dialog` 作用域)与解析器。`resolveTheme` 对不可解析的 `$ref` 与循环只记录 `problems`,不抛异常——渲染器持有 TTY 时不能被畸形主题拖死。`tokenColor(theme, path)` 按**完整 token 路径**查(`text.base`、`text.action.primary.$focused`):无状态后缀的路径解析到该 token 的 `base`,主题未定义的状态回退到同一 token 的 `base`。`themeFromPayload` 把宿主发来的已解析文档平铺成一张色表(`tokens` + `@dialog.*` + `hue..`)。 - `scripts/generate-token-themes.mjs` + `src/token-theme-data.ts`:98 个 OMP 主题表达为 token 文档。25 个角色色直接映射到 token 路径(保留主题字面值),`hue` 色阶从角色派生(red←`error`、orange←`warning`、green←`success`、cyan←`spend`、blue←`context`、purple←`model`、accent←`accent`;neutral 色阶 100–500 是前景色、600–900 按与 `text` 的亮度距离排序的背景色,800 是画布)。三处必须派生:`text`(94 主题)与 `toolTitle`(81 主题)是 `[0,0,0]` 哨兵(OMP 语义为"用终端默认前景",渲染器画背景时必须给真实色,取 `muted` 向方案极值推);`scheme: 'neutral'` 的明暗判定用 `userMessageBg` 亮度(`amethyst` 实为暗色);派生画布与前景必须保证 ≥ 3:1 对比度(`light-poimandres` 的 `text` 在纯白上只有 2.44:1,前景向极值推)。`syntax.*` 按设计缺席:tree-sitter 语法高亮排在 0.6.0。 - `src/host/theme-doc.ts`:`activeThemeId`(`selected` 钉一个主题,`dynamic` 按终端方案取配对成员)、`themeDocPayload`(已解析文档,无残留 `$ref`)、`TOKEN_THEME_IDS`。 - 测试:`renderer/src/theme/tokens.test.ts` 7 例(对 vendored `vendor/opencode/theme-v2-opencode.json` 断言解析契约:dark 色阶反向、`text.base` 在 dark 解析为 `#eeeeee`、`categorical` 是 hue 名数组不是 token、别名 `accent`/`interactive`/`neutral`、状态修饰键、不可解析 ref 记入 `problems`);`tests/theme-doc.spec.ts` 10 例(98 主题、token 覆盖完整、九槽完整、对比度达标、neutral 200/800 与 `text.base`/`background.base` 一致、scheme 判定、无 `$ref`、未知 id 返回 `undefined`)。 ### 渲染 shell、transcript、composer、命令面板 - `renderer/src/app.tsx`:布局骨架(header / transcript viewport / composer / notices / footer)与信号 store。store 承载结构化 transcript 行(`TranscriptEvent`),`session-frame` 按 `id` 就地替换或追加,流式行保持 `id` 增长;`theme-doc` / `theme-list` / `reasoning-efforts` / `session-list` 都进 store。 - `src/host/transcript-events.ts`:宿主把 session log 归一为 `TranscriptEvent`。`user/message`(非 user source 保留标签,微信桥/子代理卡片同形)、`assistant/message`、`tool/call` 按 `callId` 开一行、`tool/result` 就地填充该行的输出(截断到 `maxToolOutputLines`,默认 40 行);`system/message`、`step/*`、`subagent/descriptor` 等簿记事件不是 transcript 行,跳过。 - composer:OpenTUI 的 ``(`InputRenderable`,solid intrinsic `input`)绑定 `store.composer`,`onEnter` → `submitComposer()` → 上游 `send-message`(trim 后为空则不发),发送后清空草稿。store 的 `send` 由 `main.tsx` 注入(seq 从 `hello` 的 seq 0 续起),默认 no-op 以便测试。 - `renderer/src/keymap/keymap.ts`(移植 opencode `config/keybind.ts` + `context/keymap.tsx`,MIT,署名):leader `ctrl+x` 配 `n` new session / `l` sessions / `m` models / `t` themes / `c` compact / `x` export / `q` exit,加 `ctrl+p` 命令面板与 `ctrl+t` 循环思考强度;leader 窗口 2000ms,未知和弦关闭窗口且不吞后续按键。leader 是纯状态机而非 `@opentui/keymap` 的完整 layer registry:渲染器只有一个聚焦输入(composer)和少量和弦,状态机可用 `createTestRenderer` + `pressKey` 测试。harness 没有 git 快照层,`/undo` `/redo` 不提供,也不接假实现。 - `renderer/src/component/command-palette.tsx`(移植 opencode `component/command-palette.tsx`,MIT,署名):`ctrl+p` 打开,条目来自宿主推下来的 `theme-list` / `reasoning-efforts` / `session-list` 加 keymap 动作,按组与查询过滤,每个条目都经 store 变成宿主请求。`ctrl+x m` / `ctrl+x t` / `ctrl+x l` 打开带过滤的面板。 - 测试:`renderer/src/keymap/keymap.test.ts` 7 例(label 拼写、和弦表、和弦派发、未知和弦不派发且关闭窗口、超时关闭窗口(注入 scheduler)、dispose 取消、单键派发);`renderer/src/app.test.tsx` 14 例(握手与快照、`session-frame` 就地替换与追加、流式行保持 id、`runAction` 全部映射到上游请求、`cycle-effort` 环绕与空列表、`pickTheme` 发 `set-selection`、面板组过滤、palette 条目与过滤、帧断言、`mockInput.pressKey('x',{ctrl:true})` 经 keyInput 到达 keymap 并打开面板)。 ### 宿主集成与协议扩展 - `src/host/renderer-mount.ts`:握手门——ack 之前的 send 排队、ack 后按序 flush(渲染器在 `compatible` 后才创建 CLI renderer,早期状态会被应用到未绘制的 store);`send-message` → 宿主的 `submitText`(slash 命令、插话队列、`@session` 引用都留在宿主);`set-selection.themeId` → 回发 `theme-doc`;`terminal-scheme` → 按 dynamic 配对回发;`bye` → stop;异常退出 → 退订 + 恢复 + 报告。`spawn` / `streams` / `notice` / `onAcked` 全部可注入。 - `src/index.ts` 以 `DSH_RENDERER` 环境变量为门接入 mount:pi-tui TUI 保持默认路径,渲染器路径在 mount 时 spawn 子进程、推初始状态(theme-doc / theme-list / reasoning-efforts / session-list / session-snapshot)、订阅 `session/event` 与 `agent/assistant-stream` 推 `session-frame`,`ctx.effect` 在退出时 stop。流式行只累加 `text-delta` chunk(`AssistantStreamFrame` 的 chunk 是 `block-start`/`text-delta`/`reasoning-delta`/`block-end`)。 - 协议扩展:新增 `theme-list`(host → renderer,`ThemeListPayload`)与 `terminal-scheme`(renderer → host,`TerminalSchemePayload`),`SessionOpPayload.op` 增加 `export`。两侧 kind 表 + fixture + 渲染器 kinds 声明同步,`verify:ipc-protocol` 现为 **18 下行 / 10 上游**。渲染器在 `createCliRenderer` 后用 `renderer.getPalette()` 读终端默认背景亮度上报 scheme;不回答 OSC 11 的终端落到 `dark`,失败不致命。 - `routeStdoutToLog` 增加 `streams` 参数(默认 `process`):测试注入 fake,否则替换真实 `process.stdout.write` 会吞掉测试运行器的输出,整个 spec 文件只报 `test failed` 且无细节。 - `test:renderer` 脚本改为 `cd renderer && bun test --preload @opentui/solid/preload`:**`bun test` 不应用 `bunfig.toml` 的 preload**,没有 Solid transform 时 JSX props 是静态的(`content` 只在创建时求值一次、`For` 只渲染初始数组、`visible` 永不更新),表现是"UI 画得出来但永不更新"。`bun build --compile` 走 `@opentui/solid/bun-plugin`,transform 在构建时应用,编译产物是反应式的。 - 测试:`tests/renderer-mount.spec.ts` 12 例(缺产物不 spawn、握手后按序发送 catalog 与快照、composer submit 走宿主、渲染器选主题回发文档、未知主题报告、dynamic 按 scheme 回发、transcript delta 推 `session-frame`、`bye` 停止并退订、异常退出退订并报告、stdout 改道与 `log` 转发、stop 装回原始 write)。 - 实机验收(Bun 1.4.2 / Windows x64,真实编译产物):握手 `compatible` → 渲染器上报 `terminal-scheme: dark` → 进入 alt-screen(`[?1049h`)→ 以 `theme-doc` 的颜色绘制(`#89b4fa` border、`#c5c8d2` text、`#fab387` accent、`#7f849c` muted、`#a6e3a1` success 全部出现)→ `bye` 后 `code 0` / `abnormal false` 且 stdout 含恢复序列 `[?1049l`。 ### composer:历史、插话、补全与 `@` 路径 - composer 历史:store 的 `history` / `historyIndex` / `savedDraft`。无对话框时 `↑`/`↓`(`dialog-prev`/`dialog-next` 的回退)走历史;回到列表底部还原进入历史前的草稿。历史行走查不发上游请求。 - 插话队列语义与 0.4.0 一致:`turn-state.running` 时 Enter 发 `interject: true`(文本在下一步边界生效,不是新开一轮)。`Alt+Up`(`meta+up`)在无对话框时发 `interject-edit`,宿主弹出最早仍未生效的插话——先在 agent inbox 取消排队,再回 `composer-draft` 把文本放回 composer。队列为空、消息已生效、或 inbox 拒绝移除时宿主不发回答。宿主侧 `Alt+Up` 的实现抽成 `takeOldestInterjection()`,pi-tui 路径与渲染器共用一份语义。 - 斜杠命令补全:`renderer/src/ui/completions.ts`(自写,不声明派生)。`commandSuggestions` 只在 `/` 前缀且无空格时过滤;带 `argumentHint` 的命令接受时带尾随空格;`Tab` → `composer-accept`。补全列表画在 composer 下方。命令表由宿主 `command-catalog` 推下来,渲染器只补文本,命令由宿主执行。 - `@` 路径补全:`renderer/src/ui/paths.ts`(自写,不声明派生)。`trailingMention` 取草稿末尾的 `@…` token;`mentionSuggestions` 用 Bun 的 `node:fs`(`readdirSync(withFileTypes)`)按工作目录补全,目录带尾随 `/`,结果上限 12 条。宿主把会话 cwd 随 `session-snapshot.workspace` 送下来,宿主不发文件清单。`@[label](dsh-session:…)` 会话引用语法由宿主的解析器产生,渲染器只补路径。 - 斜杠命令本地拦截:`/settings` `/theme` `/model` `/sessions` `/help` `/doctor` `/plugins` 在渲染器打开对应面板,不发上游;带参数的行(`/theme alabaster`)照旧送宿主执行。 - 会话轨画在 header 下方(`●`/`○`)。`session-list` 由宿主从 `sessionQuery.listSessions()` + `readTitleSnapshots()` 推下来(项目内最近 20 条,标题取不到时回退 id)。`ctrl+x l` 面板选择 → `session-op {op:'switch', target}`。 - 模型流程:宿主在 `onAcked` 里异步推 `model-catalog`(逐 provider `listModels`,读不到的 provider 跳过);`ctrl+x m` 面板选择 → `set-selection {provider, model}` → 宿主 `handles.saveSelection`。`{effort}` → `handles.setReasoningEffort`(branded `ReasoningEffortId` 包装);`{permission}` → `/permission` + `registryPermissionName`;`{preset}` → `/mode`(复用宿主的会话空白规则与预设挂载)。空 patch 不调用宿主。 - 会话生命周期:`session-op` 的 `new` / `switch` / `resume` 都走宿主的斜杠命令路径(未保存改动确认、标题刷新、微信桥跟随会话只有一处实现)。`rename` / `export` 打印「渲染器暂不支持」告警(新增 i18n 双语键 `noticeSessionOpUnavailable`)。 - **Enter 提交修复**:OpenTUI 的 solid 映射把 `InputRenderableEvents.ENTER` 绑在 **`onSubmit`** 属性上,`onEnter` 不在映射表里 —— composer 的 Enter 在编译产物里从不提交。store 测试绕过 input,缺陷只在真实产物检查里暴露。新增走 mock 键盘的测试(`typeText` + `pressEnter` → `send-message`)钉住。 - **纵向布局修复**:transcript 盒固定 `height={12}` 加二维码块 3 行,在 24 行终端下总高超过内容区,composer 行与二维码第三行重叠绘制(网格实测 `▀▄█▀▄ser:`)。transcript 改 `flexGrow={1} minHeight={1}`,其余行 `width="100%"`,行不再重叠。 ### 主题迁移、settings 页与 cli.json 导入 - `src/theme-migrate.ts`:`ROLE_TO_TOKEN` 是生成器四张映射表的逆向,24 个角色落到各自的主 token 路径;`ROLE_ALIASES` 列出「一个角色喂多个 token」时未被覆盖的槽位;`statusLineTail` 在 0.5.0 无槽位,进 `ROLE_WITHOUT_TOKEN` 并报告丢弃。`migrateThemeCustom` 幂等:含点的键(已是 token 路径)原样保留,迁移跑两次不会重写。 - `theme.custom` 的键在 0.5.0 是 token 路径。`TuiSettings.themeSchema`(`1` = 角色名 / `2` = token 路径)由迁移写入,不手工改。`MigrationReport` 增 `themeRoles` / `themeDropped` / `themeAliases`,启动通知打印三段(角色→路径、丢弃、未覆盖槽位)。 - `themeDocPayload(themeId, custom?)` 合并覆盖:`[r,g,b]` → `#rrggbb`(截断 + 钳位 0..255);`@dialog.` 前缀落 dialog 作用域,其余落 tokens;**主题不携带的路径一律忽略**,迁移来的旧键不会凭空造出渲染器不会问的 token。`RendererMountDeps.themeCustom`,ack / `set-selection` / `terminal-scheme` 三条发文档的路径都带上覆盖。 - `/settings` 页在渲染器重写:宿主 `settingsSection`(`settingsValues` 投影 + schemaVersion 2)随 ack 发 `settings`;`update-settings {patch}` 走宿主 `ctx.settings.update`(空 patch 不调用)。渲染器 store 的 `settings` signal + `setSetting`;`openSettings` 列四字段(theme mode / theme / show reasoning / max tool output lines),每字段开自己类型的选择器,prompt 字段校验数字后写回。`ctrl+x s` 与 `/settings` 都进这一页。 - `src/cli-config.ts`:`cli.json` 一次性导入。`OPENCODE_TUI_CONFIG` **权威**——设了但文件缺失时返回 `undefined`,不静默读另一份配置。表驱动 `CLI_FIELDS` 映射 theme / keybinds / leader_timeout / scroll_speed / scroll_acceleration / diff_style / cursor / mouse,类型不符记 `ignored`。`filterCliImportPatch` 保留已有值;`formatCliImportReport` 一个键写多个字段时只报真正移动的。启动时命名空间迁移之后导入,`cliImportedAt` 记录已导入,重启写零。`TuiSettings` 新增 `leaderTimeout` / `keybinds` / `scrollSpeed` / `scrollAcceleration` / `diffStyle` / `cursor` / `mouse` / `cliImportedAt`;渲染器 keymap 的 leader 窗口读导入的 `leaderTimeout`。 ### 微信桥 UI、doctor/plugins 与 i18n - 协议扩到 **22 下行 / 11 上行**:新增下行 `wechat-output`(`log` / `qr` / `error` / `status`,加入可选下行子集);`wechat-op` 的 `op` 从四值枚举放宽为 `/wechat-` 之后的命令名(login / status / pair / allow / deny / list / logout / notify / config),宿主拼整行过自己的命令路径,参数解析与未知命令拒绝都留在宿主。 - 宿主把桥的订阅抽成 `subscribeWechat(emit)`,pi-tui 路径与渲染器共用。桥在订阅前缓冲、订阅时按序回放,启动期产生的二维码不会在「桥已出码、渲染器尚未存在」的窗口里丢掉;订阅排在快照之后,扫码块不会先于 transcript 到达。渲染器 `wechatQr` / `wechatStatus` 两个 signal:`qr` 画成**无边框等宽文本**(0.4.0 规则:卡片边框干扰手机扫码),`status` 是 footer 一行,`log` / `error` 进通知块(error 前缀 `!`)。 - `/doctor` `/plugins`:渲染器拦截裸命令 → `doctor-check`;宿主跑自己的检查,把报告文本作为 `command-result` 按**请求的 seq** 回答。渲染器按 seq 领取,领一次即删,seq 不在挂起表的结果丢弃(同一 seq 不弹两次报告框)。宿主把 `/doctor` 与 `/plugins` 的报告行抽成 `doctorRows(check)` / `pluginRows(target)`,pi-tui 卡片与渲染器 alert 共用,不可能漂移。 - i18n 走 IPC:`src/i18n.ts` 新增 24 个渲染器键(zh/en 双语填满,`Messages` 类型强制键数一致),`messagesFor(locale)` 把整份词典交出去;宿主随 ack 发 `i18n`(`locale` + `strings`)。渲染器是独立进程,不能 import 宿主模块。渲染器 store 的 `strings` / `locale` signals + `label(key, fallback)`;词典没带的键落渲染器自带的英文标签,首帧永不空白。Shell 各行标签、settings 页四字段标题、theme mode 两项、show/hide、`/help` 标题与正文、面板标题、面板 `current` footer 全部走 `label`。 - **CJK 已知限制**:OpenTUI 把非 ASCII grapheme 存进原生 grapheme 池,单元格存引用 + 延续格;管道 stdout 下渲染器拿不到终端能力(OSC 66 查询无响应),CJK 标签在流里序列化为空格(网格实测 `会话: ` → 空格 + `:`)。`createTestRenderer` 的网格里有 CJK 字符(测试断言通过),真实终端(有显式宽度能力)下的 CJK 渲染属实机验收。渲染器标签机制可用,`en` locale 全通。 - 真实产物断言改用**屏幕网格重建**:渲染器用绝对光标定位 + SGR 上色,差分渲染会跳过与上一帧同格的字符,原始流里 `@tmp/check-frame.ts` 到达为 `@` + 光标跳转 + `mp/check-frame.ts`;被后续覆盖的内容(doctor alert 被 palette 盖住)在最终流里丢失。`createScreen(width,height)` 按 chunk 增量重建网格,在特性在屏时取快照(slash 1700ms、mention 2600ms、doctor alert 3800ms、生成中指示 3900ms、面板 4300ms、provider form 5300ms),最终网格断言常驻元素。分帧正则必须匹配**完整** CSI/OSC 序列(只匹配 `[...]` 会把 ESC 留在文本 token 里,网格出现字面控制字符)。 ### 供应商表单走 IPC - 协议扩到 **23 下行 / 12 上行**:新增下行 `form-spec`(`form` + `title` + `fields`)、上行 `form-submit`(`form` + `values`),都加入可选子集。宿主 `dialog-request {kind}` 时若有该 form 的字段清单就把 `form-spec` 发下去让渲染器画,没有则回落到 `dialogRequest`(宿主自己跑表单,按**请求的 seq** 用 `dialog-response` 回答)。`form-submit` 过宿主的 settings seam;写入失败作为 error 通知报告。 - 渲染器新增自写的多字段表单 `renderer/src/ui/dialog-fields.tsx`:`up`/`down` 在字段间移动、`left`/`right` 循环 select 字段的值、Enter 提交(值以 JSON 字符串走对话框的 resolve 通道,因为 resolve 通道只承载 `string | undefined`)、Escape 取消不发送。`DialogSpec` 增 `kind: 'fields'` 与 `fields`。 - 字段类型只有 `text` / `secret` / `select`,**没有多行字段**:OpenTUI 的 `textarea` 在 solid reconciler 里不映射 `value` 与 `onInput`(只有 `initialValue` / `onSubmit` 是访问器,`onInput`/`onChange` 的 case 只匹配 `InputRenderable`)。模型清单因此是单行、空格分隔,宿主按空白切分。 - 宿主读侧抽成 `providerDraftFor(providerId, templateId?, templateName?)`(pi-tui `ProviderForm` 与渲染器 `form-spec` 共用,两条路径不可能读出不同的 profile);写侧 `saveProvider` 不变(id 规则、凭据引用、settings mutations 都在宿主)。`providerDraftFor` / `saveProvider` / `providerSettingsTarget` / `providerDiscoveryNamespace` / `credentialReference` / `ProviderCatalogEntry` / `providerCatalog` / `modelCatalog` 从 `runCommand`(命令处理器)作用域移到**插件作用域**:渲染器 mount 的 deps 在命令处理器之外接线,`runCommand` 里的 `const` 对它不可见。`providerCatalog` / `modelCatalog` 的填充留在 `runCommand` 里,靠闭包写入同一份 `let`。 - i18n 新增 6 个供应商字段标签(zh/en 双语):`providerFieldId` / `providerFieldName` / `providerFieldApi` / `providerFieldBaseURL` / `providerFieldApiKey` / `providerFieldModels`。 - **`DialogPrompt` 的 Enter 修复**:`onEnter` 不在 solid 映射表里(`InputRenderableEvents.ENTER` 绑在 `onSubmit` 属性上),prompt 对话框的 Enter 在编译产物里不提交。它此前看起来能用,是因为 keymap 的 `dialog-submit` 路由到对话框控制器自己提交;store 与 dialog 测试都走那条路径,缺陷被掩盖。改为 `onSubmit`,并新增不挂 keymap 的测试钉住 input 自身路径(实测 `onEnter` 下该测试失败、`onSubmit` 下通过)。**随后发现这条修复是错的,已回退**:给 prompt 的 input 绑 `onSubmit` 会制造第二条提交路径,而**从按键处理器里打开的对话框会收到同一个按键**——新 input 带 `focused={true}` 挂载、按键事件继续传播到它,prompt 一开就提交空值(实测栈:`KeyHandler.emitWithPriority` → `Input.submit()` → `emit ENTER` → `onSubmit`)。prompt 的 Enter 只走 keymap 的 `dialog-submit`(路由到对话框控制器),input 上不绑 Enter。钉 input 路径的那个测试随之删除。 ### user questions 走 IPC - 协议扩到 **24 下行 / 13 上行**:新增下行 `question-request`(`request` id + `questions`)、上行 `question-answer`(`request` + `answers`),都加入可选子集。两侧 kind 表 + payload 登记 + fixture + 渲染器镜像 + `docs/contracts.md` §12.2 同步。 - 宿主的 `user-questions/request` 处理器是**阻塞**的:它要等人类答完才返回。`mountRenderer` 现在返回 `ask(questions)`——宿主调用它,渲染器逐题提问(有选项的题走选择框、无选项的题走单行输入),全部答完把 `answers` 按 mount 自己生成的 `request` id 送回,`ask` 的 promise 解析。id 不在挂起表里的答案丢弃,领过一次即删除。`AskUserQuestionAnswerItem.selected` 在 dsh 侧是可变数组,IPC 形状是 readonly,宿主映射时拷贝。 - 微信桥的 `askWithFallback` 把本地路径与 peer 的回复赛跑;两条渲染路径共用一个入口 `askLocally`(渲染器已挂载走 IPC,否则走 pi-tui 的 inline 流程)。 - 渲染器侧 `DialogSpec` 的 resolve 通道只承载 `string | undefined`,所以表单值走 JSON 字符串;提问流的答案在 store 里累积(`questionFlow` signal),答完最后一题才发送。 - **`DialogPrompt` 播种值的坑**:prompt 的初值是 `spec.value ?? spec.placeholder ?? ''`。把问题文本当 placeholder 传,问题文本就成了答案。问题文本走 `title`,placeholder 留默认提示。 - 测试:`renderer-mount.spec.ts` +1 例(`ask` 发下 `question-request`、按 `request` id 解析答案、id 不在挂起表的答案丢弃、消费过的 id 不重开);`app.test.tsx` +1 例(两题的请求逐题提问:选择框画出 `route`/`fast`,Enter 选第一项后 `sent` 仍为空、第二题的 `notes` 在屏,键入 `ok` 后 Enter 发出全量答案);`dialog.test.tsx` +1 例(空初值的 prompt 提交键入文本)。 - 真实产物断言补提问流:6450ms 快照断言 `notes` 与 `ok` 都在屏(在 prompt 提交前取),`question-answer` 载荷断言 `{a:[fast], b:[], custom:"ok"}`。 ### 自定义模型表单与技能命令目录 - `form-spec` 机制复用给自定义模型:`form` 名换成 `custom-model`,一个字段(模型 id)。宿主记住该表单是为哪个 provider 建的(`formProvider` map,`form-submit` 只带字段值,不带 provider),写入走 `SESSION_TITLE_SETTINGS_NAMESPACE`,与 pi-tui 的 prompt 同一个 seam,并刷新会话标题、报告通知。provider 回落取 `handles.selectionRef` 的当前模型选择——mount deps 里的 `selection` 是**主题**选择(mode/darkId/lightId/selectedId),没有 `provider`,用它会产生类型错误。 - 渲染器 settings 页新增两行:`provider form` 与 `custom model`,选中发 `dialog-request`(自定义模型带当前 catalog 里 `current` 模型的 provider,供应商表单不带 args)。 - **技能命令到不了渲染器**:`refreshSkillCommands` 把 `skill:` 命令推进 `commandEntries`,但渲染器的命令目录只在 ack 时发一次,而技能快照是异步的、在 ack 之后才落地——渲染器永远看不到 `skill:` 条目,技能在渲染器路径下不可达。修复:刷新后重发 `command-catalog`。 - 测试:`renderer-mount.spec.ts` +1(请求的 `args` 到达 `formSpec`,字段清单发下);`app.test.tsx` +1(settings 页画出 `provider form`,选中 `form:custom-model` 发出带 provider 的 `dialog-request`,选中 `form:provider` 发出不带 args 的请求)。 - **发现(真实缺陷,与 prompt 那条同源)**:**提交 composer 的同一个按键会打开 settings 页,keymap 的 `dialog-submit` 随即路由到新对话框的控制器,同一个 Enter 又选中一行**。settings 页的选择框 filter 是 mount 后 1ms 才 focus(`setTimeout`),所以按键不会落到它的输入框;但 keymap 的 dispatch 在按键监听里,`dialog-submit` 直接路由到刚挂载的控制器。真实产物里这条链不稳(`/settings` + Enter 后断言 settings 行落空),断言撤掉,机制由测试覆盖。**凡是「按键处理器打开的对话框」都不能让同一个按键落到它**:input 的 focus 要延迟(`setTimeout`),且不要在 input 上绑 Enter。 - 选择框的列表窗口是 `floor(终端高度/2) - 6` 行(24 行终端下 6 行)。七行的 settings 页最后一行被裁在窗口外,`scrollToSelection` 在测试网格里没有滚动——断言「某行在屏」对窗口外的行不稳,改走对话框的 resolve 通道断言处理器接线。 - 渲染器路径下的供应商表单覆盖标量字段(id / 名称 / 接口类型 / Base URL / API Key)与模型 id 清单。**嵌套的逐模型编辑器(name / efforts / effort / contextWindow / maxTokens)与上游发现探测不在渲染器**,走 pi-tui 路径可用。自定义模型表单复用同一机制(`form` 名换成 `custom-model`)。 ### status line 细节 - snapshot 里的 `provider` / `model` / `effort` / `permission` / `preset` 进 store(四个新 signal + `effortCurrent` 由 snapshot 填)。status 行画出 `status: · provider/model · effort · permission · preset`,各段用各自的 token 色(路由 `text.base`、effort `text.context.base`、权限 `text.action.primary.base`、预设 `text.muted`),空值不画。 - 测试:`app.test.tsx` +1(snapshot 填五个信号、status 行画出 `cliproxy/mimo-v2.6-pro` / `high` / `auto` / `standard`)。真实产物断言补 `status route painted`(常驻行,最终网格断言)。 - **真实缺陷修复**:宿主给渲染器的 snapshot 把 `permission` 设成 `uiMode`(agent 预设模式),与 `preset` 同一个值——status 行会把预设名显示两遍。权限的真实来源是 `currentPermissionPreset(ctx, session)`(pi-tui 路径的状态行就用它)。改为 `permission: currentPermissionPreset(...)`、`preset: uiMode`。 ### 子代理面板走 IPC - 协议扩到 **25 下行 / 13 上行**:新增下行 `subagent-list`(`agents`:`label` / `provider` / `mode`),加入可选子集。两侧 kind 表 + payload 登记 + fixture + 渲染器镜像 + `docs/contracts.md` §12.2 同步。 - 宿主把同一份 descriptor 列表推给渲染器:`subagentListeners` + `subagentList` 在插件作用域,`subagent/descriptor` 事件同时喂 pi-tui 面板与渲染器(两条路径不可能对「有哪些子代理」有分歧);订阅时先发当前列表,之后每次变化重发,`stop` 里退订。渲染器画 `↳ label · provider · mode` 行,空列表不画。 - 测试:`renderer-mount.spec.ts` +1(订阅即发当前列表);`app.test.tsx` +1(列表进 store、每行画出、清空后行消失)。真实产物断言补 `subagent row painted`(实测 true)。 - **发现**:行首标记 `↳` 与分隔符 `·` 是非 ASCII grapheme,在管道流里序列化为空格(与 CJK 同一限制);行尾在面板宽度处被裁断,所以真实产物断言只取行首的 ASCII 标签。`final` 是拼好的网格字符串,取行要先 `split('\n')` 再 `slice`(`final.slice(10,16).map` 是 `TypeError`)。 - fixture 用 PowerShell `Add-Content` 写 JSON 时,双引号转义会把字面反斜杠写进文件(`Expected property name ... at position 1`)。用单引号字符串变量写。 ### 逐模型字段走扁平行 - 供应商表单的 `models` 字段从「空格分隔的 id」改为**每模型一段 `id|name|efforts|effort|context|maxTokens`**(段间空格分隔)。`ReasoningEffortSetting` 是 `{level: wire}` 字典(`{high:'high', max:'max'}`)或 `false`,不是数组,所以 efforts 段编码为 `level=wire` 对、以 `;` 分隔,非推理模型写 `none`。宿主 `formSubmit` 解析回 `ProviderModelDraft`(`normalizeProviderModels` 接受字符串与对象两种形状)。 - **不是嵌套编辑器**:pi-tui 的逐模型编辑器(逐行选字段、逐字段编辑、上游发现探测)在渲染器没有重写;渲染器路径下逐模型字段以一行文本表达,可编辑、可写回,但没有逐行 UI。上游发现探测(`ctx.llm.discoverModels`)仍只在 pi-tui 路径。 - 测试:`renderer-mount.spec.ts` +1(models 行完整到达宿主 seam)。真实产物断言补 `models line complete in form-submit`(input 宽 40 格,40 字符的行被裁断,网格断言只取行首;完整行断言在 `form-submit` 载荷上,实测 true)。 ### prompt 模板进 settings 页 - settings 页新增两行:`left prompt` / `right prompt`(TUI settings 字段,写回走同一个 `update-settings` seam)。选中开 prompt 对话框,模板文本自由形式,答案原样写入,空答案也写(清空模板是有效编辑)。i18n 新增 `rendererLeftPrompt` / `rendererRightPrompt`(zh/en)。**这两个模板现在真正驱动渲染器 footer**:`renderer/src/ui/prompt-template.ts` 解析 `${name}` 并渲染,槽位覆盖 `location`(`repo:branch`)/`cwd`/`git/worktree`/`provider`/`model`/`effort`/`permission`/`mode`/`context`/`queued`/`subagents`/`indicator`;缺值的槽位连同它的字面量前缀一起丢弃(不会留下孤立的 ` · `),右侧按槽位整段丢弃而不是截断。默认值:`${location}` 与 `${model}${effort}${permission}${mode}${queued}${context}${subagents}`;生成中左侧槽位是本地化的 `esc interrupt`(模板显式写 `${indicator}` 时以模板为准)。 - 测试:`app.test.tsx` +2(settings 页的八行选项顺序断言——走对话框栈的 options,不靠网格;左侧模板选中后写入 `update-settings`,右侧模板空答案也写入)。真实产物断言补 `settings rows painted`(实测 true)。 - **发现**:leader 和弦的第二个键(`ctrl+x` 后的 `s`)会落到 settings 页的 filter 输入框——focused renderable 在按键监听之前处理按键,keymap 的 `handle` 是 keypress 监听器。列表被过滤成含 `s` 的行,所以断言取过滤后在屏的行,不是「所有行」。窗口外的行(`left prompt`)被 filter 移除,网格断言落空。 - 新增两行把 `provider form` 挤出 6 行窗口,网格断言随之落空——**列表窗口外的行不能靠网格断言**,改走对话框栈的 options 断言(`store.dialogStack().at(-1)?.options`)。 ### skills 面板走 IPC - 协议扩到 **26 下行 / 13 上行**:新增下行 `skills-list`(`skills`:`name` / `description`),加入可选子集;两侧 kind 表 + payload 登记 + fixture + 渲染器 `SkillSpec` 镜像 + `docs/contracts.md` §12.2 同步。 - 宿主把同一份技能快照推给渲染器:`skillListeners` + `skillList` 在插件作用域,`refreshSkillCommands` 读快照后填 `skillList` 并 `notifySkills()`;订阅时先发当前列表,之后每次刷新重发,`stop` 里退订(`unsubscribeSkills`)。渲染器命令面板新增 skills 分类:每行 `skill ` + description,选中发 `send-message` 的 `skill:` 行——与 composer 补全接受的是同一行,宿主的命令注册表拥有 `skill:` 调用。独立的 skills 页没有(技能行在面板与斜杠补全里可达)。 - 测试:`renderer-mount.spec.ts` +1(订阅即发当前列表);`app.test.tsx` +2(面板 skills 分类的 value/description 断言、`skills-list` 进 store 后选中技能发出 `send-message`)。真实产物断言补 `palette skills row painted`。 - **发现**:**同一个快照时点不能同时断言「未过滤的行」与「过滤后的行」**。面板行数多于 6 行窗口、skills 分类在最后,所以时间线重排:面板行快照在键入 filter **之前**(4300ms),skills 行快照在 `skill` 过滤**之后**(4800ms)。此前把 filter 键入排在 4200ms,把面板行断言挤掉了(`palette rows painted` 落空)。 ### 快捷键行进 settings 页 - settings 页新增两行 `key tools` / `key reasoning`(pi-tui advanced tab 的同一对字段 `keyTools` / `keyReasoning`),选中开 prompt,写回走同一个 `update-settings` seam。空答案不写:pi-tui 的键位编辑跳过空值,`matchesKey` 永远匹配不到的键不值得持久化,两条路径规则一致。 - **如实记录限制**:渲染器自己的 keymap 是固定的 leader 表(`ctrl+x` 和弦 + `ctrl+p` 面板 + `ctrl+t` 循环思考强度),**不读这两个键**;从 `cli.json` 导入的 `keybinds` 表在渲染器也不生效。写回的值对 pi-tui 路径有效(`matchesKey(data, keyTools)`)。这一批是 settings 页表面的对齐 + 写回,渲染器路径的键位可配置仍是缺口。 - 另一处缺口:`maxParallelToolCalls`(pi-tui advanced tab 的一行)在 **agent-loop** 命名空间,宿主的 `updateSettings` seam 只写 `TUI_SETTINGS_NAMESPACE`,渲染器不可达。要覆盖它得让 seam 接受命名空间,属于协议改动。 - 测试:`app.test.tsx` +1(键入 `shift+tab` 写入、空答案不写);settings 页 options 断言从 8 行改 10 行。真实产物断言补 `settings keybind row painted`(`s` filter 保留两行——两个标题都含 `s`,过滤后 5 行全落在 6 行窗口内)。 ### 模型发现探测走 IPC - 协议扩到 **27 下行 / 14 上行**:新增上行 `model-discover`(`form` + `fields`)与下行 `model-discovery`(`form` + `models`),都加入可选子集;两侧 kind 表 + payload 登记 + fixture + 渲染器镜像 + `docs/contracts.md` §12.2 同步。`FormFieldSpec` 的 kind 加 `action`。 - **探测是宿主的**:`ctx.llm.discoverModels` + 发现命名空间查找 + 凭据规则都在宿主。渲染器把当前字段值送上去,宿主跑探测、把发布的 id 合并进扁平行、把合并后的行发回。扁平行 `id|name|level=wire;...|effort|context|max` 的编解码抽成 `parseModelsLine` / `encodeModelsLine`(插件作用域),表单提交与探测共用同一份编码,渲染器只原样替换值。部署没有探测服务时宿主发告警通知,不发 `model-discovery`。 - **渲染器没有按钮基元**,探测是字段清单里的一行(`kind: 'action'`,画成 `[ label ]`):Enter 在该行把表单当前值送上去,**表单保持打开**(不是提交)。`up`/`down` 在字段间移动,`left`/`right` 对 action 行无效。 - **发现(Solid 信号存函数的坑)**:把函数当信号值直接 set,Solid 会把它当 producer 调用——两参数的写入器被用前值调用、返回 `undefined`,信号里存的是 `undefined`,写入器永远「未注册」。正解是包一层 producer:`setSignal(() => writer)`。 - **发现(信号驱动的字段清单不会把值搬进已有的 input)**:更新字段清单信号后,`For` 的行没有重建(探针实测 effect 与 `For` 的 memo 在 set 之后不再运行),input 的文本仍是旧值。正解是走表单的**本地值**:对话框在 mount 注册写入器(与 controller 同样的 capture-and-restore 模式),宿主答案通过写入器写进 `state.values`——那正是键入用的同一条路径,答案与键入的编辑不可能分叉。 - 渲染器的 hello kinds 此前只列了它接收的下行 kind 与一部分上行 kind,**漏了自己发送的**(`form-submit` / `question-answer`)。已补全。 - 测试:`renderer-mount.spec.ts` +2(探测答合并行、无探测时什么都不发);`app.test.tsx` +1(action 行画出、Enter 发 `model-discover` 且表单保持打开、合并行落进 models 字段)。真实产物断言补 `discover row painted` 与 `discovery merge painted`;`provider field typed` 改断 models 字段尾部的 `4096x`(action 行是活动行时键入无处可去,时间线先 `up` 回到 models 字段再键入);`models line complete in form-submit` 改断合并行在提交载荷里。 ### agent-loop 设置行走 IPC - `maxParallelToolCalls`(pi-tui advanced tab 的一行)在 **agent-loop** 命名空间,不在 TUI 命名空间,此前宿主的 `updateSettings` seam 只写 TUI,渲染器不可达。本批让 seam 接受命名空间。 - 协议:`SettingsPayload` 加 `agentLoop?: { maxParallelToolCalls }`(agent-loop 的值随设置段发下,不与 TUI 值混在一个对象里);`UpdateSettingsPayload` 加 `ns?: 'tui' | 'agent-loop'`,默认 `tui`。kind 表不变(27 下行 / 14 上行)。 - 宿主:`settingsSection` 读 `settingsNamespace('agent-loop')` 的值并随段发下;`updateSettings(patch, ns)` 按命名空间写。mount 发 `settings` 时带上 `agentLoop`。 - 渲染器:settings 页新增一行 `max parallel tool calls: `(prompt,数字校验),写回带 `ns: 'agent-loop'`;agent-loop 的值折进 settings signal(settings 页每行读同一个地方),`setSetting(key, value, ns?)` 加命名空间参数。 - 测试:`renderer-mount.spec.ts` 改 1 例(settings 段带 agentLoop、默认命名空间是 `tui`、agent-loop 写带 `ns`、空 patch 丢弃);`app.test.tsx` +1(agent-loop 行写回带命名空间)、settings 页 options 断言从 10 行改 11 行。真实产物断言补 `settings agent-loop row painted`(`s` filter 保留它,过滤后 6 行正好填满 6 行窗口)。 ### 逐模型嵌套编辑器走 IPC - 协议扩到 **28 下行 / 16 上行**:新增下行 `model-list`(`form` + `models`(`id` / `label`))、上行 `model-list-request`(`form` + `fields`)与 `model-edit`(`form` + `id` + `models`),都进可选子集;两侧 kind 表 + payload 登记 + fixture(下行 seq 27 / 上行 seq 33、34)+ 渲染器 hello kinds(补全自己发送的 kind)+ 渲染器镜像同步。 - **扁行的编解码是同一份**:`encodeEfforts` / `parseModelsLine` / `encodeModelsLine` 在插件作用域,表单提交、发现探测与逐模型编辑共用。解析、编码、合并都在宿主,渲染器只搬运字符串。 - 宿主 `modelListRequest(form, fields)` 把当前扁平行解析成模型清单(`id` + `label`=name)发下 `model-list`;`modelEdit(form, id, models)` 为选中的模型建六字段表单(`id` / `name` / `efforts` / `effort` / `context` / `max`,全 `text`),并记住「打开编辑器时的那条扁平行」与「被选中的模型 id」(`modelLines` / `modelEditIds`)。 - `formSubmit` 的 `form === 'model'` 分支把编辑合并进**记住的那条扁平行**、替换被选中的模型(提交的 `id` 可重命名,空 `id` 保留原 id;`efforts` 为 `none` 写 `false`,空写不改动;`context` / `max` 是数字),过 `normalizeProviderModels` 归一,报告通知,**返回合并后的行**。mount 把 `formSubmit` 的字符串返回值作为 `model-discovery` 发回,落进 provider 表单的 models 字段(走第十一批的写入器路径)。 - 渲染器:provider 字段清单新增 `edit-models` action 行(画成 `[ 编辑模型 ]`);Enter 在该行先记住当前 models 行(`modelLine` signal)再发 `model-list-request`,**表单保持打开**;`model-list` 开自己的选择框(标题走 `settingsModels`,行 `id (label)`),选中发 `model-edit` 带上记住的行。 - i18n 新增 6 键(zh/en 双语):`providerFieldEditModels` / `providerFieldEfforts` / `providerFieldEffort` / `providerFieldContext` / `providerFieldMax` + `noticeProviderModelsEdited`。 - 测试:`renderer-mount.spec.ts` +3(清单答解析行、选中答 `form-spec`(`form: 'model'`)、model 表单提交返回合并行发 `model-discovery`);`app.test.tsx` +2(`[ Edit models ]` 在屏、`down` + Enter 发 `model-list-request` 且表单保持打开、选择框画 `gpt (GPT)`、选中发 `model-edit` 带记住的行)。真实产物断言补 4 项:`edit models row painted`(provider 表单里的 `Edit models` 行)、`model list painted`(选择框的 `gpt (GPT)`)、`model form painted`(标题 `models · gpt` 与字段值 `GPT`)、`model edit merged back`(合并行尾 `4096x` 落回 models 字段);`models line complete in form-submit` 改断**逐模型编辑合并后的行**(`gpt|GPT|none||128000|4096` 在 provider 表单的提交载荷里)。时间线:6400 form-spec(models + edit-models)、6600 down、6800 Enter(list request)、7100 快照断 `gpt (GPT)`、7200 down、7400 Enter(model-edit)、7700 快照断 `models · gpt` + `GPT`、7800 down down、8000 键入 `x`、8200 Enter(提交)、8500 快照断合并行尾 `4096x`。 - **真实缺陷修复(渲染器关闭自己的 IPC 通道)**:见下节。本批的嵌套流(provider 表单 → 模型清单 → 逐模型表单)正是触发它的场景。 ### 渲染器 IPC 通道健壮性 - **根因**:Bun 在子进程**第一次写 fd 2(stderr)**时关闭它的 IPC 通道——`stdio: 'inherit'` 与 `'pipe'` 皆然,编译产物与 `bun` 跑脚本皆然。Node 的警告(`MaxListenersExceededWarning`、`process.emitWarning` 的报告)走 `process.stderr.write`。渲染器里注册了**两个** IPC 分发器,每个下行信封被应用两遍:每个对话框开两遍,每个对话框的 `useTerminalDimensions` resize 监听也翻倍,跨过默认上限 10 → 警告打印 → 通道关闭。宿主之后的 `child.send` 抛 `ERR_IPC_CHANNEL_CLOSED`;渲染器进程**活着**——继续画、继续吃按键,`child.on('exit')` 不触发,`child.on('disconnect')` 才是信号。 - 渲染器:`main.tsx` 只注册**一个**分发器(握手前注册,覆盖整个生命周期,握手窗口不留空);`process.stderr.write` 改道进 store 的通知块(警告画在屏幕上,通道不断,store 未建时缓冲);`process.setMaxListeners(0)` 与 `renderer.setMaxListeners(0)`(嵌套流的 resize 订阅本就超过 10);渲染器代码不得用 `console.*`(OpenTUI 把 console 方法改写到自己的覆盖层,`console.error` 绕过改道),失败报告一律走 stdout。 - 宿主:`childChannel.send` 包 try/catch——第一次失败标记通道已关闭,之后的发送直接丢弃,关闭原因只报告一次;`spawnRenderer` 听 `child.on('disconnect')`,200ms 后补 kill 与终端恢复,`exit` 先到时跳过(正常退出先 disconnect 后 exit)。 - 测试:`tests/ipc.spec.ts` +2(通道关闭后 `send` 不抛、后续发送丢弃、原因只报一次;未关闭时正常发送);`tests/renderer-process.spec.ts` +3(disconnect 触发 kill 与恢复、exit 先到时不重复、`bye` 容错);`renderer/src/app.test.tsx` +1(真实按键链:`testRender` + `pressArrow('down')` + `pressEnter()` 选中 model-list 的行 → `model-edit` 发出、宿主答 `form-spec` 后表单开**一次**、通知块只画一条 paint 记录)。`renderer/src/tmp-crash.test.tsx`(store 层复现,通过,不是缺陷复现)删除,断言搬进 `app.test.tsx`。 ### 安装产物里的两处启动崩溃 - **宿主看不见启动器缓存的产物名**:启动器把产物写成 `~/.dsh/dpsk/renderer/-/dpsk-renderer--[.exe]`,宿主的缓存查找却在找 `dpsk-renderer.exe` —— 已下载的产物对宿主不可见,`DSH_RENDERER=1` 下报「产物缺失」并回退 pi-tui。新增 `rendererArtifactName()`,缓存查找与 dev 查找共用;`rendererCacheDir` / `resolveRendererPath` 加可选 `home` 参数(测试注入)。`tests/renderer-process.spec.ts` +2(产物名与启动器一致、按启动器名找到缓存产物)。 - **TDZ:渲染器 hello 在激活体的 `await` 挂起点到达**:渲染器挂载后激活体 `return`,`hello` 到达时它悬在后面的 `await` 上,挂载的 `onAck` 要建快照、发目录,于是后声明的 `const` 在 temporal dead zone 里被读——第一次是 `selectionFor`(`Cannot access 'selectionFor' before initialization`),修完第二次是 `commandEntries`(同一形状)。模型选择、思考强度、自定义模型 provider 的写回句柄(`handles.saveSelection` / `setReasoningEffort` / `selectionRef`)原本在 pi-tui 路径末尾赋值,渲染器路径 `return` 前它们是 `undefined`——模型选择点了等于没点。修法:渲染器路径要读的声明与赋值全部排在渲染器块之前,块内补 `void refreshSkillCommands()`(技能快照是异步的,目录要在落地后重发)。门禁全绿掩盖缺陷(mount 测试直接构造 mount,不走真实激活体),检测办法是实机 smoke 看有没有新的 fatal 日志文件。 ### 系统安装:`dpsk` 命令缺失与旧包遮蔽新包 - **`dpsk --renderer`(新)**:新版 OpenTUI 界面仍是 opt-in,默认仍走 pi-tui。此前只能先设 `DSH_RENDERER=1` 再启动;现在启动器自带开关:`dpsk --renderer` 等价于 `DSH_RENDERER=1`, `dpsk --no-renderer` 强制回 pi-tui(即使环境变量已设)。开关由启动器在透传给 dsh 之前摘掉, 不会变成 dsh 的未知选项;`--renderer=true` 这类带值写法故意不认,仍由 dsh 报错。 - **`dpsk` 命令在系统里根本不存在**:全局那份是 0.4.0 时代 `npm link` 留下的**目录链接**(`%APPDATA%\npm\node_modules\dsh-omp-tui` → 开发目录)。链接目录里的 `package.json` 已经改成 `bin: { dpsk }`,npm 却不会为改名后的 bin 重新生成 shim,PATH 上只剩指向早已不存在的 `scripts/omdsh.js` 的三个旧 shim。所以此前每轮「打包安装」只做了 profile 级安装(`dsh plugin --profile tui add`),启动器一直是靠 `node scripts/dpsk.js` 手工跑的。修法:删掉链接本身(Windows 用 `cmd /c rmdir <链接路径>`,只删链接不碰目标;递归删除会顺着链接删进源码目录)、清掉残留的 `omdsh` shim,再 `npm install --global ./` 让 npm 生成 `dpsk`/`dpsk.cmd`/`dpsk.ps1`。开发-验收循环自此补上这一步。 - **旧包把新包整体遮住**:profile 的 `dsh.profile.bundles` 与 `dependencies` 里还留着 `dsh-omp-tui@0.4.0`,而两个包注册**同一个 `tui` 服务**与同一个 session-title provider——先加载的旧包赢,新包报 `2 entries did not activate`,界面退回 0.4.0。现象与「新包没装上」完全一样,且 `dpsk check` 的「全局与 profile 一致」在这种情况下仍然是真的。原来的 `migrateLegacyProfilePackage` 只把 `node_modules/dsh-omp-tui` 目录重命名让位、且只在 `dpsk update` 跑,依赖表还留着会被下一次 pnpm 安装装回来。 - 修复:新增导出的纯函数 `stripLegacyProfilePackage(home)`(摘 `dsh.profile.bundles` 一项与 `dependencies`/`devDependencies` 各一项,报告改了什么;无旧包/文件缺失/JSON 坏掉都返回「未改」且不重写文件),`migrateLegacyProfilePackage()` 改为「摘 package.json + 目录让位」并挂到**启动**与 **`check`** 两条路径(`update` 继续复用),被旧包遮蔽的 profile 任何一次运行都自愈。启动器加主模块守卫(`process.argv[1]` 与本模块路径比较),导入不派发。`tests/launcher.spec.ts` 新增 4 条(摘三处、幂等零重写、无旧包时字节不变、缺失/坏 JSON 不崩)。 - 文档:`docs/INSTALL.md` 的「从 0.4.x 升级到 0.5.0」补 `npm uninstall`→`npm install` 的启动器重装步骤(含 junction 不生成 bin shim 的排障与 `rmdir` 安全删法)、旧包摘除的后果与手工命令 `dsh plugin --profile tui rm dsh-omp-tui`,并新增「验证真的装好了」三步(`--dump-config` 里不应再出现 `dsh-omp-tui`、`dpsk check` 哈希一致、启动不打印 `entries did not activate`)。 ### 打包与产物分发 - `scripts/generate-sha256sums.mjs` 生成 `SHA256SUMS.json`:列出 `renderer/dist` 下每个渲染器产物的 SHA-256 与字节数,随包分发并作为 release 资产上传。 - `dpsk` 启动器新增 `renderer` 子命令(`path` / `install` / `check`):`install` 下载当前平台产物,**先比字节数再比 SHA-256**,两者都过才落盘并置可执行位 —— 截断或被改动的产物绝不能拿到 TTY 上;缓存命中即返回,不重复下载。缓存目录 `~/.dsh/dpsk/renderer/-/`,与 `resolveRendererPath` 同一约定。渲染器是默认路径:启动自动确保产物并把路径写入 `DSH_RENDERER_PATH`;取不到时报告并以非零码退出(pi-tui 渲染基座已删除,没有回退路径)。 - `dpsk check` 扩展:报告当前平台缓存渲染器产物的哈希,与随包 `SHA256SUMS.json` 比对(清单缺失时只报本地哈希,check 是本地操作,不联网)。 - CI 新增 `renderer` job(Bun 1.4.2):`bun install`、`bun test --preload @opentui/solid/preload`、`bun build.ts` 全 7 平台。Release job 加 `oven-sh/setup-bun`,构建 7 平台产物、生成 `SHA256SUMS.json`,与 tarball 一起作为 release 资产上传。CI 与 release 安装的 dsh 版本从 `0.1.0-rc.8` 升到 `0.2.0-rc.2`(与 `devDependencies` 一致,此前 CI 在 0.1 线上跑)。 - `renderer/.bun-cache/`(跨平台编译缓存)加入 `.gitignore`。 ### 渲染器成为默认路径与基座退出 - `src/index.ts` 的 `DSH_RENDERER` env opt-in 删除:渲染器是默认路径,终端交给渲染器子进程,宿主经 IPC 推送会话。启动器 `--renderer` / `--no-renderer` 糖与 `consumeRendererFlag` 删除(开关由启动器吃掉、转 env 的机制随 env opt-in 一起退出);启动器每次启动无条件确保产物在缓存里并设 `DSH_RENDERER_PATH`,取不到时报告并以非零码退出。 - pi-tui 装配体全删:terminal/ui 构造、header/editor/chat/卡片组件、`ui.start`、鼠标序列、`updateTitle`、`rebuildTranscript`、`setStatus`、`ui.requestRender`、内联问题流、spinner/compacting/indicator、`subscribeWechatOutput`(二维码绘画)、`offKeys`(鼠标滚轮、Esc 中断、Ctrl+C/D/Q、Alt+Up/L/M、Ctrl+P、PageUp/Down、插件快捷键)、`editor.onSubmit`、`exitArmed` 双按退出、teardown 的 `ui.stop`/`terminal.write`。`/settings` pi-tui 分支(SettingsScreen/ProviderForm/ModelListDialog/showItems/showMain/handleSelect/handleBack/pushOverlay/screen.onClose,约 890 行)删除;`/palette` `/plugins` `/doctor` `/model` 分支删除(渲染器 intercept + `doctor-check` dep + `form-spec` 覆盖)。 - 死组件文件与测试随基座删除:`src/components/transcript.ts`、`src/components/dialogs.ts`、`src/components/settings-screen.ts`、`src/components/custom-model-form.ts`、`src/prompt.ts`;`ProviderForm` / `ModelListDialog` / `ComposerDirectoryComponent` / `CommandHintComponent` / `InputBorderComponent` / `ComposerFooterComponent` 类删除(`ProviderDraft` / `ProviderModelDraft` / `normalizeProviderModels` / 扁行编解码保留);`tests/transcript.spec.ts`、`tests/overlay.spec.ts`、`tests/settings-screen.spec.ts`、`tests/custom-model-form.spec.ts`、`tests/provider-form.spec.ts`、`tests/launcher.spec.ts` 的 `consumeRendererFlag` describe、`tests/helpers.spec.ts` 的 `fillOverlayLines`/`parseTuiPromptTemplate` describe 删除。 - `patches/`(vendored turtle-ui patch)、`scripts/verify-patch-surface.ts`、`package.json` 的 `verify:patch-surface` script 与 `files` 的 `patches` 条目删除;`NOTICE.md` pi-tui / turtle-ui 的 `derived` 条目清空(补丁文件已删)。`@earendil-works/pi-tui` devDep 保留(`TerminalColorScheme` / `AutocompleteItem` / `SlashCommand` / `ProviderModelDraft` 类型导入仍引用)。 - 新 kind 对:下行 `extension-dialog`(`requestId` / `kind` select|confirm|input / `title` / `options` / `confirmLabel` / `cancelLabel` / `initial` / `placeholder`)+ 上行 `extension-dialog-response`(`requestId` / `value`)——插件对话框后端走渲染器对话框栈,request id 关联,渲染器未挂载时 fail-closed;上行 `shortcut`(`combo`)——插件快捷键端到端:渲染器把带修饰键的按键规范化成 `ctrl+alt+shift+base` 串送上去,宿主把注册串两侧规范化后匹配 `ctx.tuiShortcuts` 并 dispatch。两侧 kind 表 + payload 登记 + fixture + `docs/contracts.md` §11.4/§12.2 同步;协议扩到 **34 下行 / 20 上游**。 - 宿主副作用重排:`renderEvent` 改 `hostEventEffects`(tokenTotals 累计、todoListeners/lastTodos、pendingInterjections 摘除、compaction/turn 通知、addSubagent、contextUsageCache 重置;删 chat.addChild/assistantStream/toolCards/contextCards/StaticCardComponent 绘画);`rebuildTranscript` 改 `replayTranscript`(纯副作用重放,渲染器快照由 `snapshotFor`/`transcriptEvents` 提供);`resetDisplay` 改 replay + `rendererSnapshotPush`;`/skills` `/details` `/help` `/wechat-*` 结果改 toast(渲染器无静态卡片面,诚实限制);`/resume` 无参选择器改 `extensionDialog('select')`;`/theme` 无参改 toast、`applyTheme` 改 settings 写 + `rendererThemePush`(theme-doc 重推送 seam);scenes backend 改 fail-closed(`open` 返回 false,`docs/contracts.md` 记录渲染器无 scene host)。 - `subagent/descriptor` 事件数据经 `SessionEventMap` 模块声明合并类型化(`src/dsh-host/declarations.ts`,label/provider/mode),无 inline cast。 - 渲染器 `interceptCommand` 新增 `/palette` case(openPalette 全类别);`main.tsx` keypress 把带修饰键的按键规范化成 combo 走 `shortcut` 上游;`onBye` dep(`ctx.appExit?.(0)`)接线,mount case `bye` 改 stop + onBye。 - 验证:`npx tsc --noEmit` 干净;`npm run build` 成功(20 文件 991.52 kB);`npm run check` 全绿(298 pass/0 fail,verify-ipc-protocol 34 下游/20 上游 kind + fixtures + send/case 字面量,verify-license ok);`npm run test:renderer` 161 pass/0 fail(1778 expects);check-frame 实机断言全真(24 行、宽度≤80、header row0、session row1、strip row2、footer 在 wechat 行之上、lip 钉栈、gutter 在 lip 之上、transcript 在 strip 与栈之间、启动器 child exit 0、renderer exit 0、恢复序列、alt-screen、session rail、wechat 状态/QR、slash/mention 补全、doctor alert、turn indicator、palette 对话框+行+skills、provider 表单、question prompt、settings 行/keybind/agent-loop、discover 行、discovery 合并、edit models 行、model list、model 表单、shortcut 上游出现)。 ### 布局保真度与命令操作(P0/P1/P2/P3) - **补全 popup 现在有两段**:命令前缀(`/thi`)之外,参数槽也补全——`/think` 用宿主的 `reasoning-efforts`,`/permission` / `/mode` 用 `permission-presets` / `agent-presets`,`/resume` 用 `session-list`,`/theme` 用 `theme-list`(`mode dynamic` / `mode selected` / `dark` / `light` + 主题 id),`/model` 用 `model-catalog` 的 `provider/model`。接受参数只是替换尾部 token,命令与前序参数保持原样。`completionRows` 新增第 4 参(catalog),store 用 `catalogs()` / `completions()` 单点构造,popup、keymap、`tab` 三处不再各自算一遍(`renderer/src/ui/completions.ts:argumentSuggestions`)。 - **裸斜杠命令在渲染器内打开选择器**:`/think` `/permission` `/mode` `/resume` `/skills` 不再把裸命令发给宿主(宿主会走"循环/列表"语义并把结果打到 toast),而是打开带当前标记的选择器,选定后发 `set-selection` / `send-message skill:`;`/details` 按 store 已有的 route / workspace / usage 拼出详情页。带参数的形式仍原样交宿主。 - **prompt 栈与区域几何**:prompt 栈(drawer / 微信二维码 / prompt / lip / footer / 补全 popup)统一套 shell 的左右边距,转写左缘与 `┃` gutter 对齐;footer 与微信状态行不再单独加边距(此前两层 padding 叠加,footer 右缘比内缘少 2 格);prompt metadata 行改为容量裁剪(`provider · model · effort` 按宽度整段丢弃),不再被 `overflow` 切半。 - **转写 scrollbox**:滚动条改为按需显示(内容溢出才出现),并接上 `change` 事件驱动 `awayFromBottom` 的响应式信号——此前 `jump-to-latest` 只在重绘时算一次,滚轮滚上去后不会出现。 - **`leader b` 变成三态**(`auto` → `open` → `closed`):`auto` 按 opencode 的 dock 规则(`width - rail > 120`)自动停靠,`open` 一律显示(放得下则停靠,否则右侧 overlay),`closed` 强制隐藏;不再出现"宽终端上按 `leader b` 只能把已停靠的 sidebar 换成 overlay"。 - **抽屉键位**:composer 层的 `left`/`right` 让回给 textarea,抽屉页签改用 `alt+left` / `alt+right`;`↑`/`↓` 的历史只在光标位于首/末行时生效(读 textarea 的 `logicalCursor.row`),光标在草稿中间时按键留给输入框。 - **assistant 行**:正文按块渲染(prose / 标题加粗 / 围栏代码卡:左侧 gutter + 语言标签 + `text.code.base`),同步切块,避免 OpenTUI `Markdown` 的异步首帧空白;`showReasoning` 关闭时 reasoning 块不再绘制。 - **spinner 合并时钟**:所有 spinner 共用一个 40 ms 定时器,首个订阅者启动、最后一个退订停止,长会话不再为每个 pending 行各起一个 interval。 - **会话切换修复**:渲染器路径下 `/new`、`/resume`、会话轨切换原本是死路——`activateAgent` / `createAgent` / `switchAgent` 随 pi-tui 装配一起被删,`handles.newAgent`/`switchAgent` 永远 `undefined`,宿主侧 `session/event` 与 `assistant-stream` 订阅还绑在 mount 时的 agent 上。现在恢复激活路径(重装 selection、重放 tokenTotals、刷新 context/branch、`rendererSnapshotPush`),订阅改为读当前前台 agent,`createForegroundSessionImpl` 与 `tuiForegroundControl` 一并接回。 - **退出路径崩溃修复**:`ctrl+x q`(渲染器的 `bye`)在宿主侧先 `stop()` 杀掉子进程,`onExit` 把 SIGTERM 当成崩溃、`appendNotice` 再往已关闭的 IPC 通道写 → `ERR_IPC_CHANNEL_CLOSED` 未捕获异常,退出码 1。修复:`renderer-process` 用 `stopping` 标记区分「宿主主动停」与「崩溃」,主动停不再报 abnormal;`renderer-mount` 的 `send` 在 `bye` 后关闭(`closing`),随之清空待发队列,退出后的 notice 与请求直接丢弃。实机 `ctrl+x q` 现在退出码 0、stderr 0 字节。 - **安装期致命缺陷修复(渲染器成为默认路径时漏删)**:`cordis.patch.yml` 仍注册 `tui-prompt`(`my-dpsk-harness-tui/prompt`),`src/index.ts` 仍 `inject: 'tuiPrompt'`,而 `src/prompt.ts` 随 pi-tui 基座一起被删 —— 装到系统后宿主直接报 `2 entries did not activate`(`tui-prompt: failed to import` + `dpsk-tui: pending (waiting for service: tuiPrompt)`),整包不激活、界面不出现。修复:删除该 patch 行、`tuiPrompt` 注入与 `package.json` 的 `./prompt` 导出;模板解析能力改由渲染器侧 `ui/prompt-template.ts` 承担,`leftPrompt`/`rightPrompt` 移入 `resolveTuiConfig` 顶层(`TuiConfig` 字段保留,settings 页写回不变),`inputPrompt`/`inputPlaceholder` 两个无人读取的字段删除。 - **`composerBuffer` TDZ 崩溃修复**:`history-prev`/`history-next` 在 store 作用域里引用了 Shell 内声明的 `let composerBuffer`,每次按 `↑`/`↓` 都抛 `ReferenceError`(测试日志里 2 次)。buffer 声明移进 store,Shell 用 `setComposerBuffer` 注册。 - **类型修正**:`renderer/src/app.tsx` 的 `../protocol/kinds.ts` 路径(应为 `./protocol`)、`extension-dialog` 的 `options` 形状(`{id,label}` → `DialogOption`)、`span attributes` 改到 `text` 上的 `attributes`、若干 `string | undefined` 与 `list[next]` 的未检查索引。 - **i18n**:`rendererGenerating` / `rendererComposer` 删除(再无绘制点);新增 `rendererInterrupt`(生成中 footer 左槽改为 `esc interrupt`,因为 `Esc` 就是中断)、`rendererThink` / `rendererPermission` / `rendererMode` / `rendererSkills` / `rendererDetails` / `rendererPlaceholder` / `rendererJumpLatest` / drawer 三个页签与提示;`rendererKeysHelp` 更新为实际键位表。 - **验证**:`npm run check` 全绿(230 tests / 229 pass / 0 fail / 1 skip,verify-ipc-protocol 34/20,verify-license ok);`npm run test:renderer` **177 pass / 0 fail**(10 文件,新增 `ui/argument-completions.test.ts` 6 例、`ui/prompt-template.test.ts` 6 例、`app.test.tsx` 3 例);`renderer/layout-120.tsx` 120x30 / 80x24 两帧人工核对(composer 钉底、gutter 与转写左缘对齐、footer 右缘齐平);win32-x64 产物重建后 `tmp/check-frame.ts` 实机断言 **41 true / 0 false**(含 footer 在 wechat 行之上、lip 钉栈、gutter 贯通、区域顺序)。 ### Home 路由(opencode 首页还原) - 新增 `renderer/src/ui/home.tsx`:**空会话 + 空闲**时渲染器不再画空转写区,而是 opencode 的首页——顶部会话 tab 条(各会话 + `+ New session`)、居中的 `deepseek` 字标(≥80 列 `block` 6 行 / 更窄 `tiny` 2 行,<44 列隐藏)、字标下的宿主身份行、居中的 prompt 面板(左 gutter + 元信息行 `agent · provider model · effort`)、`~` 与快捷键提示行、底栏 `◦ N 插件 /plugins` 与右侧版本号。 - **首条消息切回会话路由**:`homeRoute = transcript 为空 && 未在生成`。路由切换写成 JSX 三元(首版用了组件体顶层的 `if (...) return`,Solid 只在创建时求值一次,快照晚到就永远停在首页——实机 check-frame 暴露)。 - prompt 在首页是居中 75 格(opencode 的量法),补全 popup 通过新的 `PopupAnchor { bottom, left, width }` 锚在面板上沿;会话路由仍用推出来的默认锚点。 - 字标的 `ascii_font` 是 framebuffer renderable,会画到自己的盒之外:字标与副标题包在一个显式高度(`logoHeight + 1`)的组里,否则副标题会被画进字形底部(实测复现过)。 - footer 模板拆得更彻底:`promptTemplateResolver` 只返回裸值,分隔符属于模板文本;`renderPromptTemplate` 复用段落切分,缺值槽位只丢掉纯分隔符字面量(` · `),保留真实文本(`a${x}b` → `ab`)。默认右模板改为 `${model} · ${effort} · ${permission} · ${mode}${queued} · ${context}${subagents}`。 - i18n 新增 `rendererNewSession` / `rendererHomePlaceholder` / `rendererHomeKeys` / `rendererPlugins`;`rendererEmpty` 删除(首页取代了「空会话」占位)。 - 测试:`app.test.tsx` 新增首页/会话路由切换 1 例(断言 tab 条、字标行、prompt、提示行、底栏,再断言首条消息后 header/session 行/转写回来);`prompt-template.test.ts` 增至 7 例(slot 标签、分隔符丢弃规则)。会话 chrome 类测试的 fixture 补了一行 user 消息(空会话已是首页)。 - 验证:`npm run check` 全绿(230 tests / 229 pass / 0 fail / 1 skip);`npm run test:renderer` **179 pass / 0 fail**;win32 产物重建 + `tmp/check-frame.ts` 实机断言 40 true / 0 false;装好的 `dpsk` 实机首页见下方画面。 ### 渲染器:opencode v2 样式、斜杠命令、对话行(2026-10-18) **斜杠命令一次 Enter 生效(用户报的「输入框命令无法使用」)** - 根因:补全弹窗吞掉 Enter。`acceptCompletion` 只把草稿重设为同一个 `/`,而 `setComposer` 会清掉 `completionClosed`,于是 `completions()` 仍匹配、弹窗永不关闭,`onSubmit` 又被 `popupAccepted` 拦下——`/help`、`/model`、`/theme` 必须 `esc` + Enter 才有反应。 - 修法(照上游 `component/prompt/autocomplete.tsx` 的 `slash.arguments ? insertSlash : command.run`):`CompletionRow` 增加 `name` / `takesArguments`;命令行标签不再拼尾空格;`acceptCompletion` 三分支——本地 picker(`interceptCommand` 的 14 个命令)直接开选择框、带参命令插入 `/ `、无参命令立刻 `runLine`(新抽出的提交主体,与 `submitComposer` 共用),每一处 `setComposer` 之后紧跟 `setCompletionClosed(true)`。 - `commandSuggestions` 改三段排序(精确名 > 前缀 > 子串),`/model` 不会再先给 `models`。 - 新测试 `renderer/src/ui/command-accept.test.tsx`(5 例,走真实 keymap + textarea):`/help` 一次 Enter 开 `keys` 框、第二次 Enter 关闭且不漏发;`/new` 一次 Enter 发 `send-message`;`/btw` 插入空格后 `hi` + Enter 发 `/btw hi`;`/zzz` 原样交宿主;`/model` 的 Tab 与 Enter 同效。 **渲染器测试基座修复(此前后 104 例全红)** - 根因:`renderer/bunfig.toml` 顶层的 `preload` 不作用于 `bun test`,Solid JSX 落到 `solid-js` 的 server 构建——那里的 `createEffect` 是空函数,任何响应式重绘都不发生。修法:`[test] preload = ["@opentui/solid/preload"]`。顺带删除三处 box 级 ``(`home.tsx`、`session-tabs.tsx`、`markdown.tsx`),OpenTUI reconciler 会为 false 插入空文本节点并报 `Orphan text error`。 - 结果:`bun test` 从 78/104 变为 **197 pass / 0 fail**。 **对话行(`renderer/src/ui/transcript.tsx`)** - 行结构与字形照 `routes/session/index.tsx` / `message-parts.tsx`:user 卡(`┃` + `background.raised.base` + `paddingTop/Bottom=1`、`paddingLeft=2`);assistant 文本 3 格缩进;工具分 inline(`→ Read `、`✱ Glob "…" in `、`% WebFetch`、`◈ Web Search`、`← Write`、`│ Subagent — `)与 block(`┃` 卡 + raised 底 + `$ ` / `← Edit `);shell 正文保留末 9 行 + `… N earlier lines`(上游 `maxLines=10`),diff 保留前 20 行;assistant footer `agent · model · 1.2s · 40.0 tok/s · interrupted`,断点 28/36 列。 - reasoning:header `- Thought: · ` / 收起 `+ Thought`,正文在 `┃` gutter 内用 muted 色;**默认展开**(用户明确选择,与上游 `session.thinking = "hide"` 相反),点击标题行切换(`collapsedReasoning` 集合,Solid 每次写入新引用)。 - 协议新增 `TranscriptEvent.toolInput`(`string` 标量,最多 6 个键,数字转十进制),宿主 `transcript-events.ts` 的 `tool/call` 填充;渲染器靠它画 `$ `、`Read `、`Glob "…"` 等标题——此前只有 `toolOutput`,工具行只能画一个通用的 `›`。 - 宿主 `src/index.ts` 的 `assistant/message` 分支改为**复用流式行 id**:流式草稿是 `s`、落盘的终态行是 `a`,原来两个都发,屏上留一条永不更新的半截回答。 - `ui/markdown.tsx` 重写为同步切块 + 内联样式(`**bold**`、`` `code` ``、`*italic*`、列表标记),代码块仍是 `┃` 卡片。**不采用** OpenTUI 的 ``:它在线程里解析、晚一帧才画,长转写的最后一屏永远是上一帧,实测 `renderOnce()` 后首帧为空。 - 新测试 `renderer/src/ui/transcript.test.tsx`(9 例):user 卡 gutter 与内缩、footer 字段、reasoning 展开/折叠、shell 卡与尾部截断、pending `read` 的 spinner、edit 的 `←` 与 diff、failed 的 `✗` + 原因、markdown 标题与代码块。 **主题与 chrome** - `theme/tokens.ts` 新增 `shiftColor` / `decreaseColor`(上游 `theme.decrease` 的色阶平移,不是明暗混合;非色阶色原样返回)与 store 的 `decrease(path, amount)`。 - prompt 面板底色改为 `decrease(background.raised.base)`(上游的「凹槽」观感,原来直接用 raised);lip 行改用新的 `EMPTY_BORDER`(`single` 的角会切断 `┃`,`SPLIT_BORDER` 会多画一条竖线),外盒 `╹` + 内盒 `▀`。 - 新增共享 `ui/prompt-metadata.tsx`:`agent · model provider · effort`,<44 列丢 agent 与 provider;home 与会话页共用一份规则(原来各写一份,home 缺分隔符)。 - home 照 `routes/home.tsx` 重排:弹性 spacer → 3 行固定 spacer → 字标 → 1 行 → prompt(`maxWidth=75`、`paddingTop=1`)→ 弹性 spacer;**删除字标下的宿主身份行**(上游那行留给更新提示);底栏改 `⊙ MCP /plugins` + 版本,断点 64/80 列、高 ≥14 行,失败时 `⊙ plugin(s) failed`。 - 会话 tab 条照 `component/session-tabs.tsx` / `session-tabs-model.ts` 重写:README 的编号格(`numberWidth = max(2, digits)`,右对齐 + 1 格内边距)+ 标题格,chip 宽 22/8/32,溢出标记 `‹n ` / ` n›`,新增格子是上游的 3 格 `" + "`(删掉 `+ New session` 文案)。 - sidebar:顶部改为会话标题(原来写死 `dsh`),底色 `raised.base`、内边距 2/2/1/1,底栏去掉卡片底色。 - `ComposerDrawer` 拆成 `ui/subagent-dock.tsx`(子代理卡,标题 + `↳ label · provider · mode`)与 `ui/queued-dock.tsx`(单行 ` queued · <首条>`);history/queued 两个 tab、`esc` 提示、`alt+←/→` 键位与 `rendererDrawerHistory` / `rendererDrawerQueued` / `rendererDrawerHints` 三个 i18n 键一并删除(历史仍由 composer 的 `↑/↓` 提供)。 - 补全弹窗锚点:会话路由改为传入实测 anchor(`PROMPT_CHROME_ROWS = 5` + 草稿行数 + 两个 dock + 微信行),修掉弹窗压住 composer 首行的问题(实测 `/b` 时弹窗末行落在面板第一行)。 - 许可:`NOTICE.md` 的 opencode `derived` 列表更新(新增 `transcript.tsx` / `prompt-metadata.tsx` / `subagent-dock.tsx` / `queued-dock.tsx`,去掉 `composer-drawer.tsx`)。 **验证**:`npm run check` 全绿(231 tests / 230 pass / 1 skip / 0 fail,typecheck、boundary、contract、plugin-spec、ipc-protocol、license 全过);`renderer && bun test` **197 pass / 0 fail**;`bun build.ts win32-x64` 重建后用真实 IPC 逐帧探针核对(120x30):user/assistant/tool/reasoning 行、编号 tab 条、prompt `╹▀▀▀` 与 metadata 行、footer `dsh:main`;`/help` 与 `/model` **一次 Enter** 分别开 `keys` 与 `models` 框;`/btw` + Enter 后补 `hi` + Enter 发出 `send-message: /btw hi`。 ### Breaking Changes - **包名与启动器改名**:`dsh-omp-tui` → `my-dpsk-harness-tui`,`omdsh` → `dpsk`。已安装的全局启动器需要重装;旧 npm 名将发布 deprecated 别名。 - **settings 命名空间改名**:`tui` → `dpsk-tui`,`session-title-llm-tui` → `dpsk-session-title`。自动迁移一次并打印报告;旧命名空间保留一个 release 周期的只读兼容。 - `cordis.patch.yml` 的两行 id 随命名空间改名。`verify:patch-surface` 门禁与 `patch-surface.snapshot.json` 随基座退出删除。 - **键位重排(渲染器路径)**:渲染器引入 leader `ctrl+x`(`n`/`l`/`m`/`t`/`c`/`s`/`x`/`q`/`b`)、命令面板 `ctrl+p`、`ctrl+t` 循环思考强度、`ctrl+tab` 切会话、`f2` 切模型、`shift+tab` 切 agent;抽屉的 `alt+←→` 页签随三栏抽屉一起删除。pi-tui 路径的键位随基座退出删除。`/undo` `/redo` 不提供:harness 没有 git 快照层。 - **渲染器是默认路径**:pi-tui 渲染基座已随 0.5.0 退出删除,没有回退路径;取不到渲染器产物时启动器报告并以非零码退出。渲染器已覆盖 transcript、composer(历史 / 插话 / 斜杠与 `@` 补全 / 真实 Enter 提交)、对话框基座与选择器、命令面板(含 skills 分类)、settings 页(四字段 + `max parallel tool calls`(agent-loop 命名空间,写回带 `ns`)+ 左右提示模板 + `key tools` / `key reasoning` 两行 + 供应商表单与自定义模型入口)、会话轨与切换、模型 / 思考强度 / 权限 / 预设写回、微信桥二维码与状态行、`/doctor` `/plugins`、token 主题与 `theme.custom` 覆盖、i18n 标签(zh/en)、供应商表单(标量字段 + 逐模型扁平行 + **发现探测**,探测在宿主跑、合并后的行发回渲染器)、自定义模型、user questions(逐题提问)、技能命令(`skill:` 抵达面板与斜杠补全)、status line 细节(provider/model · effort · permission · preset)、子代理面板、**逐模型嵌套编辑器**(provider 表单里的 `[ 编辑模型 ]` 行 → 模型清单选择框 → 逐模型六字段表单,合并进扁平行后经 `model-discovery` 落回模型字段;解析、编码、合并都在宿主)、插件对话框后端(`extension-dialog` / `extension-dialog-response`)、插件快捷键(`shortcut`)。**尚未在渲染器重写的功能**(独立 skills 页、多行字段、插件 scene 贡献、插件静态转写行、`/skills` `/details` `/help` `/wechat-*` 的静态卡片)在渲染器路径不可用或降级为 toast;`/export` `/rename` harness 里没有,渲染器的「暂不支持」是诚实的。 ## [0.4.0] - 2026-10-16 ### 适配 dsh 0.2.0-rc.2 - 目标版本从 `0.1.2-alpha.3` 迁移到 `0.2.0-rc.2`;`devDependencies` 精确钉死 `0.2.0-rc.2`(`@dsh-std/adapter-dsh` 钉 `0.1.1-rc.4`,cordis `4.0.5-alpha.2`,schemastery `3.18.5-alpha.2`),`peerDependencies` 放宽为 `>=0.1.2-alpha.2 <0.2.1`。0.1.2-alpha.3 兼容垫片全部移除,只支持 0.2 线。 - 会话读取面:dsh 0.2 移除 `Session.events` getter,新增 `src/dsh-host/session.ts` 的 `sessionEvents(session)` 单一入口(`snapshotEvents()`)。快照在两次 append 之间身份稳定,transcript 的日志增长检查仍然成立。 - 事件面:durable `assistant/chunk` 会话事件被移除,实时文本改由 `agent/assistant-stream` 帧驱动(`src/index.ts` 新增 `offStream` 监听,`src/wechat/dsh/progress.ts` 的 chat 流式回传同步改造,chunk 帧不带 turn,由 start 帧记录 turn)。`tool/result` 的调用 id 从 `ToolResultMessage.toolCallId` 读取,不再从 content 块取。 - 消息来源:dsh 0.2 移除共享的 `kind: 'plugin'` 来源,微信桥改为自己声明的 `wechat-bridge` 来源(`src/dsh-host/declarations.ts` 的 `MessageSourceMap` 合并)。 - 设置面:dsh 0.2 的 `SettingsForms` 没有 `register`/`get`,一个 profile 行的实时 Config 就是它的 settings section。`TuiSettingsSchema` 的字段并入 `TuiConfigSchema`(行 `tui`),标题行 `session-title-llm-tui` 的 Config 即其自身 section;读取统一走 `settings.describe()`,写入走 `settings.update(namespace, …)`。 - 预设名册:`@deepseek-ai/dsh-agent-presets` 在 0.2 线不存在,改用 `@deepseek-ai/dsh-agent-preset-registry`。dsh 0.2 不随发行版提供名册,本 bundle 在 `cordis.patch.yml` 自行声明 `standard`/`minimal`/`code`/`cordis` 四套预设行(含 plan-mode `section`、compaction realm、delegation realm)。`/mode` 的当前预设改读 `agentPreset` 会话投影(registry 注册),creation header 作为回退;`agent-preset/selected` 事件在 registry 的类型合并下可正常写入与恢复。 - 默认模式:设置页读写 registry Config 的 `selectedDefault`(用户选择默认),显示走 `defaultId`(`selectedDefault ?? default`)。 - 系统提示:`dsh-system-prompt` Config 的 `persona` 改名 `personaPrefix`。 - 权限预设:`PERMISSION_SETTINGS_NAMESPACE` 不再导出,改用 `settingsNamespace('permission')`;读写走 `ctx.permissionPresets.current(session)` / `.set(session, preset)`。 - 会话持久化:dsh 0.2 的 JSONL 后端在首次物理 append 与 `SessionHandle.flush` 时物化,且后端把实时会话事件直接路由到自己的 handle。会话写入闸门(`ConversationPersistenceGate`)无法阻止空白会话物化,已删除;`src/session-persistence.ts` 只保留 LLM 流 seam 上的 tool-call id 合成(`ToolCallIdSanitizer`)。空 id 在写入前被合成,避免恢复时校验拒绝整条日志。 - 启动器:`omdsh update` 不再自动安装 `@dsh-std/adapter-dsh`。它在 dsh `0.2.0-rc.2` 的终端 profile 里无法激活(见「已知限制」),装上后每次启动都会打印一行未激活警告;`src/dsh-adapter/` 的动态链接与 `./std-adapter` / `./std-profile-loader` 子路径保留,缺失时降级为 no-op。 - `/doctor check` 子命令落地:实时探测当前 profile 的协议包/adapter 版本漂移(此前 CHANGELOG 已声明但从未实现)。 ### Breaking Changes - 不再兼容 dsh `0.1.2-alpha.3`。在 0.1 线上运行本版本会因 `Session.events`、`SettingsForms.register`、`assistant/chunk`、`dsh-agent-presets` 缺失而启动失败。 - 空白会话被放弃时不再保证「零产物」:0.2 的持久化后端在 durability barrier 会写入 header,`~/.dsh/sessions///session.v4.jsonl.zstd` 会留下仅含 header 的小文件。 - 旧会话日志里空 tool-call id 的读取侧修复被移除:0.2 的格式恢复在 handle 内部校验,插件无法在读取前修补,这类旧会话无法恢复。写入侧(流 seam 合成 id)保证新会话不再产生该损坏。 ### 已知限制 - `@dsh-std/adapter-dsh@0.1.1-rc.4`(2026-10-02 发布,peer 覆盖 `0.2.0-rc.1`–`0.2.1`,并未停更)在终端 profile 中无法激活:其自带行注入 `sessionController`,该服务由 `@deepseek-ai/dsh-api-session-controller` 提供,而后者要求 `fileUploads`(依赖 `connection`)与 `workspaceRegistry`,属于 web profile 的客户端栈。本 profile 因此不安装 adapter;TUI 正常启动,社区 std 插件不自动加载。 - `dsh-base@0.2.0-rc.2` 的 `session-query-sqlite` 默认 `openAt: never`,`ctx.sessionQuery` 已挂载但全文检索返回 `SESSION_QUERY_SEARCH_DISABLED`;`/resume` 选择器走 exact reads 与 title 读取,不受影响。 ## [0.3.2] - 2026-09-08 ### 社区 std 标准适配改进 - 依赖版本策略调整:`devDependencies` 中 harness 包改为范围依赖 `^0.1.2-alpha.3` 允许同通道补丁更新,`peerDependencies` 扩展为 `>=0.1.0-rc.6 <0.1.3` 覆盖所有已知兼容版本线。 - std adapter peer 范围更新为 `>=0.1.1-rc.2 <0.2.0`,支持未来 0.1.x 系列所有兼容更新。 - std 协议包(`@dsh-std/command`, `storage`, `messages`, `presentation`)改为范围依赖 `^0.1.1-rc.1` 接受补丁更新。 - 验证版本改为数组支持多版本线:`UPSTREAM_VALIDATED_VERSIONS` 记录所有经过烟雾测试的 harness 版本,`/doctor` 显示完整已验证清单。 - 运行时合约探针:`/doctor check` 子命令实时检测当前 profile 中协议包/adapter 版本漂移,启动时仅警告不阻断。 - 移除硬编码的 `UPSTREAM_VALIDATED_VERSION` 单版本锁定:合约检查改为范围验证,仅在 MAJOR 版本不匹配时发出警告。 ### Breaking Changes 无运行时破坏性变更;peer 范围扩展为**放宽约束**。 ### 已知限制 - TUI 仍直接依赖 `@deepseek-ai/dsh-*` harness 类型(devDep),未通过 std adapter 抽象接口消费;完全解耦需架构级重构,列入 0.4.0 规划。 - adapter 版本协商协议需 dsh-ecosystem-spec 规范补充,当前为手动声明 `STD_ADAPTER_MINIMUM`。 ## [0.3.1] - 2026-09-02 - 思考等级改为读写 dsh `llm-pi-ai` 的 `reasoningEfforts` 模型能力:读取供应商模型配置中保存的档位映射(off/minimal/low/medium/high/xhigh/max → 上游取值),保存时写回字典或 `false`(非推理模型)。不再维护 TUI 自管的 modelEfforts,也不再覆盖 LLM 适配器返回的 reasoning 信息。 - 新增 `src/reasoning-maps.ts`:统一思考等级映射的解析、格式化、预设循环与默认强度选择;识别 DeepSeek/OpenAI/GPT/Claude 预设,并保留 `false` 的往返。 - 供应商模型列表改为回车展开平铺字段:模型 ID、显示名称、思考等级、默认思考强度、上下文窗口、最大输出 tokens 均可在当前页编辑。模型 ID/名称/上下文/输出 tokens 原地输入;思考等级行按 Enter 循环预设(跟随上游 → DeepSeek → OpenAI → GPT → Claude → 非推理),按 `e` 自定义;思考强度行按 Enter 循环选择。 - 自定义思考等级输入支持更宽松的格式:预设名(`deepseek`/`openai`/`gpt`/`claude`)、档位列表(`off, high, max`)、显式映射(`high: ultra, max`)或 `false`,同时兼容旧的 `off: none, high: high, max: max` 写法。 - 保存供应商时把模型能力(`contextWindow`、`maxTokens`、`reasoningEfforts`)写入 dsh 配置;保存失败时把完整错误写入会话,错误提示保留更久。 - 简化 `/settings`:开关和数字项在当前行回车即可切换或循环;分组标题不再占用光标;「模型与供应商」页集中默认模型、默认思考强度、标题模型与供应商编辑,去掉扁平模型清单和额外操作子页。 - 设置与选择类菜单改为全屏覆盖:打开下一级时隐藏上一层,关闭后还原,不再叠出多层边框;选择列表高度随终端视口调整。 - 供应商表单中接口类型按 Enter 循环官方值 `openai-completions` / `openai-responses` / `anthropic-messages`;探测上游模型直接合并进草稿,不再自动打开另一层编辑器。 - 新增 `omdsh check`:比较全局 `dsh-omp-tui` 与 tui profile 的实际内容(不只看版本号),存在差异时用全局包覆盖 profile。普通启动仍只在版本不一致或安装不健康时自动同步,同版本内容变化不覆盖。 - 修复 `omdsh check` / 同步把开发构建打进 tui profile 导致缺少 `session-title` / `session-persistence` 而无法启动的问题:打包前改为走发布构建,并在 tsdown 入口中显式包含这两个模块。 - 默认模型选择会结合适配器声明的 reasoning 能力(默认强度与可用档位)自动确定思考强度;设置页的默认思考强度按 Enter 循环。 ## [0.3.0] - 2026-09-01 ### 社区 std 适配器(dsh-ecosystem-spec v0.15) - 插件准入改为官方 Community v0.15 体系:`dsh-plugin.json` 由 `@dsh-std/manifest` 解析,`src/plugin-spec/` 提供五态协商(`compatible / compatible_degraded / waiting_authorization / rejected / unknown`,优先级 `unknown > rejected > waiting_authorization > compatible_degraded > compatible`)。 - `src/plugin-spec/registry.ts` 改为从 vendored 上游资产加载:`registry-0.15.json`、`permissions-0.1.json`、`extensions-0.15.json`(`SettingsSection` / `Scene` 私有扩展)全部打包进 lib。 - 新增 `src/plugin-spec/catalog.ts`:用 `@dsh-std/core` 的 `ProtocolCatalog` 注册 `command` / `storage` / `messages` / `presentation` 公共协议与 `tui.dsh/*` 私有定义;`parseManifest` 接入 `projectManifest` + `ManifestDefinitionCatalog.validate`。 - `verify:plugin-spec` 增加 dsh-ecosystem-spec conformance fixtures 正反例(8 个 fixture)并构建 Host Descriptor。 - 上游版本兼容由 `@dsh-std/adapter-dsh` 吸收,同时新增 harness 包完整版本校验与运行时漂移 boot notice(`src/std-contract.ts`)。 - `dsh-host` 按域拆分:`framework` / `agent` / `llm` / `session` / `product` / `declarations`,`index.ts` 仅做 facade re-export。 - 依赖归一化:`@deepseek-ai/dsh-*` 全部改为 optional peer + pinned devDep;`@dsh-std/{command,storage,messages,presentation}` 进入运行时依赖;`dependencies` 只保留 dsh-std 协议包、commander、qrcode-terminal。 ### 宿主平面(src/host) - `tuiPluginStorage` 改为 `open(pluginCtx)` 派生 namespace,`get` 每次现查 `storage.local.read`、`set/delete` 每次现查 `storage.local.write`,拒绝时抛 `PERMISSION_NOT_GRANTED`。 - `tuiMessageObserver` envelope 对齐 `@dsh-std/messages` 0.1.1-rc.1 的 `MessageEvent`,投递前过 `validateMessageEvent`;订阅时校验 manifest 中 `MessageObserver` 声明的 scope 精确匹配;activationInstance 从 admission registry 读取。 - `tuiEffectLedger` 写入前过 `effect-ledger-record.schema.json`(`additionalProperties:false`),结构性禁止夹带 payload。 - `tuiPluginHost.admit()` 成功后记录 activation 与 manifest 到 process-wide registry,供 storage / message observer / 诊断面查询。 ### 诊断与门禁 - 新增 `/plugins`:trust disclosure、Host Descriptor 摘要、self-check、admissions、grants 规则数、ledger 尾 5 条,以及 `/plugins check `。 - 新增 `/doctor`:self-check、advertised contracts、generation。 - 新增 `verify:patch-surface` 门禁 + `patch-surface.snapshot.json`(7 overrides / 15 inserts),并挂入 `pnpm run check` / `pnpm run build`。 - `verify:boundary` 保持双边界:`@deepseek-ai/*` 仅 `src/dsh-host/`,`@dsh-std/adapter-dsh` 仅 `src/dsh-adapter/`。 ### 底部信息栏重排 - 第一行(目录/状态行)左侧显示工作目录与 git 分支,右侧固定显示「当前编辑模式 · 权限预设」;空间不足时优先折叠模式,权限始终可见。 - 第二行(底部信息栏)改为左右分区:左侧对齐显示模型与思考强度(队列指示跟随左侧),右侧对齐显示上下文用量与缓存命中率;折叠顺序为队列 → 模型 → 思考强度 → 上下文前缀 → 缓存命中。 - 新增缓存命中统计:累计 `cacheReadTokens`,按 `cacheRead / (cacheRead + inputTokens)` 显示命中率,仅在存在请求活动后出现。 - 微信状态行增加图标前缀并弱化为灰色(dim),与主对话文本区分;错误/警告通知仍保持彩色。 ### 打包与启动 - 统一使用 pnpm 安装(`package-lock.json` 移除,`pnpm-lock.yaml` 生成),pi-tui patch 随 `patchedDependencies` 生效;`tsdown` 开启 `clean: true`,消除 lib 残留旧 chunk。 - `omdsh` 正常启动不自动升级、不迁移凭据/会话、不安装 std adapter,也不打印启动日志;首次尚无 tui profile 时仍静默安装当前启动器包。 - 新增 `omdsh update`:从 GitHub Release 安装最新 tarball,并在此时做新版本适配(凭据扁平化、旧会话编码、社区 std adapter)。 - 已知问题:`@dsh-std/adapter-dsh@0.1.1-rc.2` 作为 dsh bundle 安装时,其自带 `dsh-std-adapter` 行依赖 `sessionController`,当前 dsh `0.1.2-alpha.3` profile 无此服务会阻断启动;重复 id 冲突已修复,完整激活需等 adapter 更新或改为非 bundle 依赖安装。 ## [0.2.6] - 2026-08-26 - 新增微信远程模式切换命令:`@dsh chat` 使后续普通微信消息实时分段回传且不汇报进度;`@dsh work` 使后续消息完成后回传结果,并保留周期性进度汇报。 - 启动/重开会话时历史记录先置顶显示,首个实时 transcript 事件到达后再自动跟随最新,避免打开即跳底。 - 临时通知改为固定占位行:通知只在行内刷新,不再通过增删行改变 chrome 高度,避免启动和通知出现/消失时界面上下抽动。 - 微信桥普通运行日志不再进入 TUI 通知或 transcript;固定占位行改为持久显示“微信连接状态 + 接收锁归属”,只有 error/失败类日志才走临时通知。 - 修复 `llm/stream` 消毒层对 tool-call 身份的破坏:不再把缺失 `name` 的 delta 续片替换成 `unknown`,并在续片/`block-end` 缺失 `id`/`name` 时沿用同块首次出现的非空值,避免合法工具调用被误判为 unknown tool(`unknown tool ""`)。 ## [0.2.5] - 2026-08-24 - 适配新版 dsh 的 credentials-local:`omdsh` 启动前自动将旧版 `~/.dsh/.credentials.yaml`(`version: 1` + `refs:`)迁移为新版扁平凭据文档 - 适配新版 dsh 的 agent-loop 启动契约:`agent-loop` 行注入 `sessionPersistence`,避免声明式 agent 因缺少该依赖而无法激活,修复 TUI 一直报 `session ... never became live (timed out)` - 适配新版 dsh session-persistence-jsonl 的 zstd 编码检查:`omdsh` 启动前自动把同目录混存的 `session.jsonl` 旧 plain 日志重命名为 `session.jsonl.omdsh-legacy-`,保留 zstd 版本,避免 `encoding mismatch` 阻断启动 - 同步修复全局 npm 安装目录下实际生效的 `cordis.patch.yml` 与 `scripts/omdsh.js` - 微信远程 `@dsh` 命令不再注册为本地 `/dsh-*` 命令:这些命令仅由微信消息直接分发,不会出现在本地 `/help`、命令补全菜单中,也无法在本地 TUI 中调用 ## [0.2.4] - 2026-08-21 - 修复 TUI 手动 `/compact` 等命令执行器抛错时未捕获导致 TUI 崩溃:改为显示命令失败通知,并保留压缩失败原因与提示 - 修复 rc8 `commands.execute` 方法被解包调用后丢失 `this`,导致手动 `/compact` 报 `Cannot read properties of undefined (reading 'view')`:TUI 与微信命令路径改为绑定 `ctx.commands` 调用 - 修复微信远程 `@dsh model`/`@dsh think` 只写入默认模型、未更新 TUI 前台 `selectionRef`,导致 TUI 状态栏仍显示旧模型且实际请求仍走旧模型:微信命令现在优先通过 TUI 前台控制保存模型选择 - 修复 `@dsh models` 只探测当前 provider:改为遍历全部已注册/可配置 provider 并合并模型列表,数字回复可直接跨 provider 切换 - 新增 TUI 插话机制:生成中回车把消息加入插话队列(`agent.steer`),在下一步/工具调用结束后送达模型;`Alt+Up` 可编辑未生效插话并自动取消其排队状态 - 修复 Ctrl+C 只清屏无法退出:恢复双击 Ctrl+C 退出,单次仍用于中断/清屏 - 迁移 Oh My Pi 原版快捷键:Esc 中断、Ctrl+C 清屏/中断(双击退出)、Ctrl+D 空编辑器退出、Ctrl+T 思考块、Ctrl+O 工具输出展开、Ctrl+Shift+O 工具活动显隐、Alt+L 重置显示、Ctrl+P/Shift+Ctrl+P 循环模型、Alt+M 模型选择、Ctrl+Q/Ctrl+Enter follow-up;选择器补齐 PageUp/PageDown/Home/End ## [0.2.3] - 2026-08-20 - 升级 dsh 宿主依赖与开发依赖到 `0.1.0-rc.8`:适配 `commands.execute(agent, line, images, signal)` 新签名,TUI 与微信命令路径统一传入空图片批次 - 移除失效的 `@deepseek-ai/dsh-llm` pnpm patch(原 patch 只改 `lib/types/assembler.js`,实际主入口 `lib/index.js` 未被修复);改为 TUI 侧 `llm/stream` 流式消毒器,在 `tool-call-delta` / `block-end` 进入 BlockAssembler 前把空 `id`/`name` 替换为 `call-` / `unknown` - 在会话持久化读写边界修复历史异常工具调用:包装 JSONL backend 的 `appendBatch` 与 `loadStored`,对 `tool/call`、`tool/result`、`assistant/message` 中的空 `callId`/`name`/`arguments` 做规范化,使存在空工具 id 的存量会话可被恢复 - 工具卡空名称显示回退为 `Unknown`,不再出现空白标题 - 新增回归测试:空工具 id 的损坏会话可 `Session.fromRestore` 恢复;空工具名工具卡回退渲染 ## [0.2.2] - 2026-08-20 - 修复上下文压缩进度提示仅 5s 瞬时消失:`compaction/start` 置 `isCompacting` 并经 `indicator` 持久显示 `上下文压缩中…`,`compaction/end` 清除,与 `running` 旋钮互斥,且在 `rebuildTranscript` 时按事件重建 - 修复压缩后插入的上下文卡片无法展开:`ContextCardComponent` 增加 `collapsed/expanded` 状态并接入 `Ctrl+O` 全局显隐循环,`maxOutputLines` 截断时持久提示 `… +N lines (Ctrl+O to expand)` - 修复压缩后上下文用量仍显示旧长度:`compaction/end` 强制 `contextUsageCache.measuredAt=0` 触发 `tokenMeter.measure` 立即重算,`rebuildTranscript` 重建时同步 `isCompacting` 状态 - 修复超长会话恢复失败:`BlockAssembler` 空 `tool-call` 覆盖导致 `tool/result callId:""` 持久化后 `Session.fromRestore` 拒识,补丁忽略空 `id/name` 并回退 `block-end`,`repair-sessions.py` 按 `(turn,step)` 回填并重建多帧 `zstd`(`tui-695e` 86 帧、`tui-edaa` 截断至 233797 事件),`79` 会话校验 0 fail - 修复非官方接入点 `/compact` 400:`pi-ai openai-completions isDeepSeek` 扩展至 `deepseek-official` 与 `model.id`,`dsh-compaction-basic summarizeWithLlm` 增加 `configured/latest/agentTarget` 回退,`cordis.patch.yml` 保留 `deepseek-official/deepseek-v4-flash` 并提升 `AGENT_READY_TIMEOUT 10s→30s` - 将 `AGENT_READY_TIMEOUT_MS` 提升至 `30s` 并重建多帧存储以避免 `persistence.list()` 首帧 8k 未命中导致的启动超时 ## [0.2.1] - 2026-08-18 - Port all 98 concrete OMP themes from the local OMP installation into `src/theme-data.ts`; `/theme` and settings now list every dark, light, and neutral OMP theme with no preset family groups. - Rework theme selection into `dynamic` (dark/light slots chosen independently, follows the terminal scheme) and `selected` (one fixed theme); the dark slot may hold a light theme and vice versa. - Truecolor light schemes now use the light-slot OMP theme instead of falling back to ANSI colors. - `omdsh` now automatically updates the tui profile when the installed plugin version is older than the launcher version. - Cache rendered transcript rows per component and throttle token-meter/permission reads to once per second, fixing the progressive slowdown as session history grows. - Tool cards now keep the title as the bare tool name and show the actual command or query in an `Input` section above `Output` for `pwsh`, `bash`, `read`, `write`, `edit`, `grep`, `glob`, `web_search`, `web_fetch`, and `run_code`; long commands wrap instead of truncating. - `str_replace_editor` renders `old_str`/`new_str` as a diff section (red removals, green additions) instead of hiding the edit content. - Wrap session-event rendering in try/catch error notices so one malformed event cannot crash the TUI. - Add Linux CI E2E smoke test (`dsh --profile tui` boots, renders `/help`) and WeChat command/config unit tests. - Add settings UI for per-role RGB theme overrides, status prompt templates, and tool/reasoning keybindings; add a subagent descriptor panel. ## [0.2.0] - 2026-08-18 - Fix WeChat inbound routing to always follow the foreground TUI session; only send the "task in progress" receipt after the message is successfully queued. - `@dsh new` now creates a session through the TUI and brings it to the foreground instead of switching the WeChat bridge to a background session. - Replace the `dsh` wrapper with an `omdsh` launcher that uses the system `dsh` from PATH and bootstraps the tui profile on first run. - 将 dsh 宿主 peer 依赖标记为 optional,避免 npm 11 全局安装启动器时因 peer 图触发 Arborist 的 `null.children` 崩溃。 ## [0.1.3] - 2026-08-17 - Fix WeChat inbound routing after TUI session switches: `/resume` and `/new` now update the WeChat bridge's active-agent pointer, so ordinary WeChat messages are steered into the currently visible TUI session instead of an old background session. - Fix `/model` and the settings default/title model pickers to include models configured on user-added providers in the provider settings screen, instead of only querying the live LLM registry. - The settings screen now writes custom provider profiles through the official `llm-pi-ai` settings namespace and lets the dsh adapter own registration and runtime requests; it does not implement a second LLM adapter. - Decouple WeChat start/result notifications from the progress-reporting switch: turning progress reporting off now disables only periodic progress reports, while the "task in progress" start receipt and final result/error push remain enabled. - Improve provider model management: long Base URLs and model IDs show complete wrapped details, model lists support add/edit/delete with transactional Save/Cancel, and Delete/Backspace removes the highlighted model instead of implicitly deleting the last entry. - Fix the model catalog dialog on narrow terminals: help text, model IDs, selected values, errors, and footer hints now wrap within the frame instead of being clipped. ## [0.1.2] - 2026-08-16 - Migrate the OMP WeChat (iLink) bridge into dsh as a first-class bundle service: `/wechat-*` commands, `@dsh` remote commands, `wechat_send`/`wechat_status` tools, automatic progress/result push, and WeChat-aware ask bridging. - Optimize overlay dialogs: center them by default, enlarge ask/user-question dialogs to 90% width/height, and enlarge general dialogs to 80% width/85% height so content is less likely to be folded. - Add a default config section in `/settings` for startup permission mode, model, and reasoning effort; `/new` now continues the current session's permission instead of the global default. - Rework `/settings` around OMP's full-screen framed layout: tabbed label/value rows, preserved cursors, current-value preselection, nested Escape-to-back navigation, and stale async-view guards. - Expand `/settings` with more configurable items: show reasoning, tool output lines, default mode, max parallel tool calls, custom provider/model entries (with editing of saved custom IDs), and move default model/effort onto the Model tab with visible Provider/Model rows. - Add a single-page custom model form: provider, model, and reasoning effort are shown together with save-target checkboxes and Save/Cancel actions, so no more one-field-per-screen wizard. - Reorganize `/settings` into General / Models & Providers / Advanced tabs; provider editing/creation now uses the official dsh-llm-pi-ai protocol values `openai-completions`, `openai-responses`, and `anthropic-messages`, with Base URL, API key, model discovery, manual model entry, and preset templates persisted through dsh settings. - Fix default mode persistence: `/settings` 中保存的 `agent-presets.default` 现在会在新会话启动时生效,不再固定回退到 `standard`。 - Expose each skill as a `skill:` quick command in the composer, so typing `/` lists them and fuzzy search can find them by partial names (e.g. `commit` → `skill:git-commit`); completion keeps the no-space syntax. - Sanitize ANSI/C1 escape sequences and tabs from session, tool, and todo text before differential rendering to prevent colored blocks, cursor movement, and frame corruption. - Add a `微信claw` tab in `/settings` for WeChat progress reporting, progress interval, and terminal-task push configuration. - Fix WeChat send tool return shape so empty messages and omitted `to` fields are represented cleanly. ## [0.1.1] - 2026-08-15 - Add `/new` for in-process fresh-session creation. - Keep abandoned sessions unmaterialized until they contain conversation data. - Submit exact slash-command arguments, including `/mode minimal`, with one Enter press. - Add `/think [level]` with model-specific effort completion, cycling, and persistence. - Wrap the mode, path, and Git prompt in one OMP-style dark Powerline surface. - Discover and switch any locally installed agent preset via `/mode`. - Advertise the configured `/permission` modes in help, inline hints, and argument completion, with localized built-in descriptions. - Make narrow sidebars use coherent compact status segments and keep permission state visible after context usage. - Sanitize carriage returns from shell output so multiline tool cards keep their borders intact. - Present the unrestricted permission preset as `full-access`, show a startup reminder when it is active, and highlight it with the theme's emphasis color. - Harden session resume: avoid quadratic transcript rebuilds, make Git branch reads non-blocking, cap resume/title loading with timeouts, and surface resume failures. - Keep the hardware cursor visible in the editor so IMEs that preview pinyin/composition text place it inline at the cursor instead of at the line end. - Cap the compact mode segment to 4 CJK / 8 ASCII columns with an ellipsis so long preset ids like `anchored-standard` stay compact. - Rework the footer compression order: compress the model with a middle ellipsis first, then drop the `ctx` prefix, and only then remove lower-priority segments. ## [0.1.0] - 2026-08-14 - Initial public dsh profile bundle. - OMP-styled Catppuccin terminal layout with responsive welcome panel. - Chronological user, injected-context, assistant, reasoning, and tool rendering.