dsh-recall-plugin 2.3.21 → 2.3.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  本文件格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),版本号遵循语义化版本。
4
4
 
5
+ ## [2.3.23] - 2026-09-18
6
+
7
+ ### 修复
8
+
9
+ - **撤回后子会话不打开(dsh 0.1.6-alpha.2 移除 `ISessions.open`,实弹冒烟发现)**:alpha.2 起 `ISessions` 只保留列表快照与 `retain`/`using` 引用模型,契约注释写明「navigation belongs to view owners」——原调用的 `sessions.open(sessionId)` 已不存在,守卫判断静默跳过,于是撤回的 execute/fork/lineage 全部正常,唯独 fork 出的子会话不会被打开:页面停在工作区空态。导航入口迁至独立 `uiWorkspace` 服务(`dsh-client-ui-workspace`)的 `openSession(target: SessionTarget)`(`SessionTarget = SessionId | SubagentAddress`,官方 UI 同样传裸 sessionId;`ctx.workspaces` 只有归档能力、无导航)。修复=client `inject` 声明 `uiWorkspace` + `ctx.uiWorkspace` 类型化(`ClientUiWorkspaceService`),导航点改双版本分支:`uiWorkspace.openSession(id)` 优先、旧版回退 `sessions.open(id)`(撤回节点与快照管理「切换」两处)。`tests/probe/api-surface.test.js` 新增「会话导航归属」2 例钉住两侧事实,compat-audit 新增 I37。实弹复验:撤回第二条消息后子会话自动打开、对话回退、标题继承、被撤回消息文本回填输入框。
10
+
11
+ - **快照管理「切换」把归档会话放进点击路径,一点就落到工作区空态**:按钮判据只查会话是否在官方列表里(`sessions.list`),依据「已归档会话不在列表」的假设;alpha.2 起该快照包含归档会话,而归档会话不是合法的主视图选择——官方导航会先设置选择、随即按「归档选择不保留」把它清空,视图落空态(撤回后归档的原会话最常命中)。修复=判据叠加归档集合排除:`ClientWorkspacesService` 补 `list` 面,从 `ctx.workspaces.list.getSnapshot().archivedSessionIds` 取归档集合(服务经 `buildSettingsCards` 传入快照管理卡),只有「在官方列表且未归档」的会话才渲染「切换」。实弹复验:归档会话行不再出现「切换」,保留「切换」的活跃会话点击后正常打开。
12
+
13
+ ### 变更
14
+
15
+ - **旧版回退核验 + 兼容声明下界收窄**:本批次三处改码都在跨版本面上(静态 `uiWorkspace` inject、`plugins.bundle.config` 注册、读 `workspaces` 归档集合),故把全局 dsh 降到纯 `0.1.6-alpha.1`(`npm install -g @deepseek-ai/dsh@0.1.6-alpha.1 --before=2026-09-16`;不带 `--before` 会因 dsh 自身 `^0.1.6-alpha.1` 放行同 tuple 预发布而混入 alpha.2 的包)实弹一轮,确认未升级 dsh 的用户更新插件后不会变砖——三项全过、控制台零报错:旧 slot(`settings.plugin.item`)卡片照常渲染且样式注入正常、`plugins.bundle.config` 在旧渲染器上是静默 no-op;撤回 fork 后子会话由 `uiWorkspace.openSession` 正常打开(切过去后再发消息、新快照记到子会话 id 为证);快照管理树按 lineage 聚族(`v1/2`/`v2/2`)。同时按 registry 产物逐版核验 `uiWorkspace` 服务可用性:`0.1.2-alpha.2` 起有该服务但无 `openSession`(该线走 `sessions.open` 回退,`ISessions.open` 到 alpha.1 都在)、`0.1.5-rc.2` 起 `openSession` 在位,而 **`0.1.1-rc.2` 线没有该服务**(同线亦缺 `sessions`/`workspaces`:`dsh-api-session-controller`/`dsh-api-workspace-controller` 无该版本发布)——静态声明无法满足,插件 UI 会静默不渲染。据此**7 个 dsh-* peer 范围去掉 `>=0.1.1-rc.2 <0.1.2` 段**,让该组合在安装期被明确拦住而不是装上了白屏;README 双语兼容声明与 badge 上界由 `0.1.6-alpha.1` 更新为 `0.1.6-alpha.2` 并写明不再声明支持 0.1.1-rc.2 及更早的原因;compat-audit I37 补「服务可用性」条目,实弹记录见 `docs/plans/completed/smoke-checklist-records.md`。核验完装回 `0.1.6-alpha.2`(树内 8 个 `@deepseek-ai/dsh-*` 包一致),`npm run check:upgrade` 三层门禁全绿(check:dsh 全一致 / test:probe 39 通过 / verify:host 通过)。
16
+
17
+ - **README 双语补充快照捕获窗口的边界说明**:快照为异步捕获,从收到消息到 `git add` 之间有约 0.5–1.5 秒窗口(Windows 上一次 PowerShell 启动即约 0.4 秒;index.json 记的 `time` 是脚本发起时刻而非捕获时刻)。秒级完成的琐碎任务若落在窗口内改完文件,该条快照会连带捕获本轮改动——撤回时文件回退成为空操作(预览显示「共 0 个文件将变更」),对话回退不受影响,且撤回前仍先落 `snap-pre-rollback-*` 安全快照,无数据丢失。此为异步快照的固有边界,无法消除(除非让每条消息同步阻塞等快照完成)。
18
+ - **dsh 0.1.6-alpha.2 兼容适配:设置卡片迁挂插件管理页 slot(I12)**:全局实装 `@deepseek-ai/dsh@0.1.6-alpha.2`(npm dist-tag `alpha`,tag commit `ddefc45`)后核查发现旧设置页插件 tab 整体移除——`settings.plugin.item` 与 `settings.plugins.tab` 产物字符串归零,新增 ui-plugin-manager 插件管理页,声明 `plugins.item` / `plugins.bundle.config` / `plugins.row.config` 三 slot(bundle 自带配置走 `plugins.bundle.config`,keyed by bundle 包名,page 视图要求表单自含保存控件)。Client 侧改为双键并注册:新增 `plugins.bundle.config`(key=`dsh-recall-plugin`,插件管理页 bundle 页渲染 RecallSettingsCard)+ 保留旧键 `settings.plugin.item`(key=`dsh-recall`,0.1.6-alpha.1 及以前生效)——官方 renderer 对未声明 key 的 `slots.inject` 是静默 no-op(`specDynamic` 为 undefined 直接 return),双版本各吃各键无需运行时探测。其余消费面零破坏:fork 签名与切点 `cut = boundary.seq + 1`(I35 根治)逐字保持;connection `fetch.register` 契约不变(仅新增 `streamBaseUrl?` 可选字段);client sessions 改 retain/release 引用模型(多实例共存),`binding()` 收窄为「只借已 retain 的会话」——附件重建链(I34)全链 typeof 降级兜底,最坏仅附件不重建(导航面另有两处问题,见上方修复)。peer 范围 `>=0.1.6-alpha.1 <0.1.7` 天然覆盖 alpha.2 无需扩展。机器化断言:`npm test` 361/361 + `check:upgrade` 三层门禁全绿(probe 37/37)。reference/ 镜像按 alpha.2 tag 重拉(仅 06/09 文档文字修订);compat-audit 头部追加 alpha.2 核验段并更新 I12。实弹冒烟记录见 `docs/plans/completed/smoke-checklist-records.md`。
19
+
20
+ ## [2.3.22] - 2026-09-16
21
+
22
+ ### 修复
23
+
24
+ - **win32 上宿主把 `ctx.shell` 配成 bash 时功能面整个死亡(issue #15)**:官方 shell 是提供方注册制——一个 composition 恰好一个 `ctx.shell` 实现,win32 的 profile 可以只启用 `bash-sandbox`(禁用 `pwsh-sandbox`);而插件按 `process.platform` 单选 pwsh 模板,两者之间没有契约保证,于是模板被 bash 执行、第一行编码前导即语法错误(`bash: -c: line 1: syntax error near unexpected token '('`),`ensureGit` 起每一步都失败:快照从未成功、撤回按钮不可用。官方 `ShellExecutor` 公开面(`resolve`/`run`/`start` + `sandboxMode`)没有任何方言标识,**不能查询只能探测**,故修复=行为探针判方言 + 判成 bash 时改走直连通道:
25
+ - **S1 方言探针**:`runShellMeta` 首调内联执行 `Write-Output <罕见 ASCII 哨兵>`(不带 UTF8_PRELUDE、30s 短超时、直调 `ctx.shell` 不触发失败清扫);exit 0 且回显哨兵判 pwsh,非零退出(bash 下 command-not-found 即 127)/无输出/reject 一律判 bash。结果按进程缓存并以 in-flight promise 去重(突发 `session/event` 只探一次)。POSIX 不探测——bash 模板与 bash 执行器天然一致。
26
+ - **S2 直连通道**:判 bash 时 `runShellMeta` 分流到 Node `spawn` 直连 `%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe -NoProfile -NonInteractive -Command <单 argv>`(PS 5.1 全平台自带,不赌 PS7;模板本就按 5.1 兼容写),保留官方四项语义:stdin 字节透传(PS 5.1 代码页坑由字节流绕开)、stdout 超截断保留尾部并置 `truncated`(索引读取靠它区分截断与损坏)、超时 `child.kill()`、非零退出仍走 `cleanupAfterGitFailure`;env 复刻官方清洗(剥凭证形状名与全部 `DSH_*`,叠 `NO_COLOR`/`PAGER`/`GIT_PAGER`),cwd 取 `sandboxPolicy.workspaceRoot || process.cwd()`,`windowsHide` 防桌面端闪窗。带 `RECALL_CLEANUP` 哨兵的失败清扫脚本不触发探针(只用缓存)——它只在真实命令失败后被调用,方言早已判定,善后路径不再叠一条探测进程。
27
+ - **默认路径零变化**:`ctx.shell` 即 pwsh 的常规部署(探针判 pwsh)与 POSIX 都不触达直连通道;探针误判的代价也低——两条通道对 pwsh 模板都兼容,判成 bash 只是绕开官方托管环境(无 `dshEnv`/PATH 注入、无进程树级终止)。
28
+ - 新增 `tests/unit/store-shell-dialect.test.js`(31 例:判定/收集/env 清洗/可执行路径四个纯函数 + 假 child 覆盖 stdin 字节透传、截断、超时 kill、spawn error + 分流接线与 in-flight 去重);`tests/probe/api-surface.test.js` 新增「win32 shell 方言」3 例(ShellExecutor 无方言字段、直连复刻的 PS 5.1 候选路径与 argv 旗标、env 清洗口径);compat-audit 新增 I36。
29
+
5
30
  ## [2.3.21] - 2026-09-15
6
31
 
7
32
  ### 变更
package/README.en.md CHANGED
@@ -7,7 +7,7 @@
7
7
  ![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20Linux%20%7C%20macOS-blue)
8
8
  ![Build](https://img.shields.io/badge/pure%20JS-green)
9
9
 
10
- [![DSH](https://img.shields.io/badge/DSH-0.1.6--alpha.1-blue)](https://github.com/deepseek-ai/deepseek-harness/releases/tag/dsh-v0.1.6-alpha.1)
10
+ [![DSH](https://img.shields.io/badge/DSH-0.1.6--alpha.2-blue)](https://github.com/deepseek-ai/deepseek-harness/releases/tag/dsh-v0.1.6-alpha.2)
11
11
  ![DSH](https://img.shields.io/badge/DSH-Desktop-blue)
12
12
  ---
13
13
  Under any message you've sent, click "↶ Recall" — **your workspace files and the conversation history roll back to just before that message was sent**.
@@ -58,6 +58,7 @@ Abilities with a version in parentheses require that version or later; the rest
58
58
  Boundaries accepted by design and edge cases not yet covered — worth checking before you rely on them.
59
59
 
60
60
  - Snapshots are created **when a message is sent**; messages from before the plugin was enabled have no snapshot and show no recall button.
61
+ - Snapshots are **best-effort**: there is a 0.5–1.5 second window between the message arriving and `git add` (on Windows, a single PowerShell start alone costs about 0.4s). If a trivial task finishes editing files inside that window, the snapshot captures the turn's own changes — the file rollback then becomes a no-op (the preview panel reports "0 files will change") while the conversation rollback is unaffected.
61
62
  - The first user message of a session cannot roll back the conversation (files only), because fork requires an earlier turn boundary.
62
63
  - Recall cannot be initiated while an agent is running in the target workspace (by design — stop the agent first).
63
64
  - Supports Windows (PowerShell 5.1/7 + git CLI) and Linux/macOS (bash + git CLI). Windows is thoroughly verified on real machines; Linux has been fully tested on WSL2 (Ubuntu 26.04, bash 5.3 + git 2.53), including Chinese paths, home fallback, session cleanup, and gc; the macOS side is written to be bash 3.2 compatible but has not been tested on real hardware yet.
@@ -67,7 +68,7 @@ Boundaries accepted by design and edge cases not yet covered — worth checking
67
68
 
68
69
  ## Installation
69
70
 
70
- Prerequisites: git CLI (without it the recall button won't appear and a notice shows at the top of the page — DSH itself keeps running); PowerShell 5.1 / 7 on Windows, bash + git on Linux/macOS; DSH `0.1.1-rc.2` through `0.1.6-alpha.1` (peerDependencies open a window per minor line: `>=0.1.1-rc.2 <0.1.2 || >=0.1.2-alpha.1 <0.1.3 || >=0.1.3-alpha.1 <0.1.4 || >=0.1.5-alpha.1 <0.1.6 || >=0.1.6-alpha.1 <0.1.7`, consistent with the `dsh.compatibility.dshReleases` declaration; each line is anchored at its first verified version with an exclusive upper bound at the next minor, so later prereleases/releases within a verified line are admitted without touching the peer declaration, while unverified new minor lines (e.g. 0.1.7) remain blocked).
71
+ Prerequisites: git CLI (without it the recall button won't appear and a notice shows at the top of the page — DSH itself keeps running); PowerShell 5.1 / 7 on Windows, bash + git on Linux/macOS; DSH `0.1.2-alpha.1` through `0.1.6-alpha.2` (peerDependencies open a window per minor line: `>=0.1.2-alpha.1 <0.1.3 || >=0.1.3-alpha.1 <0.1.4 || >=0.1.5-alpha.1 <0.1.6 || >=0.1.6-alpha.1 <0.1.7`, consistent with the `dsh.compatibility.dshReleases` declaration; each line is anchored at its first verified version with an exclusive upper bound at the next minor, so later prereleases/releases within a verified line are admitted without touching the peer declaration, while unverified new minor lines (e.g. 0.1.7) remain blocked). `0.1.1-rc.2` and earlier are no longer declared supported: their client runtime lacks the `sessions`/`workspaces`/`uiWorkspace` services, so the plugin UI silently fails to render.
71
72
 
72
73
  - Official DSH plugin command: install and auto-mount into the web profile
73
74
  ```powershell
@@ -137,6 +138,7 @@ The plugin manages disk usage automatically — no manual housekeeping needed:
137
138
  When each user message is sent (before the agent touches any files), the workspace is snapshotted into an independent shadow git repository; on recall, a "pre-rollback" safety snapshot is taken first, then files are restored via `git archive` and the conversation is rewound through DSH's official `sessions.fork` mechanism. Binary- and line-ending-safe, and your project's own git state is never touched.
138
139
 
139
140
  - Snapshot storage: `dsh-recall-snapshots/<SHA256(project absolute path)>/` under home, containing the shadow git repository (`git/`, tags named `snap-<messageID>`), the index file `index.json` (message ID → snapshot time / session), and the recall chain `lineage.json`. Scripts run via PowerShell on Windows and bash on Linux/macOS (selected automatically by platform).
141
+ - **Works on Windows even when the host configures its shell as bash** (2.3.22+): DSH's shell is registered by the profile, and on win32 you can enable only `bash-sandbox` — the PowerShell templates would then be executed by bash and fail across the board. Before running its first command the plugin probes whether the executor can run pwsh (a sentinel command); if it turns out to be bash, the plugin switches to spawning the system PowerShell 5.1 directly, so snapshots and recall keep working. Deployments where `ctx.shell` is pwsh behave exactly as before (one probe, then everything as usual).
140
142
  - To browse historical snapshots directly:
141
143
 
142
144
  ```powershell
@@ -162,7 +164,7 @@ Note: all source lives in `src/` (host in `src/host/`, browser side in `src/clie
162
164
 
163
165
  ### Tests
164
166
 
165
- - `npm test`: pure-logic unit tests (vitest, 17 files / 227 cases, no DSH dependency, runs identically in CI and locally) — config parsing, snapshot parsers, rescue orchestration, error classification, script-template same-name-export contract, client pure functions, published-package layout, snapshot index persistence, storage caps and retention, etc.;
167
+ - `npm test`: pure-logic unit tests (vitest, 28 files / 361 cases, no DSH dependency, runs identically in CI and locally) — config parsing, snapshot parsers, rescue orchestration, error classification, script-template same-name-export contract, client pure functions, published-package layout, snapshot index persistence, storage caps and retention, etc.;
166
168
  - `npm run test:probe`: official-API field probes (requires a local dsh installation; **must run after any dsh upgrade**) — pins fields like `renderMessageImages`/`node`/`cwd`, `atSeq`/`increaseTitle` of `sessions.fork`, `listSessions` record shape, `AgentRegistry`, and goes red on violation;
167
169
  - `npm run verify:host`: assembly gate (requires a local dsh installation) — boots the plugin with a real cordis context, asserting inject declarations, endpoint registration, Config schema, and teardown cleanliness, catching assembly regressions before release;
168
170
  - `npm run build`: full host+client build (mandatory after any `src/` change); `npm run check:dsh`: dsh version inspection (pre-release).
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  [![npm version](https://img.shields.io/npm/v/dsh-recall-plugin?style=flat-square&label=npm&color=3178C6)](https://www.npmjs.com/package/dsh-recall-plugin)
6
6
  [![npm downloads](https://img.shields.io/npm/dm/dsh-recall-plugin?style=flat-square&label=downloads&color=1F883D)](https://www.npmjs.com/package/dsh-recall-plugin)
7
7
  ![License](https://img.shields.io/badge/license-MIT-blue)
8
- [![DSH](https://img.shields.io/badge/DSH-0.1.6--alpha.1-blue)](https://github.com/deepseek-ai/deepseek-harness/releases/tag/dsh-v0.1.6-alpha.1)
8
+ [![DSH](https://img.shields.io/badge/DSH-0.1.6--alpha.2-blue)](https://github.com/deepseek-ai/deepseek-harness/releases/tag/dsh-v0.1.6-alpha.2)
9
9
  ---
10
10
  在任意一条你发过的消息下方点「↶ 撤回」——**工作区文件和对话历史一起回到那条消息发出之前的状态**。
11
11
  ---
@@ -56,6 +56,7 @@
56
56
  设计上接受的边界与尚未覆盖的极端情形,使用前值得先确认。
57
57
 
58
58
  - 快照在**消息发送时**创建,插件启用前的历史消息没有快照,不显示撤回按钮。
59
+ - 快照是**尽力捕获**:从收到消息到 `git add` 之间有约 0.5–1.5 秒窗口(Windows 上仅一次 PowerShell 启动就占约 0.4 秒)。秒级完成的琐碎任务若在这个窗口内改完文件,该快照会连带捕获本轮改动——撤回时文件回退成为空操作(预览面板显示「共 0 个文件将变更」),对话回退不受影响。
59
60
  - 会话第一条用户消息无法回退对话(仅文件回退),因为 fork 需要更早的 turn 边界。
60
61
  - 目标工作区的 agent 正在运行时无法发起撤回(防护设计,先停止 agent 再撤回)。
61
62
  - 支持 Windows(PowerShell 5.1/7 + git CLI)与 Linux/macOS(bash + git CLI)。Windows 真机验证充分;Linux 已在 WSL2(Ubuntu 26.04,bash 5.3 + git 2.53)实测全流程(含中文路径、home 降级、会话清理、gc);macOS 侧脚本按 bash 3.2 兼容编写,尚未真机实测。
@@ -65,7 +66,7 @@
65
66
 
66
67
  ## 安装
67
68
 
68
- 前置:git CLI(未装时撤回按钮不出现,页面顶部会提示安装 git,不影响 DSH 运行);Windows 上 PowerShell 5.1 / 7 均可,Linux/macOS 需 bash + git;DSH `0.1.1-rc.2` 至 `0.1.6-alpha.1`(peerDependencies 为按 minor 版本线开窗 `>=0.1.1-rc.2 <0.1.2 || >=0.1.2-alpha.1 <0.1.3 || >=0.1.3-alpha.1 <0.1.4 || >=0.1.5-alpha.1 <0.1.6 || >=0.1.6-alpha.1 <0.1.7`,与 `dsh.compatibility.dshReleases` 声明一致;每条线以首个核验版本为下限、开一条上界到下一 minor 的区间,同线内后续 prerelease/正式版自动放行、无需改 peer 声明,未验证的新 minor 线(如 0.1.7)仍被拦截)。
69
+ 前置:git CLI(未装时撤回按钮不出现,页面顶部会提示安装 git,不影响 DSH 运行);Windows 上 PowerShell 5.1 / 7 均可,Linux/macOS 需 bash + git;DSH `0.1.2-alpha.1` 至 `0.1.6-alpha.2`(peerDependencies 为按 minor 版本线开窗 `>=0.1.2-alpha.1 <0.1.3 || >=0.1.3-alpha.1 <0.1.4 || >=0.1.5-alpha.1 <0.1.6 || >=0.1.6-alpha.1 <0.1.7`,与 `dsh.compatibility.dshReleases` 声明一致;每条线以首个核验版本为下限、开一条上界到下一 minor 的区间,同线内后续 prerelease/正式版自动放行、无需改 peer 声明,未验证的新 minor 线(如 0.1.7)仍被拦截)。`0.1.1-rc.2` 及更早不再声明支持:那条线的客户端运行时没有 `sessions`/`workspaces`/`uiWorkspace` 服务,插件 UI 会静默不渲染。
69
70
 
70
71
 
71
72
  - DSH 官方插件命令:安装并自动挂载进 web profile
@@ -140,6 +141,7 @@ dsh plugin --profile web remove dsh-recall-plugin
140
141
  每条用户消息发送时(agent 动文件之前),工作区被快照进一个独立的影子 git 仓库;撤回时先打「回退前」安全快照、再用 `git archive` 恢复文件、通过 DSH 官方 `sessions.fork` 机制把会话切到该消息之前。二进制与换行符安全,全程不触碰项目自身的 git 状态。
141
142
 
142
143
  - 快照存储:home 下 `dsh-recall-snapshots/<SHA256(项目绝对路径)>/`,内含影子 git 仓库(`git/`,tag 名为 `snap-<消息ID>`)、索引文件 `index.json`(消息 ID → 快照时间 / 会话)与撤回链 `lineage.json`。Windows 上脚本走 PowerShell,Linux/macOS 走 bash(按平台自动分叉)。
144
+ - **Windows 上宿主把 shell 配成 bash 也照常工作**(2.3.22+):官方 shell 由 profile 注册,win32 上可以只启用 `bash-sandbox`——此时 pwsh 脚本会被 bash 执行而全盘失败。插件首次执行命令前会先探测执行器能否跑 pwsh(哨兵命令),确认是 bash 后改用系统 PowerShell 5.1 直连执行,快照与撤回不受影响;`ctx.shell` 即 pwsh 的常规部署行为不变(探测一次、判为 pwsh 后一切照旧)。
143
145
  - 想直接翻历史快照:
144
146
 
145
147
  ```powershell
@@ -165,7 +167,7 @@ pnpm install
165
167
 
166
168
  ### 测试
167
169
 
168
- - `npm test`:纯逻辑单测(vitest,17 个文件 227 例,无 DSH 依赖,CI 与本地同跑)——配置解析、快照解析器、救援编排、错误分类、脚本模板同名导出契约、客户端纯函数、发布包内容布局、快照索引持久化、存储上限与保留天数等;
170
+ - `npm test`:纯逻辑单测(vitest,28 个文件 361 例,无 DSH 依赖,CI 与本地同跑)——配置解析、快照解析器、救援编排、错误分类、脚本模板同名导出契约、客户端纯函数、发布包内容布局、快照索引持久化、存储上限与保留天数等;
169
171
  - `npm run test:probe`:官方 API 字段探针(依赖本机 dsh 安装;dsh 升级后本地必跑)——钉住 `renderMessageImages`/`node`/`cwd`、`sessions.fork` 的 `atSeq`/`increaseTitle`、`listSessions` 记录结构、`AgentRegistry` 等字段,违反即红;
170
172
  - `npm run verify:host`:装配门禁(依赖本机 dsh 安装)——用真实 cordis 起插件,断言 inject 声明、端点注册、Config schema、卸载清理,装配回归发版前即可拦截;
171
173
  - `npm run build`:host+client 全量打包(改任何 `src/` 后必跑);`npm run check:dsh`:dsh 版本巡检(发布前)。
package/lib/client.js CHANGED
@@ -438,7 +438,7 @@
438
438
  }
439
439
  return parts.join(" \xB7 ");
440
440
  }
441
- function buildRecallNode(React, util, ctx, sessionsSvc, workspacesSvc) {
441
+ function buildRecallNode(React, util, ctx, sessionsSvc, workspacesSvc, uiWorkspaceSvc) {
442
442
  const { api, ensureInit, showThrottledToast, writeClipboard, clockText: clockText2, pluginConfig, messageFor } = util;
443
443
  function CopyIcon() {
444
444
  return React.createElement(
@@ -793,7 +793,8 @@
793
793
  try {
794
794
  const childId = await sessionsSvc.fork({ sessionId, atSeq: cutSeq });
795
795
  if (childId) {
796
- if (typeof sessionsSvc.open === "function") sessionsSvc.open(childId);
796
+ if (uiWorkspaceSvc && typeof uiWorkspaceSvc.openSession === "function") uiWorkspaceSvc.openSession(childId);
797
+ else if (typeof sessionsSvc.open === "function") sessionsSvc.open(childId);
797
798
  chatReverted = true;
798
799
  fillTarget = childId;
799
800
  removeStaleQueueItems(childId, res.staleQueueItemIds);
@@ -1322,7 +1323,7 @@
1322
1323
  }
1323
1324
  return result;
1324
1325
  }
1325
- function buildSnapshotManager(React, util, sessionsSvc) {
1326
+ function buildSnapshotManager(React, util, sessionsSvc, workspacesSvc, uiWorkspaceSvc) {
1326
1327
  const { api, clockText: clockText2, sizeText: sizeText2, buildTree: buildTree2 } = util;
1327
1328
  function ManageCard() {
1328
1329
  const [items, setItems] = React.useState(null);
@@ -1481,6 +1482,13 @@
1481
1482
  } catch (e) {
1482
1483
  listById = null;
1483
1484
  }
1485
+ let archivedIds = null;
1486
+ try {
1487
+ const snapshot = workspacesSvc && workspacesSvc.list && typeof workspacesSvc.list.getSnapshot === "function" ? workspacesSvc.list.getSnapshot() : null;
1488
+ archivedIds = new Set(snapshot && snapshot.archivedSessionIds || []);
1489
+ } catch (e) {
1490
+ archivedIds = null;
1491
+ }
1484
1492
  function confirmDelete(kind, key, extra, text) {
1485
1493
  setConfirming({ kind, key, extra, text });
1486
1494
  }
@@ -1528,7 +1536,7 @@
1528
1536
  const open = expanded.has(key);
1529
1537
  const label = s.title || (titlesPending && s.sessionId ? "\u2026" : "\uFF08\u5DF2\u5220\u9664\u4F1A\u8BDD\uFF09");
1530
1538
  const version = s.sessionId ? versionMap.get(String(s.sessionId)) : null;
1531
- const switchable = Boolean(s.sessionId && listById && listById[s.sessionId]);
1539
+ const switchable = Boolean(s.sessionId && listById && listById[s.sessionId] && !(archivedIds && archivedIds.has(String(s.sessionId))));
1532
1540
  return React.createElement(
1533
1541
  "div",
1534
1542
  { className: "dsh-recall-tree-node", key },
@@ -1557,7 +1565,8 @@
1557
1565
  title: "\u5207\u6362\u5230\u8BE5\u7248\u672C\u4F1A\u8BDD",
1558
1566
  onClick: () => {
1559
1567
  try {
1560
- sessionsSvc.open(s.sessionId);
1568
+ if (uiWorkspaceSvc && typeof uiWorkspaceSvc.openSession === "function") uiWorkspaceSvc.openSession(s.sessionId);
1569
+ else if (typeof sessionsSvc.open === "function") sessionsSvc.open(s.sessionId);
1561
1570
  } catch (e) {
1562
1571
  }
1563
1572
  }
@@ -1709,7 +1718,7 @@
1709
1718
  }
1710
1719
 
1711
1720
  // src/client/settings-cards.ts
1712
- function buildSettingsCards(React, util, sessionsSvc) {
1721
+ function buildSettingsCards(React, util, sessionsSvc, workspacesSvc, uiWorkspaceSvc) {
1713
1722
  function SectionToggle(props) {
1714
1723
  return React.createElement(
1715
1724
  "button",
@@ -1728,7 +1737,7 @@
1728
1737
  }
1729
1738
  const { ConfigForm } = buildConfigForm(React, util, SectionToggle);
1730
1739
  const { ExcludeFilesSection } = buildExcludeCards(React, util);
1731
- const { ManageCard } = buildSnapshotManager(React, util, sessionsSvc);
1740
+ const { ManageCard } = buildSnapshotManager(React, util, sessionsSvc, workspacesSvc, uiWorkspaceSvc);
1732
1741
  function RecallSettingsCard() {
1733
1742
  const [open, setOpen] = React.useState(false);
1734
1743
  const [sections, setSections] = React.useState({ exclude: false, manage: false });
@@ -1801,8 +1810,9 @@
1801
1810
  document.head.appendChild(tag);
1802
1811
  }
1803
1812
  const util = buildUtil();
1804
- const { UserRecallNode } = buildRecallNode(React, util, ctx, sessionsSvc, workspacesSvc);
1805
- const { RecallSettingsCard } = buildSettingsCards(React, util, sessionsSvc);
1813
+ const uiWorkspaceSvc = ctx.uiWorkspace;
1814
+ const { UserRecallNode } = buildRecallNode(React, util, ctx, sessionsSvc, workspacesSvc, uiWorkspaceSvc);
1815
+ const { RecallSettingsCard } = buildSettingsCards(React, util, sessionsSvc, workspacesSvc, uiWorkspaceSvc);
1806
1816
  for (const slotKey of ["user", "steering"]) {
1807
1817
  try {
1808
1818
  slots.inject("conversation.chat.node", () => {
@@ -1824,6 +1834,14 @@
1824
1834
  } catch (error) {
1825
1835
  console.error("[dsh-recall-plugin] settings card register failed:", error);
1826
1836
  }
1837
+ try {
1838
+ slots.inject("plugins.bundle.config", () => slots.register(
1839
+ { name: "plugins.bundle.config", key: "dsh-recall-plugin" },
1840
+ RecallSettingsCard
1841
+ ));
1842
+ } catch (error) {
1843
+ console.error("[dsh-recall-plugin] plugin manager config register failed:", error);
1844
+ }
1827
1845
  };
1828
1846
  }
1829
1847
 
@@ -1834,7 +1852,12 @@
1834
1852
  const React = require2("react");
1835
1853
  return {
1836
1854
  name: "dsh-recall-plugin",
1837
- inject: ["slots", "sessions", "workspaces", "timer"],
1855
+ // uiWorkspace(ui-workspace UiWorkspace):0.1.6-alpha.2 起 ISessions
1856
+ // 移除 open,会话导航(fork 后打开子会话)移交其 openSession——0.1.2-alpha.1
1857
+ // 起该服务即存在,声明安全;0.1.1-rc.2 无此服务(同线亦无 sessions/
1858
+ // workspaces),声明会令 fiber pending——该线段已从 peer 范围移除
1859
+ // (见 compat-audit I37「服务可用性」)。
1860
+ inject: ["slots", "sessions", "workspaces", "uiWorkspace", "timer"],
1838
1861
  apply: createApp(React)
1839
1862
  };
1840
1863
  }
package/lib/store.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import os from "node:os";
2
2
  import crypto from "node:crypto";
3
+ import { spawn as childSpawn } from "node:child_process";
3
4
  import * as pwshScripts from "./scripts.pwsh.js";
4
5
  import * as posixScripts from "./scripts.posix.js";
5
6
  import { classifyEnvError } from "./diagnostics.js";
@@ -37,9 +38,100 @@ function isTmpConsumedError(error, basename) {
37
38
  if (!basename || s.indexOf(basename) < 0) return false;
38
39
  return /No such file/i.test(s) || /does not exist/i.test(s) || /cannot find path/i.test(s);
39
40
  }
40
- function createRuntime(ctx, config) {
41
+ const SHELL_PROBE_SENTINEL = "RCL_DIALECT_PROBE_9f4a2e";
42
+ const SHELL_PROBE_COMMAND = "Write-Output " + SHELL_PROBE_SENTINEL;
43
+ function judgeShellDialect(res) {
44
+ if (!res || res.exitCode !== 0) return "bash";
45
+ const text = res.stdout && res.stdout.text || "";
46
+ return text.indexOf(SHELL_PROBE_SENTINEL) >= 0 ? "pwsh" : "bash";
47
+ }
48
+ const SENSITIVE_ENV = /KEY|PASSWORD|SECRET|TOKEN/i;
49
+ const STDERR_KEEP_BYTES = 65536;
50
+ function scrubChildEnv(env) {
51
+ const out = {};
52
+ for (const key of Object.keys(env)) {
53
+ const value = env[key];
54
+ if (value === void 0) continue;
55
+ if (SENSITIVE_ENV.test(key) || key.toUpperCase().indexOf("DSH_") === 0) continue;
56
+ out[key] = value;
57
+ }
58
+ out.NO_COLOR = "1";
59
+ out.PAGER = "cat";
60
+ out.GIT_PAGER = "cat";
61
+ return out;
62
+ }
63
+ function collectStdout(chunks, maxBytes) {
64
+ const total = chunks.reduce((n, c) => n + c.length, 0);
65
+ if (total <= maxBytes) return { text: Buffer.concat(chunks).toString("utf8"), truncated: false };
66
+ const all = Buffer.concat(chunks);
67
+ return { text: all.subarray(all.length - maxBytes).toString("utf8"), truncated: true };
68
+ }
69
+ function directPwshPath(env) {
70
+ return (env.SystemRoot || env.windir || "C:\\Windows") + "\\System32\\WindowsPowerShell\\v1.0\\powershell.exe";
71
+ }
72
+ async function runViaSpawn(spawn, req) {
73
+ const child = spawn(directPwshPath(process.env), ["-NoProfile", "-NonInteractive", "-Command", req.command], {
74
+ cwd: req.cwd,
75
+ env: scrubChildEnv(process.env),
76
+ windowsHide: true
77
+ });
78
+ return await new Promise((resolve, reject) => {
79
+ const sink = (maxBytes) => {
80
+ const chunks = [];
81
+ let total = 0;
82
+ return {
83
+ push(chunk) {
84
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
85
+ chunks.push(buf);
86
+ total += buf.length;
87
+ while (chunks.length > 1 && total - chunks[0].length > maxBytes) {
88
+ total -= chunks[0].length;
89
+ chunks.shift();
90
+ }
91
+ },
92
+ take() {
93
+ return collectStdout(chunks, maxBytes);
94
+ }
95
+ };
96
+ };
97
+ const outSink = sink(req.stdoutMaxBytes);
98
+ const errSink = sink(STDERR_KEEP_BYTES);
99
+ let timedOut = false;
100
+ let settled = false;
101
+ const timer = setTimeout(() => {
102
+ timedOut = true;
103
+ try {
104
+ child.kill();
105
+ } catch (error) {
106
+ }
107
+ }, Math.max(1, req.timeoutMs));
108
+ const finish = (code) => {
109
+ if (settled) return;
110
+ settled = true;
111
+ clearTimeout(timer);
112
+ const stdout = outSink.take();
113
+ resolve({ text: stdout.text, truncated: stdout.truncated, exitCode: code, stderr: errSink.take().text, timedOut });
114
+ };
115
+ if (child.stdout) child.stdout.on("data", (chunk) => outSink.push(chunk));
116
+ if (child.stderr) child.stderr.on("data", (chunk) => errSink.push(chunk));
117
+ child.on("error", (error) => {
118
+ if (settled) return;
119
+ settled = true;
120
+ clearTimeout(timer);
121
+ reject(error);
122
+ });
123
+ child.on("close", (code) => finish(code === void 0 ? null : code));
124
+ if (child.stdin) {
125
+ child.stdin.on("error", () => {
126
+ });
127
+ child.stdin.end(Buffer.from(req.stdin === void 0 ? "" : req.stdin, "utf8"));
128
+ }
129
+ });
130
+ }
131
+ function createRuntime(ctx, config, deps) {
41
132
  const shell = ctx.shell;
42
133
  const sessions = ctx.sessions;
134
+ const spawn = deps && deps.spawn || childSpawn;
43
135
  const isWin = process.platform === "win32";
44
136
  const SEP = isWin ? "\\" : "/";
45
137
  const scripts = isWin ? pwshScripts : posixScripts;
@@ -66,6 +158,10 @@ function createRuntime(ctx, config) {
66
158
  gcCount: /* @__PURE__ */ new Map(),
67
159
  gitExe: null,
68
160
  posixHomeBase: null,
161
+ // win32 方言探针缓存(issue #15):判定结果 + in-flight promise(并发首调
162
+ // 只跑一次探针);POSIX 不探测,两字段恒为 null
163
+ shellDialect: null,
164
+ shellDialectProbe: null,
69
165
  homeContainer: null,
70
166
  errors: [],
71
167
  // 逐消息的快照反馈(issue #7 失败可见性):失败 {failed,error} 或
@@ -95,28 +191,71 @@ function createRuntime(ctx, config) {
95
191
  const missing = pwshKeys.filter((k) => posixKeys.indexOf(k) < 0);
96
192
  if (missing.length) recordError("recall script parity: posix \u7F3A\u5C11\u5BFC\u51FA " + missing.join(", "));
97
193
  })();
194
+ function resolveShellDialect() {
195
+ if (state.shellDialect) return Promise.resolve(state.shellDialect);
196
+ if (!state.shellDialectProbe) {
197
+ state.shellDialectProbe = (async () => {
198
+ let res = null;
199
+ try {
200
+ const sp = ctx.get("sandboxPolicy");
201
+ res = await shell.run(shell.resolve({
202
+ command: SHELL_PROBE_COMMAND,
203
+ timeoutMs: 3e4,
204
+ stdoutMaxBytes: 4096,
205
+ sandboxPolicy: { mode: "danger-full-access", workspaceRoot: sp && sp.workspaceRoot || process.cwd() }
206
+ }));
207
+ } catch (error) {
208
+ res = null;
209
+ }
210
+ const dialect = judgeShellDialect(res);
211
+ state.shellDialect = dialect;
212
+ console.error("recall shell dialect probe: " + dialect + (dialect === "bash" ? "\uFF08ctx.shell \u975E pwsh\uFF0C\u6539\u7528\u76F4\u8FDE powershell.exe \u901A\u9053\uFF09" : ""));
213
+ return dialect;
214
+ })();
215
+ }
216
+ return state.shellDialectProbe;
217
+ }
218
+ async function shellDialectFor(command) {
219
+ if (command.indexOf("RECALL_CLEANUP") >= 0) return state.shellDialect || "pwsh";
220
+ return await resolveShellDialect();
221
+ }
222
+ async function throwShellFailure(command, stderr, exitCode) {
223
+ await cleanupAfterGitFailure(command);
224
+ const detail = String(stderr || "").trim() || "exit " + String(exitCode);
225
+ throw new Error(detail.slice(0, 1500));
226
+ }
227
+ async function runShellDirect(command, req) {
228
+ const res = await runViaSpawn(spawn, req);
229
+ if (res.timedOut) {
230
+ await throwShellFailure(command, res.stderr + "\n\u76F4\u8FDE powershell.exe \u8D85\u65F6\uFF08" + String(req.timeoutMs) + "ms\uFF09", null);
231
+ }
232
+ if (res.exitCode !== 0) await throwShellFailure(command, res.stderr, res.exitCode);
233
+ return { text: res.text, truncated: res.truncated };
234
+ }
98
235
  async function runShellMeta(command, opts) {
99
236
  const sp = ctx.get("sandboxPolicy");
237
+ const workspaceRoot = sp && sp.workspaceRoot || process.cwd();
238
+ const timeoutMs = opts && opts.timeoutMs || 3e5;
239
+ const stdoutMaxBytes = opts && opts.stdoutMaxBytes || 4194304;
240
+ const fullCommand = scripts.UTF8_PRELUDE + "\n" + command;
241
+ if (isWin && await shellDialectFor(command) === "bash") {
242
+ return await runShellDirect(command, { command: fullCommand, timeoutMs, stdoutMaxBytes, cwd: workspaceRoot, stdin: opts && opts.stdin });
243
+ }
100
244
  const spec = shell.resolve({
101
- // 编码前导:pwsh 侧统一 UTF-8 输出(中文机器 GBK 代码页不再乱码);
102
- // bash 侧 LC_ALL=C 确定序。各模板自带,这里统一前置注入。
103
- command: scripts.UTF8_PRELUDE + "\n" + command,
104
- timeoutMs: opts && opts.timeoutMs || 3e5,
105
- stdoutMaxBytes: opts && opts.stdoutMaxBytes || 4194304,
245
+ command: fullCommand,
246
+ timeoutMs,
247
+ stdoutMaxBytes,
106
248
  // stdin 是官方 ShellExecRequest 契约字段(bash-local/pwsh 均实现),
107
249
  // POSIX 侧用它传 index.json 全文,绕开 argv 长度上限
108
250
  ...opts && opts.stdin !== void 0 ? { stdin: opts.stdin } : {},
109
- sandboxPolicy: { mode: "danger-full-access", workspaceRoot: sp && sp.workspaceRoot || process.cwd() }
251
+ sandboxPolicy: { mode: "danger-full-access", workspaceRoot }
110
252
  });
111
253
  const res = await shell.run(spec);
112
- const out = res && res.stdout && res.stdout.text || "";
113
254
  if (res && res.exitCode !== 0) {
114
- await cleanupAfterGitFailure(command);
115
- const err = (res && res.stderr && res.stderr.text || "").trim() || "exit " + String(res.exitCode);
116
- throw new Error(err.slice(0, 1500));
255
+ await throwShellFailure(command, res && res.stderr && res.stderr.text || "", res.exitCode);
117
256
  }
118
257
  return {
119
- text: out,
258
+ text: res && res.stdout && res.stdout.text || "",
120
259
  truncated: Boolean(res && res.stdout && res.stdout.truncated)
121
260
  };
122
261
  }
@@ -356,9 +495,16 @@ function createRuntime(ctx, config) {
356
495
  return { state, isWin, scripts, recordError, runShell, runShellMeta, writeTextViaShell, resolveRoot, resolveGit, homeDirFor, resolveHomeContainer, resolveStore, storeFromDir, tryUpgradeToHome, ensureGit, cleanupLegacy, cleanupAfterGitFailure };
357
496
  }
358
497
  export {
498
+ SHELL_PROBE_COMMAND,
499
+ SHELL_PROBE_SENTINEL,
500
+ collectStdout,
359
501
  createRuntime,
502
+ directPwshPath,
360
503
  isTmpConsumedError,
504
+ judgeShellDialect,
361
505
  parseCleanupResult,
362
506
  resolvePosixHomeBase,
507
+ runViaSpawn,
508
+ scrubChildEnv,
363
509
  selectPosixHomeBase
364
510
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-recall-plugin",
3
- "version": "2.3.21",
3
+ "version": "2.3.23",
4
4
  "description": "在 DSH 中撤回自己发过的消息:工作区文件与对话历史一起回到那条消息发出之前,消息的文本与附件自动放回输入框,改完即可重发。文件走独立影子 git 快照(不碰项目自身的 git),对话走官方 sessions.fork,原会话归档可找回。",
5
5
  "type": "module",
6
6
  "repository": {
@@ -57,18 +57,19 @@
57
57
  "0.1.5-alpha.2": "compatible",
58
58
  "0.1.5-rc.1": "compatible",
59
59
  "0.1.5-rc.2": "compatible",
60
- "0.1.6-alpha.1": "compatible"
60
+ "0.1.6-alpha.1": "compatible",
61
+ "0.1.6-alpha.2": "compatible"
61
62
  }
62
63
  }
63
64
  },
64
65
  "peerDependencies": {
65
66
  "@deepseek-ai/cordis": "^4.0.1",
66
- "@deepseek-ai/dsh-host-webserver": ">=0.1.1-rc.2 <0.1.2 || >=0.1.2-alpha.1 <0.1.3 || >=0.1.3-alpha.1 <0.1.4 || >=0.1.5-alpha.1 <0.1.6 || >=0.1.6-alpha.1 <0.1.7",
67
- "@deepseek-ai/dsh-sandbox-policy": ">=0.1.1-rc.2 <0.1.2 || >=0.1.2-alpha.1 <0.1.3 || >=0.1.3-alpha.1 <0.1.4 || >=0.1.5-alpha.1 <0.1.6 || >=0.1.6-alpha.1 <0.1.7",
68
- "@deepseek-ai/dsh-session": ">=0.1.1-rc.2 <0.1.2 || >=0.1.2-alpha.1 <0.1.3 || >=0.1.3-alpha.1 <0.1.4 || >=0.1.5-alpha.1 <0.1.6 || >=0.1.6-alpha.1 <0.1.7",
69
- "@deepseek-ai/dsh-session-query": ">=0.1.1-rc.2 <0.1.2 || >=0.1.2-alpha.1 <0.1.3 || >=0.1.3-alpha.1 <0.1.4 || >=0.1.5-alpha.1 <0.1.6 || >=0.1.6-alpha.1 <0.1.7",
70
- "@deepseek-ai/dsh-settings": ">=0.1.1-rc.2 <0.1.2 || >=0.1.2-alpha.1 <0.1.3 || >=0.1.3-alpha.1 <0.1.4 || >=0.1.5-alpha.1 <0.1.6 || >=0.1.6-alpha.1 <0.1.7",
71
- "@deepseek-ai/dsh-shell": ">=0.1.1-rc.2 <0.1.2 || >=0.1.2-alpha.1 <0.1.3 || >=0.1.3-alpha.1 <0.1.4 || >=0.1.5-alpha.1 <0.1.6 || >=0.1.6-alpha.1 <0.1.7",
67
+ "@deepseek-ai/dsh-host-webserver": ">=0.1.2-alpha.1 <0.1.3 || >=0.1.3-alpha.1 <0.1.4 || >=0.1.5-alpha.1 <0.1.6 || >=0.1.6-alpha.1 <0.1.7",
68
+ "@deepseek-ai/dsh-sandbox-policy": ">=0.1.2-alpha.1 <0.1.3 || >=0.1.3-alpha.1 <0.1.4 || >=0.1.5-alpha.1 <0.1.6 || >=0.1.6-alpha.1 <0.1.7",
69
+ "@deepseek-ai/dsh-session": ">=0.1.2-alpha.1 <0.1.3 || >=0.1.3-alpha.1 <0.1.4 || >=0.1.5-alpha.1 <0.1.6 || >=0.1.6-alpha.1 <0.1.7",
70
+ "@deepseek-ai/dsh-session-query": ">=0.1.2-alpha.1 <0.1.3 || >=0.1.3-alpha.1 <0.1.4 || >=0.1.5-alpha.1 <0.1.6 || >=0.1.6-alpha.1 <0.1.7",
71
+ "@deepseek-ai/dsh-settings": ">=0.1.2-alpha.1 <0.1.3 || >=0.1.3-alpha.1 <0.1.4 || >=0.1.5-alpha.1 <0.1.6 || >=0.1.6-alpha.1 <0.1.7",
72
+ "@deepseek-ai/dsh-shell": ">=0.1.2-alpha.1 <0.1.3 || >=0.1.3-alpha.1 <0.1.4 || >=0.1.5-alpha.1 <0.1.6 || >=0.1.6-alpha.1 <0.1.7",
72
73
  "@deepseek-ai/schemastery": "^3.18.1",
73
74
  "react": "^18.2.0"
74
75
  },