dsh-diff-approval 0.3.0 → 0.4.0

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/README.md CHANGED
@@ -2,20 +2,35 @@
2
2
 
3
3
  English | [中文](README.zh.md)
4
4
 
5
- A DeepSeek Harness (DSH) plugin for pending-change review: it automatically tracks every successful `edit` / `write` tool call, folds all unhandled changes into one entry per file, and shows each file's diff with Keep/Revert in the sidebar — no git involved.
5
+ [![npm version](https://img.shields.io/npm/v/dsh-diff-approval)](https://www.npmjs.com/package/dsh-diff-approval)
6
+ [![CI](https://img.shields.io/github/actions/workflow/status/9087/dsh-diff-approval/ci.yml)](https://github.com/9087/dsh-diff-approval/actions)
7
+
8
+ A DeepSeek Harness (DSH) plugin for pending-change review: it automatically tracks every successful `edit`, `write`, and editor (`str_replace_editor`) mutation, folds all unhandled changes into one entry per file, and shows each file's diff with Keep/Revert in the sidebar — no git involved.
9
+
10
+ ![Pending changes panel](docs/images/pending-panel.png)
6
11
 
7
12
  ## ✨ Features
8
13
 
9
- - **Diff view**: syntax-highlighted diff with +/− counts, change-block jump (previous/next diff), and drag-to-copy line references.
14
+ - **Diff view**: syntax-highlighted diff with +/− counts and change-block jump (previous/next diff). Select text in the diff and the bottom status bar shows its `file:start-end` reference — click it (or press `Ctrl+L`) to copy.
15
+ - **Highlight language**: the diff's status bar picks the highlight language — auto-detected from the file extension (shown as "Auto: …"), or overridden manually from the dropdown.
10
16
  - **Keep / Revert**: one decision per file. Keep accepts the changes; Revert writes the file back to its earliest basis (a created file is removed, a deleted tracked file is restored).
11
- - **Persistence**: pending state is stored per workspace at `<dshHome>/diff-approval/workspaces/<workspaceId>.json` and survives restarts.
17
+ - **Open / Reveal**: open a file in its default app, or reveal it in the file manager, right from the diff header.
18
+ - **Persistence**: pending state is stored per workspace at `<dshHome>/diff-approval/workspaces/<workspaceId>.json` and survives restarts — unhandled changes are still there when you come back, even in a fresh session.
12
19
 
13
20
  ## 📦 Install
14
21
 
22
+ If `dsh` is on your `PATH`:
23
+
15
24
  ```sh
16
25
  dsh plugin --profile web add dsh-diff-approval
17
26
  ```
18
27
 
28
+ Or, if you run the harness through npx (e.g. `npx @deepseek-ai/dsh web`):
29
+
30
+ ```sh
31
+ npx @deepseek-ai/dsh plugin --profile web add dsh-diff-approval
32
+ ```
33
+
19
34
  or manually: add the package to your profile's `package.json` dependencies and insert this row into the profile's `cordis.patch.yml`:
20
35
 
21
36
  ```yaml
@@ -34,12 +49,12 @@ Then restart `dsh web`.
34
49
 
35
50
  1. Work with the agent as usual — successful `edit` / `write` calls are recorded automatically.
36
51
  2. Click the **Pending changes** action at the sidebar footer (the badge shows the count).
37
- 3. Click a file row to expand its full diff; use **Previous diff** / **Next diff** to jump between change blocks.
52
+ 3. Click a file row to expand its full diff; use **Previous diff** / **Next diff** to jump between change blocks. Select text and click the reference in the bottom status bar (or press `Ctrl+L`) to copy a line reference.
38
53
  4. **Keep** accepts the file's changes; **Revert** restores its earliest basis. The row then leaves the list.
39
54
 
40
55
  ## 📝 Notes
41
56
 
42
- - Only `edit` and `write` are tracked. Deletions made outside these tools (e.g. shell `rm`) are sensed only for tracked files: the entry turns "File is gone" and its Revert restores the file.
57
+ - Only tracked mutations (`edit`, `write`, and `str_replace_editor` editor calls) are recorded. Deletions made outside these tools (e.g. shell `rm`) are sensed only for tracked files: the entry turns "File is gone" and its Revert restores the file.
43
58
  - Reverting a created file deletes it (the fs seam has no delete API).
44
59
  - Sessions with no workspace keep their entries memory-only; corrupt persistence files are rejected and can be deleted to reset.
45
60
 
package/README.zh.md CHANGED
@@ -2,20 +2,35 @@
2
2
 
3
3
  [English](README.md) | 中文
4
4
 
5
- 一个 DeepSeek Harness(DSH)待处理改动审核插件:自动跟踪每次成功的 `edit` / `write` 工具调用,把一个文件的全部未处理改动合并为一条待处理条目,在侧边栏展示每个文件的完整差异,支持保留 / 回退——全程不依赖 git。
5
+ [![npm 版本](https://img.shields.io/npm/v/dsh-diff-approval)](https://www.npmjs.com/package/dsh-diff-approval)
6
+ [![CI](https://img.shields.io/github/actions/workflow/status/9087/dsh-diff-approval/ci.yml)](https://github.com/9087/dsh-diff-approval/actions)
7
+
8
+ 一个 DeepSeek Harness(DSH)待处理改动审核插件:自动跟踪每次成功的 `edit` / `write` / 编辑器(`str_replace_editor`)改动,把一个文件的全部未处理改动合并为一条待处理条目,在侧边栏展示每个文件的完整差异,支持保留 / 回退——全程不依赖 git。
9
+
10
+ ![待处理改动面板](docs/images/pending-panel.zh.png)
6
11
 
7
12
  ## ✨ 功能
8
13
 
9
- - **差异视图**:语法高亮与 +/− 统计,按差异块跳转(上一处 / 下一处),拖选即可复制行引用。
14
+ - **差异视图**:语法高亮与 +/− 统计,按差异块跳转(上一处 / 下一处)。选中 diff 文本后,底部状态栏显示 `文件:起-止` 引用——点击(或按 `Ctrl+L`)即可复制。
15
+ - **高亮语言**:diff 底部状态栏可选高亮语言——默认按扩展名自动检测(显示为「自动:…」),也可手动从下拉切换。
10
16
  - **保留 / 回退**:每个文件一次决定。保留即接受;回退把文件写回最早基准(新建文件则删除,已被删除的跟踪文件则恢复)。
11
- - **持久化**:待处理状态按 workspace 落盘至 `<dshHome>/diff-approval/workspaces/<workspaceId>.json`,跨重启保留。
17
+ - **打开 / 定位**:从 diff 头部即可在默认应用中打开文件,或在文件管理器中定位它。
18
+ - **持久化**:待处理状态按 workspace 落盘至 `<dshHome>/diff-approval/workspaces/<workspaceId>.json`,跨重启保留——回来时未处理的改动仍在,即便换了新会话。
12
19
 
13
20
  ## 📦 安装
14
21
 
22
+ 若 `dsh` 已在 `PATH` 中:
23
+
15
24
  ```sh
16
25
  dsh plugin --profile web add dsh-diff-approval
17
26
  ```
18
27
 
28
+ 若通过 npx 启动 harness(如 `npx @deepseek-ai/dsh web`):
29
+
30
+ ```sh
31
+ npx @deepseek-ai/dsh plugin --profile web add dsh-diff-approval
32
+ ```
33
+
19
34
  或手动:把本包加入 profile 的 `package.json` 依赖,并在该 profile 的 `cordis.patch.yml` 中加入:
20
35
 
21
36
  ```yaml
@@ -34,12 +49,12 @@ dsh plugin --profile web add dsh-diff-approval
34
49
 
35
50
  1. 照常和 agent 对话——成功的 `edit` / `write` 调用会自动记录。
36
51
  2. 点击侧边栏底部的 **待处理改动** 入口(角标显示数量)。
37
- 3. 点击文件行展开该文件的完整差异,用 **上一处差异** / **下一处差异** 在差异块间跳转。
52
+ 3. 点击文件行展开该文件的完整差异,用 **上一处差异** / **下一处差异** 在差异块间跳转。选中文本后点击底部状态栏的引用(或按 `Ctrl+L`)即可复制行引用。
38
53
  4. **保留** 接受该文件的改动;**回退** 恢复其最早基准。处理后该行离开列表。
39
54
 
40
55
  ## 📝 注意事项
41
56
 
42
- - 只跟踪 `edit` `write`。通过这两个工具以外的删除(如 shell `rm`)只能对已跟踪的文件感知:条目显示"文件已不存在",回退即恢复。
57
+ - 只记录被跟踪的改动(`edit`、`write` 与编辑器 `str_replace_editor` 调用)。通过以上工具以外的删除(如 shell `rm`)只能对已跟踪的文件感知:条目显示"文件已不存在",回退即恢复。
43
58
  - 回退新建文件会删除该文件(文件系统接缝没有删除 API)。
44
59
  - 没有 workspace 的会话条目仅存内存;损坏的落盘文件会被拒绝,删除即可重置。
45
60
 
Binary file
Binary file
package/lib/client.js CHANGED
@@ -11333,6 +11333,8 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
11333
11333
  xml_default,
11334
11334
  yaml_default
11335
11335
  ];
11336
+ /** Supported grammar ids, in picker order, for the viewer's language selector. */
11337
+ const HIGHLIGHT_LANGS = [...new Set(LANGS.flat().map((lang) => lang.name))];
11336
11338
  /** All token colors resolve through `--shiki-*` custom properties (theme package sheets). */
11337
11339
  const cssVariablesTheme = createCssVariablesTheme({
11338
11340
  name: "css-variables",
@@ -11485,7 +11487,7 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
11485
11487
  * @param path - the path to shorten.
11486
11488
  * @returns the segment after the final separator.
11487
11489
  */
11488
- function basenameOf(path) {
11490
+ function basenameOf$1(path) {
11489
11491
  const normalized = path.replaceAll("\\", "/");
11490
11492
  return normalized.slice(normalized.lastIndexOf("/") + 1);
11491
11493
  }
@@ -11497,8 +11499,8 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
11497
11499
  * @returns the display path for a copied reference.
11498
11500
  */
11499
11501
  function copyDisplayPath(path, files) {
11500
- const base = basenameOf(path);
11501
- return files.some((file) => file.path !== path && basenameOf(file.path) === base) ? path : base;
11502
+ const base = basenameOf$1(path);
11503
+ return files.some((file) => file.path !== path && basenameOf$1(file.path) === base) ? path : base;
11502
11504
  }
11503
11505
  /**
11504
11506
  * Format one line range as a reference suffix: a single line number, or the
@@ -11522,8 +11524,8 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
11522
11524
  return `${copyDisplayPath(path, files)}:${lineRangeLabel(start, end)}`;
11523
11525
  }
11524
11526
  //#endregion
11525
- //#region \0dsh-css:C:\PROJECTS\dsh-diff-approval\src\client\PendingPanel.module.css.mjs
11526
- const css = ".Ay7oXG_layer{flex:none;align-items:center;width:100%;height:49px;margin:8px 0 0;display:flex;position:relative}.Ay7oXG_footerButtons{align-items:center;width:100%;display:flex}.Ay7oXG_badge{width:100%;height:49px;color:var(--dsw-alias-label-primary);cursor:pointer;background:0 0;border:none;border-radius:12px;align-items:center;gap:8px;padding:0 8px 0 6px;font-family:inherit;font-size:14px;display:inline-flex;overflow:hidden}.Ay7oXG_badge:hover{background:var(--dsw-alias-interactive-bg-hover-solid)}.Ay7oXG_badge[data-active]{background:var(--dsw-alias-interactive-bg-hover)}.Ay7oXG_badgeLabel{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.Ay7oXG_badgeCount{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;flex:none;margin-left:auto;font-size:12px;line-height:16px}.Ay7oXG_layer.Ay7oXG_rail{width:36px;height:36px;margin:0}.Ay7oXG_rail .Ay7oXG_badge{border-radius:50%;justify-content:center;gap:0;width:36px;height:36px;padding:0}.Ay7oXG_rail .Ay7oXG_badgeLabel{display:none}.Ay7oXG_rail .Ay7oXG_badgeCount{box-sizing:border-box;background:var(--dsw-alias-state-business-primary);min-width:18px;height:18px;color:var(--dsw-alias-label-primary-foreground);font-variant-numeric:tabular-nums;border-radius:9px;justify-content:center;align-items:center;padding:0 4px;font-size:11px;line-height:18px;display:flex;position:absolute;top:-3px;right:-7px}.Ay7oXG_panel{z-index:30;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-base);width:auto;max-width:none;box-shadow:var(--dsw-shadow-lv2);--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);border-radius:12px;flex-direction:column;display:flex;position:fixed;inset:8px 8px 128px;overflow:hidden}.Ay7oXG_header{box-sizing:border-box;border-bottom:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base);flex:none;justify-content:space-between;align-items:center;min-height:44px;padding:10px 12px;display:flex}.Ay7oXG_states{flex:1;min-height:0;padding:4px 12px 12px;overflow-y:auto}.Ay7oXG_split{flex:1;align-items:stretch;min-height:0;display:flex}.Ay7oXG_fileList{box-sizing:border-box;border-right:1px solid var(--dsw-alias-border-l2);flex:none;width:240px;min-height:0;padding:4px 8px 12px;overflow-y:auto}.Ay7oXG_resizeHandle{cursor:col-resize;background:0 0;flex:none;width:5px;margin:0 -2px}.Ay7oXG_resizeHandle:hover{background:var(--dsw-alias-border-l2)}.Ay7oXG_detail{flex-direction:column;flex:1;min-width:0;min-height:0;display:flex}.Ay7oXG_detailEmpty{color:var(--dsw-alias-label-tertiary);text-align:center;flex:1;justify-content:center;align-items:center;margin:0;padding:24px;font-size:12px;line-height:18px;display:flex}.Ay7oXG_title{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500;line-height:20px}.Ay7oXG_note,.Ay7oXG_readError,.Ay7oXG_hint{color:var(--dsw-alias-label-tertiary);margin:4px 0;font-size:12px;line-height:18px}.Ay7oXG_readError{color:var(--dsw-alias-state-error-primary)}.Ay7oXG_group{color:var(--dsw-alias-label-tertiary);margin:8px 0 4px;font-size:12px;font-weight:500;line-height:16px}.Ay7oXG_rows{margin:0;padding:0;list-style:none}.Ay7oXG_row{border:1px solid #0000;border-radius:10px;margin:2px 0}.Ay7oXG_rowHead{width:100%;color:var(--dsw-alias-label-primary);font:inherit;text-align:left;cursor:pointer;background:0 0;border:none;border-radius:10px;align-items:baseline;gap:8px;padding:6px 8px;display:flex}.Ay7oXG_rowHead:hover{background:var(--dsw-alias-interactive-bg-hover-solid)}.Ay7oXG_rowHead[data-selected]{background:var(--dsw-alias-interactive-bg-hover)}.Ay7oXG_rowPath{text-overflow:ellipsis;white-space:nowrap;min-width:0;font:var(--dsw-font-markdown-code-block);text-align:left;direction:rtl;overflow:hidden}.Ay7oXG_kindTag{border:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-tertiary);white-space:nowrap;border-radius:6px;flex:none;padding:1px 6px;font-size:11px;line-height:14px}.Ay7oXG_kindHint{color:var(--dsw-alias-label-tertiary);flex:none;font-size:12px;line-height:16px}.Ay7oXG_divergedHint,.Ay7oXG_missingHint{border-bottom:1px solid var(--dsw-alias-border-l2);margin:0;padding:6px 8px;font-size:12px;line-height:18px}.Ay7oXG_missing{border:1px solid var(--dsw-alias-state-warn-primary);color:var(--dsw-alias-state-warn-label,var(--dsw-alias-state-warn-primary));white-space:nowrap;border-radius:6px;flex:none;padding:1px 6px;font-size:11px;line-height:14px}.Ay7oXG_missingHint{color:var(--dsw-alias-state-warn-label,var(--dsw-alias-state-warn-primary))}.Ay7oXG_rowMeta{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;flex:none;gap:6px;margin-left:auto;font-size:12px;line-height:16px;display:inline-flex}.Ay7oXG_addCount{color:color-mix(in srgb, var(--dsw-alias-state-success-primary) 60%, var(--dsw-alias-label-primary))}.Ay7oXG_delCount{color:color-mix(in srgb, var(--dsw-alias-state-error-primary) 60%, var(--dsw-alias-label-primary))}.Ay7oXG_diff{background:var(--dsw-alias-markdown-code-block);flex-direction:column;flex:1;min-height:0;display:flex;position:relative;overflow:hidden}.Ay7oXG_diffActions{border-bottom:1px solid var(--dsw-alias-border-l2);align-items:center;gap:8px;padding:6px 8px;display:flex}.Ay7oXG_diffStats{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;font-size:12px;line-height:16px}.Ay7oXG_flexSpacer{flex:1}.Ay7oXG_action{border:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-primary);cursor:pointer;background:0 0;border-radius:8px;padding:3px 12px;font-family:inherit;font-size:12px;line-height:18px}.Ay7oXG_action:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.Ay7oXG_action:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}.Ay7oXG_iconAction{justify-content:center;align-items:center;padding:4px 6px;display:inline-flex}.Ay7oXG_diffBody{min-height:0;font:var(--dsw-font-markdown-code-block);flex:1;padding:8px 10px;overflow:auto}.Ay7oXG_lines{border-spacing:0;width:max-content;min-width:100%;display:table}.Ay7oXG_line{user-select:none;display:table-row}.Ay7oXG_gutter{box-sizing:border-box;text-align:right;width:44px;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;user-select:none;padding-right:10px;display:table-cell}.Ay7oXG_code{white-space:pre;display:table-cell}.Ay7oXG_context{color:var(--dsw-alias-label-primary)}.Ay7oXG_del{background-color:color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent)}.Ay7oXG_add{background-color:color-mix(in srgb, var(--dsw-alias-state-success-primary) 10%, transparent)}.Ay7oXG_toolbar{z-index:2;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-base);box-shadow:var(--dsw-shadow-lv2);border-radius:8px;gap:4px;padding:3px;display:flex;position:absolute;right:10px}.Ay7oXG_toolbarButton{color:var(--dsw-alias-label-primary);white-space:nowrap;cursor:pointer;background:0 0;border:none;border-radius:6px;padding:3px 10px;font-family:inherit;font-size:12px;line-height:18px}.Ay7oXG_toolbarButton:hover{background:var(--dsw-alias-interactive-bg-hover)}";
11527
+ //#region \0dsh-css:/home/runner/work/dsh-diff-approval/dsh-diff-approval/src/client/PendingPanel.module.css.mjs
11528
+ const css = ".F1KBNa_layer{flex:none;align-items:center;width:calc(100% + 8px);height:34px;margin:4px -4px;display:flex;position:relative}.F1KBNa_footerButtons{align-items:center;width:100%;display:flex}.F1KBNa_badge{box-sizing:border-box;width:100%;height:34px;color:var(--dsw-alias-label-primary);cursor:pointer;background:0 0;border:none;border-radius:12px;align-items:center;gap:8px;padding:6px 2px 6px 10px;font-family:inherit;font-size:14px;line-height:22px;display:flex;overflow:hidden}.F1KBNa_badge:hover{background:var(--dsw-alias-interactive-bg-hover)}.F1KBNa_badge[data-active]{background:var(--dsw-alias-interactive-bg-hover-solid)}.F1KBNa_badgeLabel{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.F1KBNa_badgeCount{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;flex:none;margin-left:auto;margin-right:6px;font-size:12px;line-height:16px}.F1KBNa_layer.F1KBNa_rail{width:36px;height:36px;margin:8px 0 10px}.F1KBNa_rail .F1KBNa_badge{border-radius:50%;justify-content:center;gap:0;width:36px;height:36px;padding:0}.F1KBNa_rail .F1KBNa_badgeLabel{display:none}.F1KBNa_rail .F1KBNa_badgeCount{box-sizing:border-box;background:var(--dsw-alias-state-business-primary);min-width:18px;height:18px;color:var(--dsw-alias-label-primary-foreground);font-variant-numeric:tabular-nums;border-radius:9px;justify-content:center;align-items:center;padding:0 4px;font-size:11px;line-height:18px;display:flex;position:absolute;top:-3px;right:-7px}.F1KBNa_panel{z-index:30;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-base);width:auto;max-width:none;box-shadow:var(--dsw-shadow-lv2);--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);border-radius:12px;flex-direction:column;display:flex;position:fixed;inset:8px 8px 128px;overflow:hidden}.F1KBNa_header{box-sizing:border-box;border-bottom:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base);flex:none;justify-content:space-between;align-items:center;min-height:44px;padding:10px 12px;display:flex}.F1KBNa_states{flex:1;min-height:0;padding:4px 12px 12px;overflow-y:auto}.F1KBNa_split{flex:1;align-items:stretch;min-height:0;display:flex}.F1KBNa_fileList{box-sizing:border-box;border-right:1px solid var(--dsw-alias-border-l2);flex:none;width:240px;min-height:0;padding:4px 8px 12px;overflow-y:auto}.F1KBNa_resizeHandle{cursor:col-resize;background:0 0;flex:none;width:5px;margin:0 -2px}.F1KBNa_resizeHandle:hover{background:var(--dsw-alias-border-l2)}.F1KBNa_detail{flex-direction:column;flex:1;min-width:0;min-height:0;display:flex}.F1KBNa_detailEmpty{color:var(--dsw-alias-label-tertiary);text-align:center;flex:1;justify-content:center;align-items:center;margin:0;padding:24px;font-size:12px;line-height:18px;display:flex}.F1KBNa_title{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500;line-height:20px}.F1KBNa_note,.F1KBNa_readError,.F1KBNa_hint{color:var(--dsw-alias-label-tertiary);margin:4px 0;font-size:12px;line-height:18px}.F1KBNa_readError{color:var(--dsw-alias-state-error-primary)}.F1KBNa_group{color:var(--dsw-alias-label-tertiary);margin:8px 0 4px;font-size:12px;font-weight:500;line-height:16px}.F1KBNa_rows{margin:0;padding:0;list-style:none}.F1KBNa_row{border:1px solid #0000;border-radius:10px;margin:2px 0}.F1KBNa_rowHead{width:100%;color:var(--dsw-alias-label-primary);font:inherit;text-align:left;cursor:pointer;background:0 0;border:none;border-radius:10px;align-items:baseline;gap:8px;padding:6px 8px;display:flex}.F1KBNa_rowHead:hover{background:var(--dsw-alias-interactive-bg-hover-solid)}.F1KBNa_rowHead[data-selected]{background:var(--dsw-alias-interactive-bg-hover)}.F1KBNa_rowPath{text-overflow:ellipsis;white-space:nowrap;min-width:0;font:var(--dsw-font-markdown-code-block);overflow:hidden}.F1KBNa_kindTag{border:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-tertiary);white-space:nowrap;border-radius:6px;flex:none;padding:1px 6px;font-size:11px;line-height:14px}.F1KBNa_kindHint{color:var(--dsw-alias-label-tertiary);flex:none;font-size:12px;line-height:16px}.F1KBNa_divergedHint,.F1KBNa_missingHint{border-bottom:1px solid var(--dsw-alias-border-l2);margin:0;padding:6px 8px;font-size:12px;line-height:18px}.F1KBNa_missing{border:1px solid var(--dsw-alias-state-warn-primary);color:var(--dsw-alias-state-warn-label,var(--dsw-alias-state-warn-primary));white-space:nowrap;border-radius:6px;flex:none;padding:1px 6px;font-size:11px;line-height:14px}.F1KBNa_missingHint{color:var(--dsw-alias-state-warn-label,var(--dsw-alias-state-warn-primary))}.F1KBNa_rowMeta{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;flex:none;gap:6px;margin-left:auto;font-size:12px;line-height:16px;display:inline-flex}.F1KBNa_addCount{color:color-mix(in srgb, var(--dsw-alias-state-success-primary) 60%, var(--dsw-alias-label-primary))}.F1KBNa_delCount{color:color-mix(in srgb, var(--dsw-alias-state-error-primary) 60%, var(--dsw-alias-label-primary))}.F1KBNa_diff{background:var(--dsw-alias-markdown-code-block);flex-direction:column;flex:1;min-height:0;display:flex;position:relative;overflow:hidden}.F1KBNa_diffHeader{border-bottom:1px solid var(--dsw-alias-border-l2);align-items:center;gap:8px;padding:6px 8px;display:flex}.F1KBNa_diffPath{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-primary);font:var(--dsw-font-markdown-code-block);flex:1;font-size:12px;line-height:18px;overflow:hidden}.F1KBNa_diffActions{border-bottom:1px solid var(--dsw-alias-border-l2);align-items:center;gap:8px;padding:6px 8px;display:flex}.F1KBNa_diffStats{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;font-size:12px;line-height:16px}.F1KBNa_flexSpacer{flex:1}.F1KBNa_action{border:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-primary);cursor:pointer;background:0 0;border-radius:8px;padding:3px 12px;font-family:inherit;font-size:12px;line-height:18px}.F1KBNa_action:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.F1KBNa_action:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}.F1KBNa_iconAction{justify-content:center;align-items:center;padding:4px 6px;display:inline-flex}.F1KBNa_close{cursor:pointer;width:28px;height:28px;color:var(--dsw-alias-label-tertiary);background:0 0;border:none;border-radius:28px;justify-content:center;align-items:center;padding:0;display:inline-flex}.F1KBNa_close:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.F1KBNa_diffBody{min-height:0;font:var(--dsw-font-markdown-code-block);flex:1;padding:8px 10px;overflow:auto}.F1KBNa_lines{border-spacing:0;width:max-content;min-width:100%;display:table}.F1KBNa_line{display:table-row}.F1KBNa_gutter{box-sizing:border-box;text-align:right;width:44px;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;user-select:none;padding-right:10px;display:table-cell}.F1KBNa_code{white-space:pre;display:table-cell}.F1KBNa_context{color:var(--dsw-alias-label-primary)}.F1KBNa_del{background-color:color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent)}.F1KBNa_add{background-color:color-mix(in srgb, var(--dsw-alias-state-success-primary) 10%, transparent)}.F1KBNa_statusBar{box-sizing:border-box;border-top:1px solid var(--dsw-alias-border-l2);height:34px;color:var(--dsw-alias-label-tertiary);font-family:var(--dsw-font-markdown-code-block);font-variant-numeric:tabular-nums;flex:none;align-items:center;padding:0 8px;font-size:12px;line-height:18px;display:flex}.F1KBNa_statusAction{border:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-primary);white-space:nowrap;cursor:pointer;background:0 0;border-radius:8px;padding:3px 12px;font-family:inherit;font-size:12px;line-height:18px}.F1KBNa_statusAction:hover{background:var(--dsw-alias-interactive-bg-hover)}.F1KBNa_langSelect{border:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-primary);cursor:pointer;background:0 0;border-radius:8px;flex:none;align-items:center;gap:4px;padding:3px 12px;font-family:inherit;font-size:12px;line-height:18px;display:inline-flex}.F1KBNa_langSelect:hover{background:var(--dsw-alias-interactive-bg-hover)}.F1KBNa_langLabel{text-overflow:ellipsis;white-space:nowrap;max-width:140px;overflow:hidden}";
11527
11529
  const tagId = "dsh-diff-approval/PendingPanel.module.css";
11528
11530
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
11529
11531
  const tag = document.createElement("style");
@@ -11533,53 +11535,58 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
11533
11535
  document.head.appendChild(tag);
11534
11536
  }
11535
11537
  var PendingPanel_module_css_default = {
11536
- "panel": "Ay7oXG_panel",
11537
- "note": "Ay7oXG_note",
11538
- "code": "Ay7oXG_code",
11539
- "rowHead": "Ay7oXG_rowHead",
11540
- "line": "Ay7oXG_line",
11541
- "diffActions": "Ay7oXG_diffActions",
11542
- "diff": "Ay7oXG_diff",
11543
- "rowPath": "Ay7oXG_rowPath",
11544
- "missing": "Ay7oXG_missing",
11545
- "add": "Ay7oXG_add",
11546
- "badge": "Ay7oXG_badge",
11547
- "badgeCount": "Ay7oXG_badgeCount",
11548
- "row": "Ay7oXG_row",
11549
- "kindHint": "Ay7oXG_kindHint",
11550
- "badgeLabel": "Ay7oXG_badgeLabel",
11551
- "resizeHandle": "Ay7oXG_resizeHandle",
11552
- "iconAction": "Ay7oXG_iconAction",
11553
- "gutter": "Ay7oXG_gutter",
11554
- "diffStats": "Ay7oXG_diffStats",
11555
- "missingHint": "Ay7oXG_missingHint",
11556
- "states": "Ay7oXG_states",
11557
- "layer": "Ay7oXG_layer",
11558
- "detail": "Ay7oXG_detail",
11559
- "readError": "Ay7oXG_readError",
11560
- "del": "Ay7oXG_del",
11561
- "detailEmpty": "Ay7oXG_detailEmpty",
11562
- "title": "Ay7oXG_title",
11563
- "hint": "Ay7oXG_hint",
11564
- "rowMeta": "Ay7oXG_rowMeta",
11565
- "rows": "Ay7oXG_rows",
11566
- "divergedHint": "Ay7oXG_divergedHint",
11567
- "fileList": "Ay7oXG_fileList",
11568
- "addCount": "Ay7oXG_addCount",
11569
- "split": "Ay7oXG_split",
11570
- "rail": "Ay7oXG_rail",
11571
- "delCount": "Ay7oXG_delCount",
11572
- "kindTag": "Ay7oXG_kindTag",
11573
- "action": "Ay7oXG_action",
11574
- "diffBody": "Ay7oXG_diffBody",
11575
- "toolbar": "Ay7oXG_toolbar",
11576
- "toolbarButton": "Ay7oXG_toolbarButton",
11577
- "header": "Ay7oXG_header",
11578
- "footerButtons": "Ay7oXG_footerButtons",
11579
- "context": "Ay7oXG_context",
11580
- "group": "Ay7oXG_group",
11581
- "flexSpacer": "Ay7oXG_flexSpacer",
11582
- "lines": "Ay7oXG_lines"
11538
+ "lines": "F1KBNa_lines",
11539
+ "badge": "F1KBNa_badge",
11540
+ "readError": "F1KBNa_readError",
11541
+ "hint": "F1KBNa_hint",
11542
+ "diffStats": "F1KBNa_diffStats",
11543
+ "flexSpacer": "F1KBNa_flexSpacer",
11544
+ "code": "F1KBNa_code",
11545
+ "close": "F1KBNa_close",
11546
+ "diffPath": "F1KBNa_diffPath",
11547
+ "split": "F1KBNa_split",
11548
+ "missingHint": "F1KBNa_missingHint",
11549
+ "add": "F1KBNa_add",
11550
+ "diffBody": "F1KBNa_diffBody",
11551
+ "rowHead": "F1KBNa_rowHead",
11552
+ "diffActions": "F1KBNa_diffActions",
11553
+ "kindHint": "F1KBNa_kindHint",
11554
+ "badgeLabel": "F1KBNa_badgeLabel",
11555
+ "fileList": "F1KBNa_fileList",
11556
+ "rowMeta": "F1KBNa_rowMeta",
11557
+ "del": "F1KBNa_del",
11558
+ "langSelect": "F1KBNa_langSelect",
11559
+ "langLabel": "F1KBNa_langLabel",
11560
+ "title": "F1KBNa_title",
11561
+ "context": "F1KBNa_context",
11562
+ "rows": "F1KBNa_rows",
11563
+ "divergedHint": "F1KBNa_divergedHint",
11564
+ "states": "F1KBNa_states",
11565
+ "statusAction": "F1KBNa_statusAction",
11566
+ "kindTag": "F1KBNa_kindTag",
11567
+ "footerButtons": "F1KBNa_footerButtons",
11568
+ "gutter": "F1KBNa_gutter",
11569
+ "addCount": "F1KBNa_addCount",
11570
+ "line": "F1KBNa_line",
11571
+ "statusBar": "F1KBNa_statusBar",
11572
+ "detail": "F1KBNa_detail",
11573
+ "missing": "F1KBNa_missing",
11574
+ "resizeHandle": "F1KBNa_resizeHandle",
11575
+ "diffHeader": "F1KBNa_diffHeader",
11576
+ "group": "F1KBNa_group",
11577
+ "rail": "F1KBNa_rail",
11578
+ "iconAction": "F1KBNa_iconAction",
11579
+ "header": "F1KBNa_header",
11580
+ "row": "F1KBNa_row",
11581
+ "delCount": "F1KBNa_delCount",
11582
+ "action": "F1KBNa_action",
11583
+ "detailEmpty": "F1KBNa_detailEmpty",
11584
+ "badgeCount": "F1KBNa_badgeCount",
11585
+ "layer": "F1KBNa_layer",
11586
+ "rowPath": "F1KBNa_rowPath",
11587
+ "panel": "F1KBNa_panel",
11588
+ "note": "F1KBNa_note",
11589
+ "diff": "F1KBNa_diff"
11583
11590
  };
11584
11591
  //#endregion
11585
11592
  //#region lib/types/client/PendingPanel.js
@@ -11595,11 +11602,25 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
11595
11602
  const FALLBACK_BOTTOM_PX = 128;
11596
11603
  /** Gap kept between the composer's top edge and the panel bottom, in px. */
11597
11604
  const COMPOSER_GAP_PX = 12;
11598
- /** The harness composer's stable hook (ui-conversation InputBar). */
11599
- const COMPOSER_SELECTOR = "[data-composer-card]";
11605
+ /** The harness composer seat (conversation scroll body + seat div). */
11606
+ const SCROLL_SELECTOR = "[data-conversation-scroll]";
11607
+ const SEAT_SELECTOR = "[data-composer-seat]";
11608
+ /** Seat height ui-conversation publishes for floating controls (its own seat observer). */
11609
+ const COMPOSER_HEIGHT_VAR = "--dsh-composer-height";
11610
+ /** Interactive composer/approval cards; clicking these keeps the panel open.
11611
+ Deliberately the cards themselves, not the seat: the approval frame's wide
11612
+ side gutters are blank space, so a click there must still close. */
11613
+ const KEEP_OPEN_SELECTOR = "[data-composer-card],[data-question-key] > *,[data-plan-review-key] > *";
11614
+ /** Seat counts as docked when its bottom is this close to the window bottom. */
11615
+ const DOCKED_TOLERANCE_PX = 48;
11600
11616
  /** File-list pane width bounds for the manual split drag, in px. */
11601
11617
  const MIN_LIST_WIDTH_PX = 160;
11602
11618
  const MAX_LIST_WIDTH_PX = 560;
11619
+ /** The trailing file-name segment of a path, used for row display. */
11620
+ function basenameOf(path) {
11621
+ const index = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
11622
+ return index < 0 ? path : path.slice(index + 1);
11623
+ }
11603
11624
  /** The diff body's row class per line kind. */
11604
11625
  const ROW_CLASS = {
11605
11626
  context: PendingPanel_module_css_default.context,
@@ -11627,8 +11648,66 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
11627
11648
  });
11628
11649
  return blocks;
11629
11650
  }
11651
+ /** The diff-row index containing a node, or undefined. */
11652
+ function rowIndexAt(node) {
11653
+ if (node === null) return void 0;
11654
+ const row = (node instanceof Element ? node : node.parentElement)?.closest("[data-diff-row]");
11655
+ if (row === null || row === void 0) return void 0;
11656
+ const index = Number(row.dataset.diffRow);
11657
+ return Number.isFinite(index) ? index : void 0;
11658
+ }
11659
+ /** Character offset of a selection boundary within its line's code text. */
11660
+ function lineOffsetAt(node, offset) {
11661
+ const code = (node instanceof Element ? node : node.parentElement)?.closest("[data-diff-code]");
11662
+ if (code === null || code === void 0) return 0;
11663
+ let before = 0;
11664
+ const walker = document.createTreeWalker(code, NodeFilter.SHOW_TEXT);
11665
+ let current = walker.nextNode();
11666
+ while (current !== null) {
11667
+ if (current === node) return before + offset;
11668
+ if (node instanceof Element && node.contains(current)) break;
11669
+ before += current.length;
11670
+ current = walker.nextNode();
11671
+ }
11672
+ if (node instanceof Element) {
11673
+ const children = [...node.childNodes];
11674
+ for (let i = 0; i < Math.min(offset, children.length); i++) {
11675
+ const inner = document.createTreeWalker(children[i], NodeFilter.SHOW_TEXT);
11676
+ let text = inner.nextNode();
11677
+ while (text !== null) {
11678
+ before += text.length;
11679
+ text = inner.nextNode();
11680
+ }
11681
+ }
11682
+ }
11683
+ return before;
11684
+ }
11685
+ /** Length of the code text on the line holding a node. */
11686
+ function lineLengthAt(node) {
11687
+ return ((node instanceof Element ? node : node.parentElement)?.closest("[data-diff-code]"))?.textContent?.length ?? 0;
11688
+ }
11689
+ /**
11690
+ * Derive the selected diff-row range from a native text selection. A
11691
+ * boundary sitting exactly at a line edge contributes no content: a start at
11692
+ * the line's end skips to the next line, an end at the line's start falls
11693
+ * back to the previous line.
11694
+ */
11695
+ function rowRangeOf(selection) {
11696
+ if (selection === null || selection.isCollapsed || selection.rangeCount === 0) return void 0;
11697
+ const range = selection.getRangeAt(0);
11698
+ let start = rowIndexAt(range.startContainer);
11699
+ let end = rowIndexAt(range.endContainer);
11700
+ if (start === void 0 || end === void 0) return void 0;
11701
+ if (lineOffsetAt(range.startContainer, range.startOffset) >= lineLengthAt(range.startContainer)) start += 1;
11702
+ if (lineOffsetAt(range.endContainer, range.endOffset) === 0) end -= 1;
11703
+ if (start > end) return void 0;
11704
+ return {
11705
+ start,
11706
+ end
11707
+ };
11708
+ }
11630
11709
  /** One row of the file list: the clickable head in the left pane. */
11631
- function PendingFileRow({ file, files, selected, t, onSelect }) {
11710
+ function PendingFileRow({ file, selected, t, onSelect }) {
11632
11711
  const stats = (0, react.useMemo)(() => computeWholeFileDiff(file.oldText, file.newText), [file.oldText, file.newText]);
11633
11712
  return (0, react_jsx_runtime.jsx)("li", {
11634
11713
  className: PendingPanel_module_css_default.row,
@@ -11637,13 +11716,17 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
11637
11716
  className: PendingPanel_module_css_default.rowHead,
11638
11717
  "data-selected": selected || void 0,
11639
11718
  onClick: () => {
11640
- onSelect(selected ? "" : file.id);
11719
+ onSelect(file.id);
11641
11720
  },
11642
11721
  children: [
11643
- (0, react_jsx_runtime.jsx)("span", {
11644
- className: PendingPanel_module_css_default.rowPath,
11645
- title: file.path,
11646
- children: copyDisplayPath(file.path, files)
11722
+ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
11723
+ label: file.path,
11724
+ delayMs: 500,
11725
+ maxWidth: 560,
11726
+ children: (0, react_jsx_runtime.jsx)("span", {
11727
+ className: PendingPanel_module_css_default.rowPath,
11728
+ children: basenameOf(file.path)
11729
+ })
11647
11730
  }),
11648
11731
  file.kind === "create" && (0, react_jsx_runtime.jsx)("span", {
11649
11732
  className: PendingPanel_module_css_default.kindTag,
@@ -11670,16 +11753,39 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
11670
11753
  }
11671
11754
  /** The selected file's diff, actions, jump controls, and copy toolbar. */
11672
11755
  function PendingDiff({ file, files, busy, t, onKeep, onRevert, onOpen }) {
11673
- const lang = (0, react.useMemo)(() => langFromPath(file.path), [file.path]);
11756
+ const [langOverride, setLangOverride] = (0, react.useState)(void 0);
11757
+ const [langMenuOpen, setLangMenuOpen] = (0, react.useState)(false);
11758
+ const langMenuItems = (0, react.useMemo)(() => [{
11759
+ id: "",
11760
+ label: t("action.langAuto")
11761
+ }, ...HIGHLIGHT_LANGS.map((language) => ({
11762
+ id: language,
11763
+ label: language
11764
+ }))], [t]);
11765
+ const detectedLang = (0, react.useMemo)(() => langFromPath(file.path), [file.path]);
11766
+ const lang = (0, react.useMemo)(() => langOverride ?? detectedLang, [detectedLang, langOverride]);
11767
+ const langLabel = langOverride ?? (detectedLang === void 0 ? t("action.langAuto") : t("action.langAutoDetected", { lang: detectedLang }));
11674
11768
  const model = (0, react.useMemo)(() => {
11675
11769
  const diff = computeWholeFileDiff(file.oldText, file.newText);
11676
11770
  return {
11677
11771
  diff,
11678
- blocks: changeBlocksOf(diff),
11679
- oldRuns: highlightLines(file.oldText, lang),
11680
- newRuns: highlightLines(file.newText, lang)
11772
+ blocks: changeBlocksOf(diff)
11773
+ };
11774
+ }, [file.oldText, file.newText]);
11775
+ const [runs, setRuns] = (0, react.useState)(void 0);
11776
+ (0, react.useEffect)(() => {
11777
+ setRuns(void 0);
11778
+ const timer = window.setTimeout(() => {
11779
+ setRuns({
11780
+ oldRuns: highlightLines(file.oldText, lang),
11781
+ newRuns: highlightLines(file.newText, lang)
11782
+ });
11783
+ }, 0);
11784
+ return () => {
11785
+ window.clearTimeout(timer);
11681
11786
  };
11682
11787
  }, [
11788
+ file.id,
11683
11789
  file.oldText,
11684
11790
  file.newText,
11685
11791
  lang
@@ -11687,13 +11793,13 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
11687
11793
  const rowRefs = (0, react.useRef)(/* @__PURE__ */ new Map());
11688
11794
  const bodyRef = (0, react.useRef)(null);
11689
11795
  const [focus, setFocus] = (0, react.useState)(0);
11690
- const [drag, setDrag] = (0, react.useState)();
11691
- const [range, setRange] = (0, react.useState)();
11796
+ const [selection, setSelection] = (0, react.useState)(void 0);
11692
11797
  const [copied, setCopied] = (0, react.useState)(false);
11693
11798
  (0, react.useEffect)(() => {
11694
11799
  setFocus(0);
11695
- setDrag(void 0);
11696
- setRange(void 0);
11800
+ setSelection(void 0);
11801
+ setLangOverride(void 0);
11802
+ setLangMenuOpen(false);
11697
11803
  setCopied(false);
11698
11804
  }, [file.id]);
11699
11805
  (0, react.useEffect)(() => {
@@ -11720,68 +11826,48 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
11720
11826
  if (element === null) rowRefs.current.delete(index);
11721
11827
  else rowRefs.current.set(index, element);
11722
11828
  };
11723
- const rowMouseDown = (index) => (event) => {
11724
- if (event.button !== 0) return;
11725
- event.preventDefault();
11726
- setDrag({
11727
- anchor: index,
11728
- end: index
11729
- });
11730
- setRange({
11731
- start: index,
11732
- end: index
11733
- });
11734
- };
11735
- const rowMouseEnter = (index) => () => {
11736
- setDrag((current) => {
11737
- if (current === void 0) return current;
11738
- const next = {
11739
- ...current,
11740
- end: index
11741
- };
11742
- setRange({
11743
- start: Math.min(next.anchor, next.end),
11744
- end: Math.max(next.anchor, next.end)
11745
- });
11746
- return next;
11747
- });
11748
- };
11749
- const endDrag = () => {
11750
- setDrag((current) => {
11751
- if (current === void 0) return void 0;
11752
- setRange({
11753
- start: Math.min(current.anchor, current.end),
11754
- end: Math.max(current.anchor, current.end)
11755
- });
11756
- });
11757
- };
11758
- const selection = drag === void 0 && range !== void 0 ? range : void 0;
11759
- const selectionTop = (() => {
11760
- if (selection === void 0) return 0;
11761
- const row = rowRefs.current.get(selection.start);
11762
- const body = bodyRef.current;
11763
- if (row === void 0 || body === null) return 0;
11764
- const bodyTop = body.offsetTop;
11765
- const rowTop = bodyTop + row.getBoundingClientRect().top - body.getBoundingClientRect().top;
11766
- const clamped = Math.max(rowTop - 36, bodyTop);
11767
- return Math.min(clamped, Math.max(bodyTop, bodyTop + body.clientHeight - 36));
11768
- })();
11769
- const copySelection = async () => {
11770
- if (selection === void 0) return;
11829
+ (0, react.useEffect)(() => {
11830
+ const update = () => setSelection(rowRangeOf(window.getSelection()));
11831
+ document.addEventListener("selectionchange", update);
11832
+ update();
11833
+ return () => {
11834
+ document.removeEventListener("selectionchange", update);
11835
+ };
11836
+ }, [file.id]);
11837
+ const selectionReference = (() => {
11838
+ if (selection === void 0) return void 0;
11771
11839
  const lineNumbers = model.diff.rows.slice(selection.start, selection.end + 1).map((row) => row.newLine ?? row.oldLine).filter((number) => number !== void 0);
11772
- if (lineNumbers.length === 0) return;
11773
- if (!await (0, _deepseek_ai_dsh_client_ui_primitives.writeClipboard)(referenceOf(file.path, files, Math.min(...lineNumbers), Math.max(...lineNumbers)))) return;
11840
+ if (lineNumbers.length === 0) return void 0;
11841
+ return referenceOf(file.path, files, Math.min(...lineNumbers), Math.max(...lineNumbers));
11842
+ })();
11843
+ const copySelection = (0, react.useCallback)(async () => {
11844
+ if (selectionReference === void 0) return;
11845
+ if (!await (0, _deepseek_ai_dsh_client_ui_primitives.writeClipboard)(selectionReference)) return;
11774
11846
  setCopied(true);
11775
11847
  window.setTimeout(() => {
11776
11848
  setCopied(false);
11777
11849
  }, 1500);
11778
- };
11850
+ }, [selectionReference]);
11851
+ (0, react.useEffect)(() => {
11852
+ const onKeyDown = (event) => {
11853
+ if (!(event.ctrlKey || event.metaKey) || event.altKey || event.shiftKey) return;
11854
+ if (event.key.toLowerCase() !== "l") return;
11855
+ if (selectionReference === void 0) return;
11856
+ event.preventDefault();
11857
+ copySelection();
11858
+ };
11859
+ document.addEventListener("keydown", onKeyDown);
11860
+ return () => {
11861
+ document.removeEventListener("keydown", onKeyDown);
11862
+ };
11863
+ }, [copySelection]);
11779
11864
  /** One line's text: token spans when highlighted, plain text otherwise. */
11780
11865
  const renderLine = (row) => {
11781
11866
  const lineNumber = row.kind === "del" ? row.oldLine : row.newLine;
11782
- const runs = lineNumber === void 0 ? void 0 : (row.kind === "del" ? model.oldRuns : model.newRuns)?.[lineNumber - 1];
11783
- if (runs === void 0 || runs.length === 0) return row.text === "" ? "\xA0" : row.text;
11784
- return runs.map((span, index) => (0, react_jsx_runtime.jsx)("span", {
11867
+ const sideRuns = row.kind === "del" ? runs?.oldRuns : runs?.newRuns;
11868
+ const lineRuns = lineNumber === void 0 ? void 0 : sideRuns?.[lineNumber - 1];
11869
+ if (lineRuns === void 0 || lineRuns.length === 0) return row.text === "" ? "\xA0" : row.text;
11870
+ return lineRuns.map((span, index) => (0, react_jsx_runtime.jsx)("span", {
11785
11871
  style: span.style,
11786
11872
  children: span.text
11787
11873
  }, index));
@@ -11791,8 +11877,38 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
11791
11877
  return (0, react_jsx_runtime.jsxs)("div", {
11792
11878
  className: PendingPanel_module_css_default.diff,
11793
11879
  "data-diff-approval-diff": true,
11794
- onMouseUp: endDrag,
11795
11880
  children: [
11881
+ (0, react_jsx_runtime.jsxs)("div", {
11882
+ className: PendingPanel_module_css_default.diffHeader,
11883
+ children: [
11884
+ (0, react_jsx_runtime.jsx)("span", {
11885
+ className: PendingPanel_module_css_default.diffPath,
11886
+ children: file.path
11887
+ }),
11888
+ (0, react_jsx_runtime.jsx)("button", {
11889
+ type: "button",
11890
+ className: `${PendingPanel_module_css_default.action} ${PendingPanel_module_css_default.iconAction}`,
11891
+ "data-diff-open": true,
11892
+ "aria-label": t("action.openFile"),
11893
+ title: t("action.openFile"),
11894
+ onClick: () => {
11895
+ onOpen(file.sessionId, file.id, "open");
11896
+ },
11897
+ children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconBrowseOutline16, { size: 14 })
11898
+ }),
11899
+ (0, react_jsx_runtime.jsx)("button", {
11900
+ type: "button",
11901
+ className: `${PendingPanel_module_css_default.action} ${PendingPanel_module_css_default.iconAction}`,
11902
+ "data-diff-reveal": true,
11903
+ "aria-label": t("action.revealFile"),
11904
+ title: t("action.revealFile"),
11905
+ onClick: () => {
11906
+ onOpen(file.sessionId, file.id, "reveal");
11907
+ },
11908
+ children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderOpenOutline16, { size: 14 })
11909
+ })
11910
+ ]
11911
+ }),
11796
11912
  (0, react_jsx_runtime.jsxs)("div", {
11797
11913
  className: PendingPanel_module_css_default.diffActions,
11798
11914
  children: [
@@ -11830,24 +11946,6 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
11830
11946
  },
11831
11947
  children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, { size: 14 })
11832
11948
  })] }),
11833
- (0, react_jsx_runtime.jsx)("button", {
11834
- type: "button",
11835
- className: PendingPanel_module_css_default.action,
11836
- "data-diff-open": true,
11837
- onClick: () => {
11838
- onOpen(file.sessionId, file.id, "open");
11839
- },
11840
- children: t("action.openFile")
11841
- }),
11842
- (0, react_jsx_runtime.jsx)("button", {
11843
- type: "button",
11844
- className: PendingPanel_module_css_default.action,
11845
- "data-diff-reveal": true,
11846
- onClick: () => {
11847
- onOpen(file.sessionId, file.id, "reveal");
11848
- },
11849
- children: t("action.revealFile")
11850
- }),
11851
11949
  (0, react_jsx_runtime.jsx)("span", { className: PendingPanel_module_css_default.flexSpacer }),
11852
11950
  (0, react_jsx_runtime.jsx)("button", {
11853
11951
  type: "button",
@@ -11884,9 +11982,8 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
11884
11982
  ref: registerRow(index),
11885
11983
  className: `${PendingPanel_module_css_default.line} ${ROW_CLASS[row.kind]}`,
11886
11984
  "data-diff-line": row.kind,
11985
+ "data-diff-row": index,
11887
11986
  "data-diff-focused": inFocusedBlock(index) ? "" : void 0,
11888
- onMouseDown: rowMouseDown(index),
11889
- onMouseEnter: rowMouseEnter(index),
11890
11987
  children: [
11891
11988
  (0, react_jsx_runtime.jsx)("span", {
11892
11989
  className: PendingPanel_module_css_default.gutter,
@@ -11898,25 +11995,65 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
11898
11995
  }),
11899
11996
  (0, react_jsx_runtime.jsx)("span", {
11900
11997
  className: PendingPanel_module_css_default.code,
11998
+ "data-diff-code": true,
11901
11999
  children: renderLine(row)
11902
12000
  })
11903
12001
  ]
11904
12002
  }, index))
11905
12003
  })
11906
12004
  }),
11907
- selection !== void 0 && (0, react_jsx_runtime.jsx)("div", {
11908
- className: PendingPanel_module_css_default.toolbar,
11909
- style: { top: selectionTop },
11910
- "data-diff-selection-toolbar": true,
11911
- children: (0, react_jsx_runtime.jsx)("button", {
11912
- type: "button",
11913
- className: PendingPanel_module_css_default.toolbarButton,
11914
- "data-diff-copy": true,
11915
- onClick: () => {
11916
- copySelection();
11917
- },
11918
- children: copied ? t("action.copied") : t("action.copyRange")
11919
- })
12005
+ (0, react_jsx_runtime.jsxs)("div", {
12006
+ className: PendingPanel_module_css_default.statusBar,
12007
+ "data-diff-status-bar": true,
12008
+ children: [
12009
+ selectionReference === void 0 ? null : (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
12010
+ label: copied ? t("action.copied") : `${t("action.copyHint")} (Ctrl+L)`,
12011
+ side: "top",
12012
+ delayMs: 300,
12013
+ children: (0, react_jsx_runtime.jsx)("button", {
12014
+ type: "button",
12015
+ className: PendingPanel_module_css_default.statusAction,
12016
+ "data-diff-copy": true,
12017
+ onMouseDown: (event) => {
12018
+ event.preventDefault();
12019
+ },
12020
+ onClick: () => {
12021
+ copySelection();
12022
+ },
12023
+ children: copied ? t("action.copied") : selectionReference
12024
+ })
12025
+ }),
12026
+ (0, react_jsx_runtime.jsx)("span", { className: PendingPanel_module_css_default.flexSpacer }),
12027
+ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
12028
+ open: langMenuOpen,
12029
+ portal: true,
12030
+ compact: true,
12031
+ align: "end",
12032
+ items: langMenuItems,
12033
+ selectedId: langOverride ?? "",
12034
+ onSelect: (id) => {
12035
+ setLangOverride(id === "" ? void 0 : id);
12036
+ setLangMenuOpen(false);
12037
+ },
12038
+ onClose: () => {
12039
+ setLangMenuOpen(false);
12040
+ },
12041
+ anchor: (0, react_jsx_runtime.jsxs)("button", {
12042
+ type: "button",
12043
+ className: PendingPanel_module_css_default.langSelect,
12044
+ "data-diff-lang": true,
12045
+ "aria-label": t("action.langSelect"),
12046
+ title: t("action.langSelect"),
12047
+ onClick: () => {
12048
+ setLangMenuOpen((value) => !value);
12049
+ },
12050
+ children: [(0, react_jsx_runtime.jsx)("span", {
12051
+ className: PendingPanel_module_css_default.langLabel,
12052
+ children: langLabel
12053
+ }), (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, { size: 12 })]
12054
+ })
12055
+ })
12056
+ ]
11920
12057
  })
11921
12058
  ]
11922
12059
  });
@@ -11945,31 +12082,61 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
11945
12082
  }, [current, onRefresh]);
11946
12083
  (0, react.useEffect)(() => {
11947
12084
  if (!open) return;
12085
+ const MEASURE_INTERVAL_MS = 400;
11948
12086
  const measure = () => {
11949
- const composer = document.querySelector(COMPOSER_SELECTOR);
11950
- if (composer === null) {
11951
- setBottomPx(FALLBACK_BOTTOM_PX);
12087
+ const scrollers = document.querySelectorAll(SCROLL_SELECTOR);
12088
+ for (const scroller of scrollers) {
12089
+ const seat = scroller.querySelector(SEAT_SELECTOR);
12090
+ if (seat === null) continue;
12091
+ const seatRect = seat.getBoundingClientRect();
12092
+ if (!(seatRect.bottom >= window.innerHeight - DOCKED_TOLERANCE_PX && seatRect.top > 0 && seatRect.top < window.innerHeight)) continue;
12093
+ const height = Number.parseFloat(scroller.style.getPropertyValue(COMPOSER_HEIGHT_VAR));
12094
+ const clearance = Number.isFinite(height) && height > 0 ? height : window.innerHeight - seatRect.top;
12095
+ setBottomPx(Math.round(clearance) + COMPOSER_GAP_PX);
11952
12096
  return;
11953
12097
  }
11954
- const top = composer.getBoundingClientRect().top;
11955
- if (top <= 0) return;
11956
- setBottomPx(Math.round(window.innerHeight - top) + COMPOSER_GAP_PX);
12098
+ setBottomPx(FALLBACK_BOTTOM_PX);
11957
12099
  };
11958
12100
  measure();
11959
- if (typeof ResizeObserver === "undefined") return;
11960
- const composer = document.querySelector(COMPOSER_SELECTOR);
11961
- if (composer === null) return;
11962
- const observer = new ResizeObserver(measure);
11963
- observer.observe(composer);
12101
+ const seats = [...document.querySelectorAll(SEAT_SELECTOR)];
12102
+ const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(measure);
12103
+ for (const seat of seats) observer?.observe(seat);
12104
+ const timer = window.setInterval(measure, MEASURE_INTERVAL_MS);
11964
12105
  window.addEventListener("resize", measure);
11965
12106
  return () => {
11966
- observer.disconnect();
12107
+ observer?.disconnect();
12108
+ window.clearInterval(timer);
11967
12109
  window.removeEventListener("resize", measure);
11968
12110
  };
11969
12111
  }, [open]);
12112
+ (0, react.useEffect)(() => {
12113
+ if (!open) return;
12114
+ const onPointerDown = (event) => {
12115
+ const target = event.target;
12116
+ if (target === null) return;
12117
+ const element = target instanceof Element ? target : target.parentElement;
12118
+ if (element !== null && (element.closest("[data-diff-approval-panel]") !== null || element.closest("[data-diff-approval-badge]") !== null || element.closest(KEEP_OPEN_SELECTOR) !== null)) return;
12119
+ setOpen(false);
12120
+ };
12121
+ document.addEventListener("pointerdown", onPointerDown, true);
12122
+ return () => {
12123
+ document.removeEventListener("pointerdown", onPointerDown, true);
12124
+ };
12125
+ }, [open]);
11970
12126
  const files = snapshot.files;
11971
12127
  const mine = files.filter((file) => file.sessionId === current);
11972
12128
  const theirs = files.filter((file) => file.sessionId !== current);
12129
+ (0, react.useEffect)(() => {
12130
+ if (!open) return;
12131
+ if (selected !== "" && files.some((file) => file.id === selected)) return;
12132
+ const next = [...mine, ...theirs][0];
12133
+ if (next !== void 0 && next.id !== selected) setSelected(next.id);
12134
+ }, [
12135
+ open,
12136
+ current,
12137
+ files,
12138
+ selected
12139
+ ]);
11973
12140
  (0, react.useEffect)(() => {
11974
12141
  if (open && openedCount > 0 && snapshot.read && snapshot.error === void 0 && files.length === 0) {
11975
12142
  setOpenedCount(0);
@@ -11988,7 +12155,6 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
11988
12155
  };
11989
12156
  const renderEntry = (entry) => (0, react_jsx_runtime.jsx)(PendingFileRow, {
11990
12157
  file: entry,
11991
- files,
11992
12158
  selected: selected === entry.id,
11993
12159
  t,
11994
12160
  onSelect: setSelected
@@ -12027,12 +12193,22 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
12027
12193
  style: { bottom: bottomPx },
12028
12194
  "data-diff-approval-panel": true,
12029
12195
  "aria-label": t("panel.title"),
12030
- children: [(0, react_jsx_runtime.jsx)("header", {
12196
+ children: [(0, react_jsx_runtime.jsxs)("header", {
12031
12197
  className: PendingPanel_module_css_default.header,
12032
- children: (0, react_jsx_runtime.jsx)("span", {
12198
+ children: [(0, react_jsx_runtime.jsx)("span", {
12033
12199
  className: PendingPanel_module_css_default.title,
12034
12200
  children: t("panel.title")
12035
- })
12201
+ }), (0, react_jsx_runtime.jsx)("button", {
12202
+ type: "button",
12203
+ className: PendingPanel_module_css_default.close,
12204
+ "data-diff-approval-close": true,
12205
+ "aria-label": t("action.close"),
12206
+ title: t("action.close"),
12207
+ onClick: () => {
12208
+ setOpen(false);
12209
+ },
12210
+ children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseOutline16, { size: 14 })
12211
+ })]
12036
12212
  }), snapshot.error !== void 0 || !snapshot.read || files.length === 0 ? (0, react_jsx_runtime.jsxs)("div", {
12037
12213
  className: PendingPanel_module_css_default.states,
12038
12214
  children: [
@@ -12099,7 +12275,7 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
12099
12275
  type: "button",
12100
12276
  className: PendingPanel_module_css_default.badge,
12101
12277
  "data-diff-approval-badge": files.length,
12102
- "data-active": open || files.length > 0 ? "" : void 0,
12278
+ "data-active": open ? "" : void 0,
12103
12279
  "aria-label": t("panel.aria"),
12104
12280
  "aria-expanded": open,
12105
12281
  onClick: toggleOpen,
@@ -12337,7 +12513,7 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
12337
12513
  "panel.group.others": "其他会话",
12338
12514
  "panel.aria": "待处理改动",
12339
12515
  "panel.stats": "+{added} -{removed}",
12340
- "panel.selectHint": "点击每项查看整个文件的差异,拖动选择代码行可复制引用",
12516
+ "panel.selectHint": "点击每项查看整个文件的差异;选中文本后,用底部状态栏或 Ctrl+L 复制引用",
12341
12517
  "panel.missing": "文件已不存在",
12342
12518
  "panel.missingHint": "该文件已不存在,回退会恢复该文件。",
12343
12519
  "panel.createHint": "回退会删除该新建的文件。",
@@ -12351,8 +12527,12 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
12351
12527
  "action.busy": "处理中…",
12352
12528
  "action.prevDiff": "上一处差异",
12353
12529
  "action.nextDiff": "下一处差异",
12354
- "action.copyRange": "复制引用",
12530
+ "action.copyHint": "复制引用",
12355
12531
  "action.copied": "已复制",
12532
+ "action.langAuto": "自动",
12533
+ "action.langAutoDetected": "自动:{lang}",
12534
+ "action.langSelect": "高亮语言",
12535
+ "action.close": "关闭",
12356
12536
  "status.kept": "已保留",
12357
12537
  "status.reverted": "已回退",
12358
12538
  "status.missing": "该改动已不存在"
@@ -12367,7 +12547,7 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
12367
12547
  "panel.group.others": "Other sessions",
12368
12548
  "panel.aria": "Pending changes",
12369
12549
  "panel.stats": "+{added} -{removed}",
12370
- "panel.selectHint": "Select an item to review its whole-file diff; drag lines to copy a reference",
12550
+ "panel.selectHint": "Select an item to review its whole-file diff; select text and copy its reference from the status bar (Ctrl+L)",
12371
12551
  "panel.missing": "File is gone",
12372
12552
  "panel.missingHint": "This file no longer exists; reverting restores it.",
12373
12553
  "panel.createHint": "Reverting removes this created file.",
@@ -12381,8 +12561,12 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
12381
12561
  "action.busy": "Working…",
12382
12562
  "action.prevDiff": "Previous diff",
12383
12563
  "action.nextDiff": "Next diff",
12384
- "action.copyRange": "Copy reference",
12564
+ "action.copyHint": "Copy reference",
12385
12565
  "action.copied": "Copied",
12566
+ "action.langAuto": "Auto",
12567
+ "action.langAutoDetected": "Auto: {lang}",
12568
+ "action.langSelect": "Highlight language",
12569
+ "action.close": "Close",
12386
12570
  "status.kept": "Kept",
12387
12571
  "status.reverted": "Reverted",
12388
12572
  "status.missing": "This change no longer exists"
package/lib/index.js CHANGED
@@ -236,6 +236,29 @@ var PendingPersistence = class {
236
236
  return entries.sort((left, right) => left.updatedAt - right.updatedAt);
237
237
  }
238
238
  /**
239
+ * Load every session's entries for one workspace, oldest capture first.
240
+ * The list shows a workspace's pending changes across sessions — a session
241
+ * that restarted carries a fresh id while its earlier entries sit under the
242
+ * original session ids in the same workspace file — so hydration reads the
243
+ * whole workspace, not one session.
244
+ * @param workspaceId - the owning workspace's stable id.
245
+ * @returns the persisted entries across all sessions; empty when none were saved.
246
+ */
247
+ async loadWorkspace(workspaceId) {
248
+ const envelope = await this.readWorkspace(this.fileOf(workspaceId));
249
+ if (envelope === void 0) return [];
250
+ const entries = [];
251
+ for (const sessionId of Object.keys(envelope.sessions)) {
252
+ const rows = envelope.sessions[sessionId];
253
+ if (!Array.isArray(rows)) continue;
254
+ for (const row of rows) {
255
+ const entry = pendingEntryOf(row);
256
+ if (entry !== void 0) entries.push(entry);
257
+ }
258
+ }
259
+ return entries.sort((left, right) => left.updatedAt - right.updatedAt);
260
+ }
261
+ /**
239
262
  * Replace one session's entries durably. Saves to one file are serialized;
240
263
  * a previous save's failure does not block the next one.
241
264
  * @param workspaceId - the owning workspace's stable id.
@@ -348,9 +371,10 @@ function defaultOpenPath(path, action) {
348
371
  * ```
349
372
  *
350
373
  * Pending entries persist per (workspace, session) so an unhandled operation
351
- * survives a harness restart; the list endpoint re-reads the live file, so a
352
- * change or deletion made after the tracked operation is reported after
353
- * restart exactly as it is mid-session.
374
+ * survives a harness restart; the list endpoint hydrates the whole workspace
375
+ * and merges every registered session's entries, so a fresh session after a
376
+ * restart still reports the earlier sessions' pending changes, live-verified
377
+ * exactly as it is mid-session.
354
378
  *
355
379
  * @module dsh-diff-approval
356
380
  */
@@ -460,8 +484,11 @@ function apply(ctx, config) {
460
484
  const store = new PendingDiffStore();
461
485
  const persistence = new PendingPersistence(resolve(expandHomePath(storageDir ?? defaultStorageDir())));
462
486
  const launchPath = config?.openPath ?? defaultOpenPath;
463
- const loaded = /* @__PURE__ */ new Set();
464
- const loading = /* @__PURE__ */ new Map();
487
+ /** Sessions seen per workspace, so the list can merge a workspace's sessions. */
488
+ const sessionsByWorkspace = /* @__PURE__ */ new Map();
489
+ /** Workspace ids whose persisted state has been hydrated into the store. */
490
+ const loadedWorkspaces = /* @__PURE__ */ new Set();
491
+ const loadingWorkspaces = /* @__PURE__ */ new Map();
465
492
  /** Pre-write bases captured at the intent seams, keyed by the tool call id. */
466
493
  const editorIntents = /* @__PURE__ */ new Map();
467
494
  /**
@@ -602,31 +629,94 @@ function apply(ctx, config) {
602
629
  for (const workspace of ctx.workspaceRegistry.list()) if (workspace.sessionIds.includes(sessionId)) return workspace;
603
630
  }
604
631
  /**
605
- * Merge one session's persisted entries into the store, once per session.
606
- * Concurrent callers share the in-flight load, and folds arriving while the
607
- * load runs stay safe: `hydrate` never overwrites a live entry.
608
- * @param sessionId - the session to hydrate.
609
- * @returns resolution after the session's persisted state is merged (or skipped).
632
+ * Record one session in its workspace's account. Every path that touches a
633
+ * session registers it, so the list merges all of a workspace's sessions'
634
+ * entries a fresh session after restart still sees the workspace's
635
+ * persisted pending changes.
636
+ * @param sessionId - the session to register.
637
+ * @returns the owning workspace, or `undefined` when none accounts it.
610
638
  */
611
- function ensureLoaded(sessionId) {
612
- const key = String(sessionId);
613
- if (loaded.has(key)) return Promise.resolve();
614
- const pending = loading.get(key);
639
+ function registerSession(sessionId) {
640
+ const workspace = workspaceOf(sessionId);
641
+ if (workspace === void 0) return void 0;
642
+ const key = String(workspace.id);
643
+ const sessions = sessionsByWorkspace.get(key);
644
+ if (sessions === void 0) sessionsByWorkspace.set(key, /* @__PURE__ */ new Set([String(sessionId)]));
645
+ else sessions.add(String(sessionId));
646
+ return workspace;
647
+ }
648
+ /**
649
+ * Merge one workspace's persisted entries into the store, once per
650
+ * workspace. Hydration is workspace-scoped: after a restart the current
651
+ * session has a fresh id while the persisted entries live under their
652
+ * original session ids in the same workspace file, so the whole workspace
653
+ * is loaded and every persisted session is accounted. Concurrent callers
654
+ * share the in-flight load, and folds arriving while the load runs stay
655
+ * safe: `hydrate` never overwrites a live entry.
656
+ * @param workspace - the workspace whose persisted state to merge.
657
+ * @returns resolution after the workspace's persisted state is merged (or skipped).
658
+ */
659
+ function ensureWorkspaceLoaded(workspace) {
660
+ const key = String(workspace.id);
661
+ if (loadedWorkspaces.has(key)) return Promise.resolve();
662
+ const pending = loadingWorkspaces.get(key);
615
663
  if (pending !== void 0) return pending;
616
664
  const task = (async () => {
617
- const workspace = workspaceOf(sessionId);
618
- if (workspace !== void 0) try {
619
- store.hydrate(sessionId, await persistence.load(String(workspace.id), key));
665
+ try {
666
+ const persisted = await persistence.loadWorkspace(key);
667
+ const bySession = /* @__PURE__ */ new Map();
668
+ for (const entry of persisted) {
669
+ const sessionKey = String(entry.sessionId);
670
+ const group = bySession.get(sessionKey);
671
+ if (group === void 0) bySession.set(sessionKey, [entry]);
672
+ else group.push(entry);
673
+ }
674
+ for (const [sessionKey, entries] of bySession) {
675
+ const sessions = sessionsByWorkspace.get(key) ?? /* @__PURE__ */ new Set();
676
+ sessions.add(sessionKey);
677
+ sessionsByWorkspace.set(key, sessions);
678
+ store.hydrate(SessionId(sessionKey), entries);
679
+ }
620
680
  } catch (error) {
621
- ctx.logger.warn(`diff-approval: loading persisted state for session ${key} failed: ${errorMessage(error)}`);
681
+ ctx.logger.warn(`diff-approval: loading persisted state for workspace ${key} failed: ${errorMessage(error)}`);
622
682
  }
623
- loaded.add(key);
624
- loading.delete(key);
683
+ loadedWorkspaces.add(key);
684
+ loadingWorkspaces.delete(key);
625
685
  })();
626
- loading.set(key, task);
686
+ loadingWorkspaces.set(key, task);
627
687
  return task;
628
688
  }
629
689
  /**
690
+ * Merge the session's workspace's persisted state into the store (a session
691
+ * with no workspace is the memory-only edge and has nothing to load).
692
+ * @param sessionId - the session to hydrate for.
693
+ * @returns resolution after the workspace's persisted state is merged.
694
+ */
695
+ function ensureLoaded(sessionId) {
696
+ const workspace = registerSession(sessionId);
697
+ if (workspace === void 0) return Promise.resolve();
698
+ return ensureWorkspaceLoaded(workspace);
699
+ }
700
+ /**
701
+ * All entries visible to one session: every registered session of its
702
+ * workspace, merged oldest capture first. This is what makes an unhandled
703
+ * change survive a restart — the new session lists the workspace's whole
704
+ * pending set, its own live folds plus the earlier sessions' persisted
705
+ * entries.
706
+ * @param sessionId - the viewing session.
707
+ * @returns the merged entries; a session with no workspace lists only itself.
708
+ */
709
+ async function workspaceEntries(sessionId) {
710
+ await ensureLoaded(sessionId);
711
+ const workspace = workspaceOf(sessionId);
712
+ if (workspace === void 0) return store.list(sessionId);
713
+ const sessions = sessionsByWorkspace.get(String(workspace.id));
714
+ if (sessions === void 0) return store.list(sessionId);
715
+ const entries = [];
716
+ for (const sessionKey of sessions) entries.push(...store.list(SessionId(sessionKey)));
717
+ return entries.sort((left, right) => left.updatedAt - right.updatedAt);
718
+ }
719
+ /**
630
720
  * Mirror one session's entries to disk. A write fault logs a warning and
631
721
  * leaves the in-memory view intact: the review flow must not break on a
632
722
  * storage fault, and the next successful mutation rewrites the whole file.
@@ -668,10 +758,9 @@ function apply(ctx, config) {
668
758
  case "list": {
669
759
  const sessionId = sessionOf(payload);
670
760
  if (sessionId === void 0) return rpcError("sessionId must be a non-empty string");
671
- await ensureLoaded(sessionId);
672
761
  return {
673
762
  ok: true,
674
- value: { files: await listWithState(store.list(sessionId)) }
763
+ value: { files: await listWithState(await workspaceEntries(sessionId)) }
675
764
  };
676
765
  }
677
766
  case "keep": {
@@ -15,6 +15,8 @@ export interface HighlightSpan {
15
15
  text: string;
16
16
  style: CSSProperties;
17
17
  }
18
+ /** Supported grammar ids, in picker order, for the viewer's language selector. */
19
+ export declare const HIGHLIGHT_LANGS: string[];
18
20
  /**
19
21
  * Tokenize `code` into per-line highlighted runs when `lang` names a
20
22
  * registered grammar; `undefined` means the caller renders its plain fallback.
@@ -24,8 +24,12 @@ export declare const zh: {
24
24
  'action.busy': string;
25
25
  'action.prevDiff': string;
26
26
  'action.nextDiff': string;
27
- 'action.copyRange': string;
27
+ 'action.copyHint': string;
28
28
  'action.copied': string;
29
+ 'action.langAuto': string;
30
+ 'action.langAutoDetected': string;
31
+ 'action.langSelect': string;
32
+ 'action.close': string;
29
33
  'status.kept': string;
30
34
  'status.reverted': string;
31
35
  'status.missing': string;
@@ -62,8 +66,12 @@ export declare const en: {
62
66
  'action.busy': string;
63
67
  'action.prevDiff': string;
64
68
  'action.nextDiff': string;
65
- 'action.copyRange': string;
69
+ 'action.copyHint': string;
66
70
  'action.copied': string;
71
+ 'action.langAuto': string;
72
+ 'action.langAutoDetected': string;
73
+ 'action.langSelect': string;
74
+ 'action.close': string;
67
75
  'status.kept': string;
68
76
  'status.reverted': string;
69
77
  'status.missing': string;
@@ -26,9 +26,10 @@
26
26
  * ```
27
27
  *
28
28
  * Pending entries persist per (workspace, session) so an unhandled operation
29
- * survives a harness restart; the list endpoint re-reads the live file, so a
30
- * change or deletion made after the tracked operation is reported after
31
- * restart exactly as it is mid-session.
29
+ * survives a harness restart; the list endpoint hydrates the whole workspace
30
+ * and merges every registered session's entries, so a fresh session after a
31
+ * restart still reports the earlier sessions' pending changes, live-verified
32
+ * exactly as it is mid-session.
32
33
  *
33
34
  * @module dsh-diff-approval
34
35
  */
@@ -37,6 +37,16 @@ export declare class PendingPersistence {
37
37
  * @returns the persisted entries; empty when none were saved.
38
38
  */
39
39
  load(workspaceId: string, sessionId: string): Promise<PendingEntry[]>;
40
+ /**
41
+ * Load every session's entries for one workspace, oldest capture first.
42
+ * The list shows a workspace's pending changes across sessions — a session
43
+ * that restarted carries a fresh id while its earlier entries sit under the
44
+ * original session ids in the same workspace file — so hydration reads the
45
+ * whole workspace, not one session.
46
+ * @param workspaceId - the owning workspace's stable id.
47
+ * @returns the persisted entries across all sessions; empty when none were saved.
48
+ */
49
+ loadWorkspace(workspaceId: string): Promise<PendingEntry[]>;
40
50
  /**
41
51
  * Replace one session's entries durably. Saves to one file are serialized;
42
52
  * a previous save's failure does not block the next one.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-diff-approval",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "DeepSeek Harness plugin: pending-edit review with whole-file diff and Keep/Revert",
5
5
  "packageManager": "pnpm@11.21.0",
6
6
  "author": "Wu Zhiwei",
@@ -31,7 +31,8 @@
31
31
  "lib/index.js",
32
32
  "lib/client.js",
33
33
  "lib/types/**/*.d.ts",
34
- "cordis.patch.yml"
34
+ "cordis.patch.yml",
35
+ "docs/images/**"
35
36
  ],
36
37
  "dsh": {
37
38
  "bundle": {