dsh-ptc-diff-card 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.en.md +116 -0
- package/README.md +97 -2
- package/VERIFY.md +61 -0
- package/cordis.patch.yml +5 -0
- package/lib/client.js +575 -0
- package/lib/index.js +6 -0
- package/package.json +76 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 GrounzerLiu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.en.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# dsh-ptc-diff-card
|
|
2
|
+
|
|
3
|
+
[English](README.en.md) · [简体中文](README.md)
|
|
4
|
+
|
|
5
|
+
DSH Web GUI plugin: shows the standard diff card for `edit`/`write` tool calls made
|
|
6
|
+
inside PTC programs.
|
|
7
|
+
|
|
8
|
+
## The problem
|
|
9
|
+
|
|
10
|
+
In PTC (Programmatic Tool Calling) mode the agent calls `edit`/`write` from inside a
|
|
11
|
+
`run_code` program. Those calls are recorded as nested `tool/ptc-dispatch` events and
|
|
12
|
+
rendered as generic rows under the `run_code` card: expanding one shows the result text
|
|
13
|
+
(`The file ... has been updated successfully`) instead of a diff card.
|
|
14
|
+
|
|
15
|
+
The shipped Tool layer derives its diff card in `packages/client/ui-tool`
|
|
16
|
+
(`diffCardModel`) and stops at root calls on its first line:
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
if (block.parentCallId !== undefined) return null
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Everything the card needs is already on the nested block: the call arguments
|
|
23
|
+
(`file_path`/`old_string`/`new_string`/`content`) and the persisted `meta.diffs`.
|
|
24
|
+
|
|
25
|
+
## What you get
|
|
26
|
+
|
|
27
|
+
- Nested `edit`/`write` rows become standard file cards: title, workspace-relative path,
|
|
28
|
+
decoded input size, `+added -removed`;
|
|
29
|
+
- expanding renders the shipped `DiffBlock`: language label, wrap/copy actions, path line,
|
|
30
|
+
colored additions and deletions, `⋯ N more lines` folding;
|
|
31
|
+
- failed calls render no diff: the error's first line in the summary, raw output expanded
|
|
32
|
+
(same as the stock row);
|
|
33
|
+
- root calls look byte-for-byte identical to the shipped row, because the whole row is a
|
|
34
|
+
faithful re-implementation.
|
|
35
|
+
|
|
36
|
+
## How it works
|
|
37
|
+
|
|
38
|
+
The plugin registers its own `edit`/`write` views on the `tool.call.toolview` keyed slot at
|
|
39
|
+
`priority: -1`, which shadows the shipped rows while mounted and restores them on unload.
|
|
40
|
+
The row re-implements the stock file-mutation row chrome (same `DisclosureRow`/`DiffBlock`
|
|
41
|
+
primitives plus a vendored copy of the stock stylesheet) and reuses the stock diff
|
|
42
|
+
derivation minus the root-only guard:
|
|
43
|
+
|
|
44
|
+
| Call state | Diff source |
|
|
45
|
+
|---|---|
|
|
46
|
+
| settled success with non-empty `meta.diffs` | the recorded applied hunks (hunk paths rewritten to the caller's spelling) |
|
|
47
|
+
| `write` create / call still running | whole-file diff derived from the arguments |
|
|
48
|
+
| failed / interrupted | no diff |
|
|
49
|
+
|
|
50
|
+
## Install
|
|
51
|
+
|
|
52
|
+
Mount it as a normal DSH bundle:
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
# local directory (development)
|
|
56
|
+
dsh plugin --profile <name> add link:D:/dsh-plugins/dsh-ptc-diff-card
|
|
57
|
+
|
|
58
|
+
# from GitHub
|
|
59
|
+
dsh plugin --profile <name> add "github:GrounzerLiu/dsh-ptc-diff-card"
|
|
60
|
+
|
|
61
|
+
# from npm (once published)
|
|
62
|
+
dsh plugin --profile <name> add dsh-ptc-diff-card
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Then add `dsh-ptc-diff-card` to that profile's `package.json` `dsh.profile.bundles` and
|
|
66
|
+
restart the app.
|
|
67
|
+
|
|
68
|
+
## Compatibility and limits
|
|
69
|
+
|
|
70
|
+
- Targets DSH `>= 0.2.1-rc.2` (`engines.dsh`) on the Web GUI (`platform: web`).
|
|
71
|
+
- Takes over `edit` and `write` only; `str_replace_editor`/`apply_patch` are not covered.
|
|
72
|
+
- The row stylesheet is a vendored snapshot of ui-tool 0.2.1-rc.3 chrome; re-vendor after a
|
|
73
|
+
major restyle.
|
|
74
|
+
- Cannot coexist with another plugin shadowing the same `edit`/`write` keys at
|
|
75
|
+
`priority: -1` (e.g. `dsh-diff-viewer`): the slot rejects equal key + priority. This
|
|
76
|
+
plugin catches that per key and logs, leaving the shipped row in charge instead of
|
|
77
|
+
failing the whole plugin load.
|
|
78
|
+
- No auto-expand; behavior matches standard mode.
|
|
79
|
+
|
|
80
|
+
## Development
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
pnpm install
|
|
84
|
+
pnpm run check # typecheck + pure derivation assertions + build
|
|
85
|
+
pnpm run build # lib/index.js and lib/client.js only
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
src/index.ts host half: inert Loader entry (mount point for the browser half)
|
|
90
|
+
src/client/index.tsx slot registration (edit/write, priority -1)
|
|
91
|
+
src/client/row.tsx the row component (preparing + dispatched/settled stages)
|
|
92
|
+
src/client/model.ts row state, paths, and diff derivation
|
|
93
|
+
src/client/tool-row.module.css vendored stock row stylesheet
|
|
94
|
+
scripts/model-check.ts pure derivation assertions (Node type stripping, no extra deps)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The browser half is bundled with the DSH client-plugin wrapper
|
|
98
|
+
(`window.__ModuleLoader__.load({ id, factory })`): the platform module table stays external,
|
|
99
|
+
everything else is inlined, and CSS Modules compile into a hashed class map plus one
|
|
100
|
+
`<style data-plugin>` tag.
|
|
101
|
+
|
|
102
|
+
## Checks
|
|
103
|
+
|
|
104
|
+
`scripts/model-check.ts` pins the derivation's behavior without a browser: settled write
|
|
105
|
+
create, applied edit hunks with path rewriting, nested PTC dispatch (the case this plugin
|
|
106
|
+
exists for), failed edit, no-op edit, running call, preparing call, interrupted result,
|
|
107
|
+
windowed result with a null call head, and the path helpers.
|
|
108
|
+
|
|
109
|
+
## Verification
|
|
110
|
+
|
|
111
|
+
See [VERIFY.md](VERIFY.md) (Chinese): how to boot a test profile, which row to look at in a
|
|
112
|
+
PTC session, and how to compare standard mode and failed calls.
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
MIT
|
package/README.md
CHANGED
|
@@ -1,3 +1,98 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-ptc-diff-card
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[简体中文](README.md) · [English](README.en.md)
|
|
4
|
+
|
|
5
|
+
DSH Web GUI 插件:让 **PTC 模式**下的 `edit`/`write` 显示和标准模式一样的 diff 卡片。
|
|
6
|
+
|
|
7
|
+
## 解决什么问题
|
|
8
|
+
|
|
9
|
+
PTC(Programmatic Tool Calling)模式下,agent 在 `run_code` 程序里调用 `edit`/`write`。
|
|
10
|
+
这些调用被记录成嵌套的 `tool/ptc-dispatch` 事件,在 `run_code` 卡片下面渲染成通用行:
|
|
11
|
+
展开只有结果文本 `The file ... has been updated successfully`,没有 diff 卡片。
|
|
12
|
+
|
|
13
|
+
官方 Tool 层的 diff 推导在 `packages/client/ui-tool` 的 `diffCardModel` 里,第一行就把嵌套调用排除了:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
if (block.parentCallId !== undefined) return null
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
而卡片需要的数据其实都在——嵌套块上有调用参数(`file_path`/`old_string`/`new_string`/`content`)
|
|
20
|
+
和落盘的 `meta.diffs`(已应用 hunks)。
|
|
21
|
+
|
|
22
|
+
## 效果
|
|
23
|
+
|
|
24
|
+
- 嵌套的 `edit`/`write` 行变成标准文件卡片:标题、工作区相对路径、输入大小、`+新增 -删除`;
|
|
25
|
+
- 展开就是官方 `DiffBlock`:语言标签、换行/复制、路径行、增删着色、`⋯ 其余 N 行` 折叠;
|
|
26
|
+
- 失败的调用不画 diff,标题显示错误首行、展开是原始输出(与官方一致);
|
|
27
|
+
- 根调用(标准模式)外观与官方逐字一致,因为整行是照官方实现的复刻。
|
|
28
|
+
|
|
29
|
+
## 原理
|
|
30
|
+
|
|
31
|
+
在 `tool.call.toolview` 这个 keyed slot 上注册自己的 `edit`/`write` 视图,`priority: -1`
|
|
32
|
+
覆盖官方行(低优先级渲染),插件卸载自动还原。行本身复刻官方文件行(同一套
|
|
33
|
+
`DisclosureRow`/`DiffBlock` 原语 + 一份 vendor 的官方样式表),diff 推导沿用官方逻辑,
|
|
34
|
+
只去掉「根调用限定」:
|
|
35
|
+
|
|
36
|
+
| 调用状态 | diff 来源 |
|
|
37
|
+
|---|---|
|
|
38
|
+
| 成功 settle 且 `meta.diffs` 非空 | 记录的实际 hunks(路径回写成入参拼写) |
|
|
39
|
+
| `write` 新建 / 调用进行中 | 由入参推导的整文件 diff |
|
|
40
|
+
| 失败 / 中断 | 不画 diff |
|
|
41
|
+
|
|
42
|
+
## 安装
|
|
43
|
+
|
|
44
|
+
作为普通 DSH bundle 挂到某个 profile:
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
# 本地目录(开发)
|
|
48
|
+
dsh plugin --profile <name> add link:D:/dsh-plugins/dsh-ptc-diff-card
|
|
49
|
+
|
|
50
|
+
# 从 GitHub
|
|
51
|
+
dsh plugin --profile <name> add "github:GrounzerLiu/dsh-ptc-diff-card"
|
|
52
|
+
|
|
53
|
+
# 从 npm(已发布后)
|
|
54
|
+
dsh plugin --profile <name> add dsh-ptc-diff-card
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
然后把 `dsh-ptc-diff-card` 加进该 profile `package.json` 的 `dsh.profile.bundles`,
|
|
58
|
+
重启应用生效。
|
|
59
|
+
|
|
60
|
+
## 兼容性与边界
|
|
61
|
+
|
|
62
|
+
- 目标:DSH `>= 0.2.1-rc.2`(`engines.dsh`),Web GUI(`platform: web`)。
|
|
63
|
+
- 只接管 `edit` 和 `write`;`str_replace_editor`/`apply_patch` 等未覆盖。
|
|
64
|
+
- 行样式是从 ui-tool 0.2.1-rc.3 vendor 的一份拷贝,应用大改版后如需同步外观要重新 vendor。
|
|
65
|
+
- 与同样以 `priority: -1` 抢占 `edit`/`write` 的插件(如 `dsh-diff-viewer`)不能共存:
|
|
66
|
+
同 key 同优先级会被 DSH 拒。本插件会捕获并打日志,让官方行继续接管,不会让整个插件加载失败。
|
|
67
|
+
- 不自动展开 diff,行为与标准模式一致。
|
|
68
|
+
|
|
69
|
+
## 开发
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
pnpm install
|
|
73
|
+
pnpm run check # typecheck + 纯逻辑断言 + 构建
|
|
74
|
+
pnpm run build # 只构建 lib/index.js 与 lib/client.js
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`source` 结构:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
src/index.ts 宿主半身:inert 的 Loader 入口(浏览器半身的挂载点)
|
|
81
|
+
src/client/index.tsx slot 注册(edit/write,priority -1)
|
|
82
|
+
src/client/row.tsx 行组件(preparing / dispatched-settled 两态)
|
|
83
|
+
src/client/model.ts 行状态、路径、diff 推导
|
|
84
|
+
src/client/tool-row.module.css vendor 的官方行样式
|
|
85
|
+
scripts/model-check.ts 纯逻辑断言(Node 原生 type stripping,无额外依赖)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
浏览器半身按 DSH 客户端插件格式打包:platform 模块表(`react`、`@deepseek-ai/cordis`、
|
|
89
|
+
`ui-slots`、`ui-primitives`…)保持 external,其余内联;CSS Modules 编译成哈希类名 +
|
|
90
|
+
一个 `<style data-plugin>`。
|
|
91
|
+
|
|
92
|
+
## 验收
|
|
93
|
+
|
|
94
|
+
见 [VERIFY.md](VERIFY.md):怎么起测试 profile、在 PTC 会话里看哪一行、怎么对照标准模式与失败调用。
|
|
95
|
+
|
|
96
|
+
## 许可
|
|
97
|
+
|
|
98
|
+
MIT
|
package/VERIFY.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# 验收指南(PTC diff 卡片)
|
|
2
|
+
|
|
3
|
+
## 已经装好的测试环境
|
|
4
|
+
|
|
5
|
+
- profile:`ptcdiff`(`%USERPROFILE%\.dsh\profiles\ptcdiff`),bundles =
|
|
6
|
+
`@deepseek-ai/dsh-base` + `@deepseek-ai/dsh-web-app` + `dsh-ptc-diff-card`
|
|
7
|
+
- 服务:`dsh --profile ptcdiff --port 19399 --no-open`
|
|
8
|
+
- 浏览器入口:启动日志里打印的 `http://127.0.0.1:19399/?token=...`(每次启动 token 会变)
|
|
9
|
+
|
|
10
|
+
重启服务:
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
dsh --profile ptcdiff --port 19399 --no-open
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## 看什么
|
|
17
|
+
|
|
18
|
+
### 1. PTC 模式(核心诉求)
|
|
19
|
+
|
|
20
|
+
打开侧栏 `dsh-plugins` → `PTC 模式 diff 卡片插件开发`(或 `winia` 的任意 PTC 会话)。
|
|
21
|
+
|
|
22
|
+
- 每个由 PTC 程序调用的 `edit`/`write` 现在渲染成标准文件卡片:
|
|
23
|
+
`编辑 winia/src/xxx.rs 1KB +5 -2` / `写入 .refs/foo.mjs 2KB +30 -0`
|
|
24
|
+
(标题 + 相对路径 + 输入大小 + 增删行数)
|
|
25
|
+
- 点一下这行 → 展开的就是标准 `DiffBlock` diff 卡片(语言标签、换行/复制按钮、路径行、
|
|
26
|
+
绿色新增 / 红色删除、`⋯ 其余 N 行` 折叠)。
|
|
27
|
+
|
|
28
|
+
旧行为(对照):同一行展开后只有结果文本
|
|
29
|
+
`The file ... has been updated successfully`,没有增删统计。
|
|
30
|
+
|
|
31
|
+
> 历史轮次的工具行收在「运行了代码,…,已写入文件等」这类进程分组里:先点分组标题展开,
|
|
32
|
+
> 再点卡片行展开 diff。最新一轮默认是展开的。
|
|
33
|
+
|
|
34
|
+
### 2. 标准模式(回归)
|
|
35
|
+
|
|
36
|
+
打开 `any` 工作区的 `你好` 会话(真正的根级 `write`/`edit`)。
|
|
37
|
+
|
|
38
|
+
- 折叠行文本与官方渲染逐字一致:`写入 write-tool-test.txt 1KB +16 -0`
|
|
39
|
+
(已在 desktop 官方 profile 用同一会话对照验证)
|
|
40
|
+
- 展开后同样是 diff 卡片。
|
|
41
|
+
|
|
42
|
+
### 3. 失败调用
|
|
43
|
+
|
|
44
|
+
`winia` 会话里有失败的 `edit`(`old_string matched 4 times`):行首显示「失败」+ 错误首行,
|
|
45
|
+
不渲染 diff,展开是原始错误输出 —— 与标准模式一致。
|
|
46
|
+
|
|
47
|
+
## 已知边界
|
|
48
|
+
|
|
49
|
+
- 只接管 `edit` 和 `write`;`str_replace_editor` / `apply_patch` 这类构建里没有的工具不覆盖。
|
|
50
|
+
- 样式表是从 ui-tool 0.2.1-rc.3 vendor 的一份拷贝;应用大改版后如需同步外观需重新同步该文件。
|
|
51
|
+
- 插件通过 `tool.call.toolview` keyed slot 以 `priority: -1` 覆盖官方行;卸载或停用后自动回到官方渲染。
|
|
52
|
+
|
|
53
|
+
## 装到 desktop profile(验收通过后)
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
# 1) 依赖
|
|
57
|
+
dsh plugin --profile desktop add link:D:/dsh-plugins/dsh-ptc-diff-card
|
|
58
|
+
# 2) 确认 %USERPROFILE%\.dsh\profiles\desktop\package.json 的 dsh.profile.bundles 末尾有
|
|
59
|
+
# "dsh-ptc-diff-card"
|
|
60
|
+
# 3) 重启应用
|
|
61
|
+
```
|
package/cordis.patch.yml
ADDED
package/lib/client.js
ADDED
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "dsh-ptc-diff-card",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let react = require("react");
|
|
8
|
+
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
9
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
10
|
+
// node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs
|
|
11
|
+
function r(e) {
|
|
12
|
+
var t, f, n = "";
|
|
13
|
+
if ("string" == typeof e || "number" == typeof e) n += e;
|
|
14
|
+
else if ("object" == typeof e) if (Array.isArray(e)) {
|
|
15
|
+
var o = e.length;
|
|
16
|
+
for (t = 0; t < o; t++) e[t] && (f = r(e[t])) && (n && (n += " "), n += f);
|
|
17
|
+
} else for (f in e) e[f] && (n && (n += " "), n += f);
|
|
18
|
+
return n;
|
|
19
|
+
}
|
|
20
|
+
function clsx() {
|
|
21
|
+
for (var e, t, f = 0, n = "", o = arguments.length; f < o; f++) (e = arguments[f]) && (t = r(e)) && (n && (n += " "), n += t);
|
|
22
|
+
return n;
|
|
23
|
+
}
|
|
24
|
+
// src/client/model.ts
|
|
25
|
+
const KILOBYTE = 1024;
|
|
26
|
+
const CONTENT_FIELDS = [
|
|
27
|
+
"content",
|
|
28
|
+
"old_string",
|
|
29
|
+
"new_string"
|
|
30
|
+
];
|
|
31
|
+
const PATH_FIELDS = ["file_path", "path"];
|
|
32
|
+
/** Narrow a block to the preparing stage. */
|
|
33
|
+
function isPreparing(block) {
|
|
34
|
+
return !("kind" in block) && block.phase === "preparing";
|
|
35
|
+
}
|
|
36
|
+
/** Narrow a block to the settled stage. */
|
|
37
|
+
function isSettled(block) {
|
|
38
|
+
return "kind" in block;
|
|
39
|
+
}
|
|
40
|
+
/** The wire name and raw argument JSON paired with a dispatched block. */
|
|
41
|
+
function callHead(block) {
|
|
42
|
+
return "kind" in block ? block.call : {
|
|
43
|
+
name: block.name,
|
|
44
|
+
argsRaw: block.argsRaw
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function parseArgs(raw) {
|
|
48
|
+
if (raw === "") return null;
|
|
49
|
+
try {
|
|
50
|
+
const value = JSON.parse(raw);
|
|
51
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
52
|
+
return value;
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** The parsed top-level argument object, or null when the call carries none. */
|
|
58
|
+
function argObject(block) {
|
|
59
|
+
const head = callHead(block);
|
|
60
|
+
const parsed = head === null ? null : parseArgs(head.argsRaw);
|
|
61
|
+
if (parsed !== null) return parsed;
|
|
62
|
+
const fallback = {};
|
|
63
|
+
for (const key of [...PATH_FIELDS, ...CONTENT_FIELDS]) {
|
|
64
|
+
const value = completedText(block.args, key);
|
|
65
|
+
if (value !== void 0) fallback[key] = value;
|
|
66
|
+
}
|
|
67
|
+
return Object.keys(fallback).length === 0 ? null : fallback;
|
|
68
|
+
}
|
|
69
|
+
function pickString(args, keys) {
|
|
70
|
+
if (args === null) return void 0;
|
|
71
|
+
for (const key of keys) {
|
|
72
|
+
const value = args[key];
|
|
73
|
+
if (typeof value === "string" && value.trim() !== "") return value;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function stringOf(args, key) {
|
|
77
|
+
const value = args[key];
|
|
78
|
+
return typeof value === "string" ? value : void 0;
|
|
79
|
+
}
|
|
80
|
+
/** Whether a path is absolute in either spelling the Host accepts (POSIX, Windows drive, UNC). */
|
|
81
|
+
function isAbsolutePath(path) {
|
|
82
|
+
return path.startsWith("/") || /^[A-Za-z]:[/\\]/.test(path) || path.startsWith("\\\\");
|
|
83
|
+
}
|
|
84
|
+
/** Whether a path uses a Windows drive or UNC prefix. */
|
|
85
|
+
function isWindowsStylePath(value) {
|
|
86
|
+
return /^[A-Za-z]:[/\\]/.test(value) || value.startsWith("\\\\");
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Abbreviate a POSIX home directory for display.
|
|
90
|
+
* @param path - absolute or already-short display path.
|
|
91
|
+
* @param home - host account home; absent, empty, or Windows-spelled paths skip abbreviation.
|
|
92
|
+
* @returns `~` or `~/…` for the POSIX home and its descendants, otherwise `path`.
|
|
93
|
+
*/
|
|
94
|
+
function abbreviateHomePath(path, home) {
|
|
95
|
+
if (home === void 0 || home === "") return path;
|
|
96
|
+
if (isWindowsStylePath(path) || isWindowsStylePath(home)) return path;
|
|
97
|
+
const root = home.replace(/\/+$/, "");
|
|
98
|
+
if (root === "" || root === "/") return path;
|
|
99
|
+
if (path.replace(/\/+$/, "") === root) return "~";
|
|
100
|
+
return path.startsWith(`${root}/`) ? `~${path.slice(root.length)}` : path;
|
|
101
|
+
}
|
|
102
|
+
/** A completed string field of a streaming argument view. */
|
|
103
|
+
function completedText(args, key) {
|
|
104
|
+
return args.complete(key) ? args.text(key) : void 0;
|
|
105
|
+
}
|
|
106
|
+
/** The call's target path at every stage. */
|
|
107
|
+
function blockPath(block) {
|
|
108
|
+
if (isPreparing(block)) {
|
|
109
|
+
const path = completedText(block.args, "file_path");
|
|
110
|
+
return path === void 0 || path.trim() === "" ? void 0 : path;
|
|
111
|
+
}
|
|
112
|
+
return pickString(argObject(block), PATH_FIELDS);
|
|
113
|
+
}
|
|
114
|
+
/** Kilobytes of decoded input at any stage; null before a content field arrives. */
|
|
115
|
+
function contentKilobytes(args) {
|
|
116
|
+
const open = CONTENT_FIELDS.find((key) => args.has(key) && !args.complete(key));
|
|
117
|
+
const completed = CONTENT_FIELDS.reduce((total, key) => key !== open && args.complete(key) ? total + (args.stringLength(key, { step: KILOBYTE }) ?? 0) : total, 0);
|
|
118
|
+
if (open === void 0) return CONTENT_FIELDS.some((key) => args.has(key)) ? Math.ceil(completed / KILOBYTE) : null;
|
|
119
|
+
const chars = completed + (args.stringLength(open, {
|
|
120
|
+
step: KILOBYTE,
|
|
121
|
+
offset: completed
|
|
122
|
+
}) ?? 0);
|
|
123
|
+
return Math.ceil(chars / KILOBYTE);
|
|
124
|
+
}
|
|
125
|
+
/** The whole-file diff derived from the call's own arguments. */
|
|
126
|
+
function intendedDiff(toolName, args) {
|
|
127
|
+
if (args === null) return null;
|
|
128
|
+
const path = pickString(args, PATH_FIELDS);
|
|
129
|
+
if (path === void 0) return null;
|
|
130
|
+
if (toolName === "write") {
|
|
131
|
+
const content = stringOf(args, "content");
|
|
132
|
+
return content === void 0 ? null : {
|
|
133
|
+
path,
|
|
134
|
+
oldText: null,
|
|
135
|
+
newText: content
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
if (toolName === "edit") {
|
|
139
|
+
const oldText = stringOf(args, "old_string");
|
|
140
|
+
const newText = stringOf(args, "new_string");
|
|
141
|
+
if (newText === void 0) return null;
|
|
142
|
+
return {
|
|
143
|
+
path,
|
|
144
|
+
oldText: oldText === void 0 || oldText === "" ? null : oldText,
|
|
145
|
+
newText
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
/** Narrow the persisted `meta.diffs` payload to well-formed hunks. */
|
|
151
|
+
function narrowDiffs(diffs) {
|
|
152
|
+
if (!Array.isArray(diffs)) return null;
|
|
153
|
+
const out = [];
|
|
154
|
+
for (const hunk of diffs) {
|
|
155
|
+
if (typeof hunk !== "object" || hunk === null) return null;
|
|
156
|
+
const { path, oldText, newText } = hunk;
|
|
157
|
+
if (typeof path !== "string") return null;
|
|
158
|
+
if (oldText !== null && typeof oldText !== "string") return null;
|
|
159
|
+
if (typeof newText !== "string") return null;
|
|
160
|
+
out.push({
|
|
161
|
+
path,
|
|
162
|
+
oldText,
|
|
163
|
+
newText
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
return out;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* The persisted applied hunks of a settled call.
|
|
170
|
+
* @returns the hunks, 'empty' for a settled call that recorded an empty list,
|
|
171
|
+
* or null when the result carries no usable diff metadata.
|
|
172
|
+
*/
|
|
173
|
+
function appliedDiffs(meta) {
|
|
174
|
+
if (typeof meta !== "object" || meta === null || Array.isArray(meta)) return null;
|
|
175
|
+
const diffs = meta.diffs;
|
|
176
|
+
if (!Array.isArray(diffs)) return null;
|
|
177
|
+
if (diffs.length === 0) return "empty";
|
|
178
|
+
return narrowDiffs(diffs);
|
|
179
|
+
}
|
|
180
|
+
/** The recorded absolute path the settled metadata belongs to (stock narrowing). */
|
|
181
|
+
function recordedPath(meta) {
|
|
182
|
+
if (typeof meta !== "object" || meta === null || Array.isArray(meta)) return void 0;
|
|
183
|
+
const path = meta.path;
|
|
184
|
+
return typeof path === "string" && isAbsolutePath(path) ? path : void 0;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Diff hunks to render for this mutation call.
|
|
188
|
+
*
|
|
189
|
+
* Root and nested dispatches follow the same rule: a settled success shows
|
|
190
|
+
* the applied hunks the tool recorded; a create (`diffs: []`) and a running
|
|
191
|
+
* call show the argument-derived whole-file diff. A nested dispatch whose
|
|
192
|
+
* metadata is absent falls back to the argument-derived diff, which the tool
|
|
193
|
+
* arguments already describe exactly.
|
|
194
|
+
* @param toolName - wire Tool name.
|
|
195
|
+
* @param block - the call block at its current stage.
|
|
196
|
+
* @returns hunks for the diff card, or null when the call shows no card.
|
|
197
|
+
*/
|
|
198
|
+
function mutationDiffs(toolName, block) {
|
|
199
|
+
if (isPreparing(block)) return null;
|
|
200
|
+
const intended = intendedDiff(toolName, argObject(block));
|
|
201
|
+
if (!isSettled(block)) return intended === null ? null : [intended];
|
|
202
|
+
if (block.isError) return null;
|
|
203
|
+
const applied = appliedDiffs(block.meta);
|
|
204
|
+
if (applied !== null && applied !== "empty" && applied.length > 0) {
|
|
205
|
+
const display = intended === null ? void 0 : intended.path;
|
|
206
|
+
const recorded = recordedPath(block.meta);
|
|
207
|
+
return applied.map((hunk) => display === void 0 || hunk.path !== recorded ? hunk : {
|
|
208
|
+
...hunk,
|
|
209
|
+
path: display
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
if (toolName === "write") return intended === null ? null : [intended];
|
|
213
|
+
return applied === null && intended !== null ? [intended] : null;
|
|
214
|
+
}
|
|
215
|
+
function firstLine(text) {
|
|
216
|
+
const nl = text.indexOf("\n");
|
|
217
|
+
return nl === -1 ? text : text.slice(0, nl);
|
|
218
|
+
}
|
|
219
|
+
/** Flatten a settled result's content blocks to display text. */
|
|
220
|
+
function resultText(node) {
|
|
221
|
+
const parts = [];
|
|
222
|
+
for (const block of node.content) if (block.type === "text") parts.push(block.text);
|
|
223
|
+
else parts.push(JSON.stringify(block, null, 2));
|
|
224
|
+
if (parts.length === 0 && node.error !== void 0) parts.push(`${node.error.name}: ${node.error.code}`);
|
|
225
|
+
return parts.join("\n");
|
|
226
|
+
}
|
|
227
|
+
/** The displayed form of a path: workspace-rooted paths shed the session cwd. */
|
|
228
|
+
function relativize(path, cwd) {
|
|
229
|
+
if (cwd === void 0 || cwd === "") return path;
|
|
230
|
+
const root = cwd.replace(/[/\\]+$/, "");
|
|
231
|
+
if (!path.startsWith(root)) return path;
|
|
232
|
+
const rest = path.slice(root.length);
|
|
233
|
+
return rest.startsWith("/") || rest.startsWith("\\") ? rest.slice(1) : path;
|
|
234
|
+
}
|
|
235
|
+
/** Row state for the block's current stage. */
|
|
236
|
+
function rowState(block) {
|
|
237
|
+
if (isPreparing(block)) return "preparing";
|
|
238
|
+
if (!isSettled(block)) return "running";
|
|
239
|
+
if (block.error?.code === "interrupted") return "stopped";
|
|
240
|
+
return block.isError ? "error" : "ok";
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Derive the whole row model.
|
|
244
|
+
*
|
|
245
|
+
* The summary keeps the call's own spelling (relative to the session
|
|
246
|
+
* workspace), while the link prefers the absolute path the settled result
|
|
247
|
+
* recorded — the same split the stock model makes, so a relative argument
|
|
248
|
+
* still opens the right file after the session's working directory moves.
|
|
249
|
+
* @param toolName - wire Tool name (edit or write).
|
|
250
|
+
* @param block - the call block at its current stage.
|
|
251
|
+
* @param cwd - session workspace root, used to shorten the summary path.
|
|
252
|
+
* @param home - host account home, used to abbreviate a POSIX home path.
|
|
253
|
+
* @returns the row model.
|
|
254
|
+
*/
|
|
255
|
+
function mutationModel(toolName, block, cwd, home) {
|
|
256
|
+
const path = blockPath(block);
|
|
257
|
+
const settled = isSettled(block);
|
|
258
|
+
const recorded = settled ? recordedPath(block.meta) : void 0;
|
|
259
|
+
const usableArgumentPath = path !== void 0 && (isAbsolutePath(path) || settled && !block.isError && block.parentCallId === void 0);
|
|
260
|
+
const linkPath = recorded ?? (usableArgumentPath ? path : void 0);
|
|
261
|
+
const output = settled ? resultText(block) || null : null;
|
|
262
|
+
return {
|
|
263
|
+
path,
|
|
264
|
+
linkPath,
|
|
265
|
+
summary: path === void 0 ? "" : abbreviateHomePath(relativize(path, cwd), home),
|
|
266
|
+
output,
|
|
267
|
+
state: rowState(block),
|
|
268
|
+
diffs: mutationDiffs(toolName, block)
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
/** First line of the settled output, for the failed-row summary. */
|
|
272
|
+
function errorSummary(model) {
|
|
273
|
+
return model.state === "error" && model.output !== null ? firstLine(model.output) : null;
|
|
274
|
+
}
|
|
275
|
+
// \0dsh-css:src/client/tool-row.module.css.mjs
|
|
276
|
+
const css = ".tool-row-module_root{flex-direction:column;display:flex}.tool-row-module_leading{flex-shrink:0}.tool-row-module_root[data-tool^=cordis_] .tool-row-module_title{font-weight:500}.tool-row-module_title{font-weight:400}.tool-row-module_sep{background:var(--dsw-alias-label-caption);border-radius:1px;flex:none;width:2px;height:2px;margin:0 8px}.tool-row-module_summary{text-overflow:ellipsis;white-space:nowrap;min-width:0;font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(24px + var(--dsh-content-font-delta,0px));flex:auto;overflow:hidden}.tool-row-module_summarySuffix{white-space:nowrap;font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(24px + var(--dsh-content-font-delta,0px));flex:none;margin-left:4px}.tool-row-module_diffStat{font-family:var(--ds-font-family-code);font-size:calc(var(--dsh-content-font-size-secondary,13px) - 2px);margin-left:10px;transform:translateY(.5px)}.tool-row-module_row[aria-expanded=true] .tool-row-module_diffAdded{color:var(--dsw-alias-state-success-primary)}@media (hover:hover){.tool-row-module_row:hover .tool-row-module_diffAdded{color:var(--dsw-alias-state-success-primary)}}.tool-row-module_row[aria-expanded=true] .tool-row-module_diffRemoved{color:var(--dsw-alias-state-error-primary)}@media (hover:hover){.tool-row-module_row:hover .tool-row-module_diffRemoved{color:var(--dsw-alias-state-error-primary)}}.tool-row-module_fileLink{text-overflow:ellipsis;white-space:nowrap;min-width:0;font:inherit;text-align:left;font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(24px + var(--dsh-content-font-delta,0px));color:inherit;text-underline-offset:3px;cursor:pointer;background:0 0;border:none;flex:0 auto;margin:0;padding:0;text-decoration:underline 1px dotted;overflow:hidden}.tool-row-module_errorSummary{color:var(--dsw-alias-state-error-primary)}.tool-row-module_stoppedSummary{color:var(--dsw-alias-state-warn-label)}.tool-row-module_bodyWrap{flex-direction:column;display:flex}.tool-row-module_inspectButton{border:.5px solid var(--dsw-alias-border-l3);corner-shape:round;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-secondary);cursor:pointer;opacity:0;border-radius:999px;align-self:flex-start;align-items:center;gap:4px;margin:4px 0 2px 4px;padding:2px 8px;font-size:11px;line-height:16px;transition:opacity .1s;display:inline-flex}.tool-row-module_inspectButton:focus-visible{opacity:1}@media (hover:hover){.tool-row-module_root:hover .tool-row-module_inspectButton{opacity:1}}.tool-row-module_detailsBodyWrap{position:relative}.tool-row-module_detailsBodyWrap .tool-row-module_inspectButton{margin:0;position:absolute;top:12px;right:12px}@media (hover:hover){.tool-row-module_inspectButton:hover{background:var(--dsw-alias-interactive-bg-hover-solid);color:var(--dsw-alias-label-primary)}}.tool-row-module_bodyScroll{max-height:260px;overflow-y:auto}.tool-row-module_ioCard{border:.5px solid var(--dsw-alias-border-l1);border-radius:var(--dsw-radius-lg);background:var(--dsw-alias-markdown-code-block);font:var(--dsw-font-markdown-code-block-small);flex-direction:column;margin:4px 0 4px 4px;display:flex}.tool-row-module_ioSection{grid-template-columns:max-content 1fr;align-items:baseline;column-gap:14px;max-height:150px;padding:12px 16px;display:grid;overflow-y:auto}.tool-row-module_ioSection::-webkit-scrollbar-thumb{border-radius:var(--dsw-radius-sm);background-clip:padding-box;border:2px solid #0000}.tool-row-module_ioSection::-webkit-scrollbar-track{margin:6px 0}.tool-row-module_ioLabel{color:var(--dsw-alias-label-caption);align-self:start;position:sticky;top:0}.tool-row-module_ioDivider{background:var(--dsw-alias-border-l2);flex:none;height:.5px}.tool-row-module_ioText{white-space:pre-wrap;word-break:break-word;min-width:0;color:var(--dsw-alias-label-secondary)}.tool-row-module_ioText[data-error]{color:var(--dsw-alias-state-error-primary)}.tool-row-module_codeBody,.tool-row-module_terminalBody,.tool-row-module_diffBody,.tool-row-module_readBody,.tool-row-module_imageBody,.tool-row-module_searchBody,.tool-row-module_webBody{margin:4px 0 4px 4px}.tool-row-module_searchRecovery{white-space:pre-wrap;overflow-wrap:anywhere;font:var(--dsw-font-xs-13);color:var(--dsw-alias-label-tertiary);margin:4px 0 4px 4px}.tool-row-module_imageMeta{white-space:pre-wrap;overflow-wrap:anywhere;font:var(--dsw-font-markdown-code-block-small);color:var(--dsw-alias-label-tertiary);margin-top:4px}.tool-row-module_imageExtra{white-space:pre-wrap;overflow-wrap:anywhere;font:var(--dsw-font-xs-13);color:var(--dsw-alias-label-tertiary);margin-top:4px}.tool-row-module_terminalBody{--dsl-terminal-font:var(--dsw-font-markdown-code-block-small);--dsl-terminal-line-height:18px;--dsl-terminal-output-max-height:224px;--dsl-terminal-command-whitespace:pre-wrap;border:.5px solid var(--dsw-alias-border-l1)}.tool-row-module_visuallyHidden{clip:rect(0 0 0 0);white-space:nowrap;width:1px;height:1px;position:absolute;overflow:hidden}@media (hover:none){.tool-row-module_inspectButton{opacity:1}}";
|
|
277
|
+
const tagId = "dsh-ptc-diff-card/tool-row.module.css";
|
|
278
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
279
|
+
const tag = document.createElement("style");
|
|
280
|
+
tag.dataset.plugin = "dsh-ptc-diff-card";
|
|
281
|
+
tag.dataset.pluginCss = tagId;
|
|
282
|
+
tag.textContent = css;
|
|
283
|
+
document.head.appendChild(tag);
|
|
284
|
+
}
|
|
285
|
+
var tool_row_module_css_default = {
|
|
286
|
+
"bodyScroll": "tool-row-module_bodyScroll",
|
|
287
|
+
"bodyWrap": "tool-row-module_bodyWrap",
|
|
288
|
+
"codeBody": "tool-row-module_codeBody",
|
|
289
|
+
"detailsBodyWrap": "tool-row-module_detailsBodyWrap",
|
|
290
|
+
"diffAdded": "tool-row-module_diffAdded",
|
|
291
|
+
"diffBody": "tool-row-module_diffBody",
|
|
292
|
+
"diffRemoved": "tool-row-module_diffRemoved",
|
|
293
|
+
"diffStat": "tool-row-module_diffStat",
|
|
294
|
+
"errorSummary": "tool-row-module_errorSummary",
|
|
295
|
+
"fileLink": "tool-row-module_fileLink",
|
|
296
|
+
"imageBody": "tool-row-module_imageBody",
|
|
297
|
+
"imageExtra": "tool-row-module_imageExtra",
|
|
298
|
+
"imageMeta": "tool-row-module_imageMeta",
|
|
299
|
+
"inspectButton": "tool-row-module_inspectButton",
|
|
300
|
+
"ioCard": "tool-row-module_ioCard",
|
|
301
|
+
"ioDivider": "tool-row-module_ioDivider",
|
|
302
|
+
"ioLabel": "tool-row-module_ioLabel",
|
|
303
|
+
"ioSection": "tool-row-module_ioSection",
|
|
304
|
+
"ioText": "tool-row-module_ioText",
|
|
305
|
+
"leading": "tool-row-module_leading",
|
|
306
|
+
"readBody": "tool-row-module_readBody",
|
|
307
|
+
"root": "tool-row-module_root",
|
|
308
|
+
"row": "tool-row-module_row",
|
|
309
|
+
"searchBody": "tool-row-module_searchBody",
|
|
310
|
+
"searchRecovery": "tool-row-module_searchRecovery",
|
|
311
|
+
"sep": "tool-row-module_sep",
|
|
312
|
+
"stoppedSummary": "tool-row-module_stoppedSummary",
|
|
313
|
+
"summary": "tool-row-module_summary",
|
|
314
|
+
"summarySuffix": "tool-row-module_summarySuffix",
|
|
315
|
+
"terminalBody": "tool-row-module_terminalBody",
|
|
316
|
+
"title": "tool-row-module_title",
|
|
317
|
+
"visuallyHidden": "tool-row-module_visuallyHidden",
|
|
318
|
+
"webBody": "tool-row-module_webBody"
|
|
319
|
+
};
|
|
320
|
+
// src/client/row.tsx
|
|
321
|
+
/**
|
|
322
|
+
* The `edit`/`write` Tool view this plugin mounts over the shipped
|
|
323
|
+
* file-mutation row.
|
|
324
|
+
*
|
|
325
|
+
* It re-implements the stock row's chrome (the same DisclosureRow/DiffBlock
|
|
326
|
+
* primitives and the vendored stock stylesheet) and swaps in one behavior:
|
|
327
|
+
* the expanded body is the diff card at every call site, including the
|
|
328
|
+
* nested `tool/ptc-dispatch` blocks that the stock row sends to the generic
|
|
329
|
+
* fallback. A root call therefore looks and behaves exactly as before, and a
|
|
330
|
+
* call dispatched from inside a PTC program finally shows the same card.
|
|
331
|
+
*/
|
|
332
|
+
/** The stock chat diff cap (ui-tool's CHAT_DIFF_MAX_LINES). */
|
|
333
|
+
const CHAT_DIFF_MAX_LINES = 9;
|
|
334
|
+
/** The row's leading glyph, sized like the stock file-mutation row. */
|
|
335
|
+
const MUTATION_ICON = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconEditOutlineRegular, { size: 14 });
|
|
336
|
+
/** Row variant per wire Tool name (the two keys this plugin takes over). */
|
|
337
|
+
function variantOf(toolName) {
|
|
338
|
+
return toolName === "edit" ? "edit" : "write";
|
|
339
|
+
}
|
|
340
|
+
/** The stock title key per wire Tool name. */
|
|
341
|
+
function titleKeyOf(toolName) {
|
|
342
|
+
return toolName === "edit" ? "tool.title.edit" : "tool.title.write";
|
|
343
|
+
}
|
|
344
|
+
/** Read one shared chrome string through the conversation seat. */
|
|
345
|
+
function chromeLabel(t, key) {
|
|
346
|
+
return t(key);
|
|
347
|
+
}
|
|
348
|
+
/** Localized chrome for the diff card, from the same conversation dictionary. */
|
|
349
|
+
function diffLabels(t) {
|
|
350
|
+
return {
|
|
351
|
+
codeLabel: chromeLabel(t, "codeBlock.title"),
|
|
352
|
+
wrapLabel: chromeLabel(t, "codeBlock.wrap"),
|
|
353
|
+
unwrapLabel: chromeLabel(t, "codeBlock.unwrap"),
|
|
354
|
+
copy: chromeLabel(t, "copy"),
|
|
355
|
+
copied: chromeLabel(t, "copied"),
|
|
356
|
+
collapseAria: t("diff.collapseAria"),
|
|
357
|
+
expandAria: (count) => t("diff.expandAria", { count }),
|
|
358
|
+
collapse: chromeLabel(t, "collapse"),
|
|
359
|
+
expand: (count) => t("diff.expandRest", { count })
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
/** Visually hidden run-state label (the stock row's AT requirement). */
|
|
363
|
+
function stateStatus(state, t) {
|
|
364
|
+
switch (state) {
|
|
365
|
+
case "preparing": return t("row.preparing");
|
|
366
|
+
case "running": return t("row.running");
|
|
367
|
+
case "error": return t("row.failed");
|
|
368
|
+
case "stopped": return t("row.stopped");
|
|
369
|
+
default: return null;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
/** The preparing prefix: named call, path once its string closes, input size. */
|
|
373
|
+
function PreparingRow({ toolName, block, cwd, home, useDisclosure, t }) {
|
|
374
|
+
const { expanded, toggle } = useDisclosure();
|
|
375
|
+
const path = block.args.complete("file_path") ? block.args.text("file_path") : void 0;
|
|
376
|
+
const summary = path === void 0 || path === "" ? "" : abbreviateHomePath(relativize(path, cwd), home);
|
|
377
|
+
const kilobytes = contentKilobytes(block.args);
|
|
378
|
+
const size = kilobytes === null ? null : t("tool.preparing.content", { kilobytes });
|
|
379
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
380
|
+
className: tool_row_module_css_default.root,
|
|
381
|
+
"data-variant": variantOf(toolName),
|
|
382
|
+
"data-tool": toolName,
|
|
383
|
+
"data-state": "preparing",
|
|
384
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
385
|
+
className: tool_row_module_css_default.visuallyHidden,
|
|
386
|
+
children: t("row.preparing")
|
|
387
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.DisclosureRow, {
|
|
388
|
+
rowClassName: tool_row_module_css_default.row,
|
|
389
|
+
leadingClassName: tool_row_module_css_default.leading,
|
|
390
|
+
titleClassName: tool_row_module_css_default.title,
|
|
391
|
+
icon: MUTATION_ICON,
|
|
392
|
+
title: t(titleKeyOf(toolName)),
|
|
393
|
+
running: true,
|
|
394
|
+
open: expanded,
|
|
395
|
+
expandable: false,
|
|
396
|
+
onToggle: toggle,
|
|
397
|
+
collapsedContent: summary !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
398
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
399
|
+
className: tool_row_module_css_default.sep,
|
|
400
|
+
"aria-hidden": true
|
|
401
|
+
}),
|
|
402
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
403
|
+
className: tool_row_module_css_default.summary,
|
|
404
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.TextShimmer, { children: summary })
|
|
405
|
+
}),
|
|
406
|
+
size !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
407
|
+
className: tool_row_module_css_default.summarySuffix,
|
|
408
|
+
children: size
|
|
409
|
+
})
|
|
410
|
+
] })
|
|
411
|
+
})]
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
/** The dispatched and settled row: path link, diff totals, diff card body. */
|
|
415
|
+
function DispatchedRow({ toolName, block, cwd, home, openFile, inspect, useDisclosure, t }) {
|
|
416
|
+
const { expanded, toggle } = useDisclosure();
|
|
417
|
+
const model = mutationModel(toolName, block, cwd, home);
|
|
418
|
+
const kilobytes = contentKilobytes(block.args);
|
|
419
|
+
const size = kilobytes === null ? null : t("tool.preparing.content", { kilobytes });
|
|
420
|
+
const totals = model.diffs === null ? null : (0, _deepseek_ai_dsh_client_ui_primitives.diffTotals)(model.diffs);
|
|
421
|
+
const failure = errorSummary(model);
|
|
422
|
+
const summaryText = failure ?? model.summary;
|
|
423
|
+
const expandable = model.diffs !== null || model.output !== null;
|
|
424
|
+
const open = expanded && expandable;
|
|
425
|
+
const settledWithCue = model.state === "error" || model.state === "stopped";
|
|
426
|
+
const suffix = settledWithCue ? null : size;
|
|
427
|
+
const fileLink = model.linkPath !== void 0 && failure === null && !settledWithCue ? model.linkPath : null;
|
|
428
|
+
const labels = (0, react.useMemo)(() => diffLabels(t), [t]);
|
|
429
|
+
const openPath = (0, react.useMemo)(() => fileLink === null ? void 0 : () => {
|
|
430
|
+
openFile(fileLink);
|
|
431
|
+
}, [fileLink, openFile]);
|
|
432
|
+
const status = stateStatus(model.state, t);
|
|
433
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
434
|
+
className: tool_row_module_css_default.root,
|
|
435
|
+
"data-variant": variantOf(toolName),
|
|
436
|
+
"data-tool": toolName,
|
|
437
|
+
"data-state": model.state,
|
|
438
|
+
children: [status !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
439
|
+
className: tool_row_module_css_default.visuallyHidden,
|
|
440
|
+
children: status
|
|
441
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.DisclosureRow, {
|
|
442
|
+
rowClassName: tool_row_module_css_default.row,
|
|
443
|
+
leadingClassName: tool_row_module_css_default.leading,
|
|
444
|
+
titleClassName: tool_row_module_css_default.title,
|
|
445
|
+
icon: MUTATION_ICON,
|
|
446
|
+
title: t(titleKeyOf(toolName)),
|
|
447
|
+
running: model.state === "running",
|
|
448
|
+
open,
|
|
449
|
+
expandable,
|
|
450
|
+
expandOnRowClick: true,
|
|
451
|
+
keepContentWhenOpen: true,
|
|
452
|
+
onToggle: toggle,
|
|
453
|
+
collapsedContent: summaryText !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
454
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
455
|
+
className: tool_row_module_css_default.sep,
|
|
456
|
+
"aria-hidden": true
|
|
457
|
+
}),
|
|
458
|
+
fileLink !== null && openPath !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
459
|
+
type: "button",
|
|
460
|
+
className: tool_row_module_css_default.fileLink,
|
|
461
|
+
onClick: (event) => {
|
|
462
|
+
event.stopPropagation();
|
|
463
|
+
openPath();
|
|
464
|
+
},
|
|
465
|
+
onKeyDown: (event) => {
|
|
466
|
+
if (event.key === "Enter" || event.key === " ") event.stopPropagation();
|
|
467
|
+
},
|
|
468
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.TextShimmer, { children: summaryText })
|
|
469
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
470
|
+
className: clsx(tool_row_module_css_default.summary, failure !== null && tool_row_module_css_default.errorSummary, failure === null && model.state === "stopped" && tool_row_module_css_default.stoppedSummary),
|
|
471
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.TextShimmer, { children: summaryText })
|
|
472
|
+
}),
|
|
473
|
+
suffix !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.TextShimmer, {
|
|
474
|
+
className: tool_row_module_css_default.summarySuffix,
|
|
475
|
+
children: suffix
|
|
476
|
+
}),
|
|
477
|
+
totals !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.TextShimmer, {
|
|
478
|
+
className: clsx(tool_row_module_css_default.summarySuffix, tool_row_module_css_default.diffStat),
|
|
479
|
+
children: [
|
|
480
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
481
|
+
className: tool_row_module_css_default.diffAdded,
|
|
482
|
+
children: `+${totals.added}`
|
|
483
|
+
}),
|
|
484
|
+
" ",
|
|
485
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
486
|
+
className: tool_row_module_css_default.diffRemoved,
|
|
487
|
+
children: `-${totals.removed}`
|
|
488
|
+
})
|
|
489
|
+
]
|
|
490
|
+
})
|
|
491
|
+
] }),
|
|
492
|
+
children: open && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
493
|
+
className: tool_row_module_css_default.bodyWrap,
|
|
494
|
+
children: [model.diffs !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.DiffBlock, {
|
|
495
|
+
diffs: model.diffs,
|
|
496
|
+
labels,
|
|
497
|
+
maxLines: CHAT_DIFF_MAX_LINES,
|
|
498
|
+
className: tool_row_module_css_default.diffBody
|
|
499
|
+
}) : model.output !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
500
|
+
className: tool_row_module_css_default.ioCard,
|
|
501
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
502
|
+
className: tool_row_module_css_default.ioSection,
|
|
503
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
504
|
+
className: tool_row_module_css_default.ioLabel,
|
|
505
|
+
children: t("row.output")
|
|
506
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
507
|
+
className: tool_row_module_css_default.ioText,
|
|
508
|
+
"data-error": model.state === "error" || void 0,
|
|
509
|
+
children: model.output
|
|
510
|
+
})]
|
|
511
|
+
})
|
|
512
|
+
}), inspect !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
513
|
+
type: "button",
|
|
514
|
+
className: tool_row_module_css_default.inspectButton,
|
|
515
|
+
onClick: inspect,
|
|
516
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconInspectOutlineRegular, {}), t("row.inspect")]
|
|
517
|
+
})]
|
|
518
|
+
})
|
|
519
|
+
})]
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* The registered `edit`/`write` Tool view.
|
|
524
|
+
* @param props - keyed toolview owner currency plus the locale seat.
|
|
525
|
+
* @returns the preparing or dispatched row for the call's current stage.
|
|
526
|
+
*/
|
|
527
|
+
function PtcDiffRow(props) {
|
|
528
|
+
return props.phase === "preparing" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PreparingRow, {
|
|
529
|
+
toolName: props.toolName,
|
|
530
|
+
block: props.block,
|
|
531
|
+
cwd: props.cwd,
|
|
532
|
+
home: props.home,
|
|
533
|
+
useDisclosure: props.useDisclosure,
|
|
534
|
+
t: props.t
|
|
535
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DispatchedRow, {
|
|
536
|
+
toolName: props.toolName,
|
|
537
|
+
block: props.block,
|
|
538
|
+
cwd: props.cwd,
|
|
539
|
+
home: props.home,
|
|
540
|
+
openFile: props.openFile,
|
|
541
|
+
inspect: props.inspect,
|
|
542
|
+
useDisclosure: props.useDisclosure,
|
|
543
|
+
t: props.t
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
// src/client/index.tsx
|
|
547
|
+
/** Required service: the slot registry. */
|
|
548
|
+
const inject = ["slots"];
|
|
549
|
+
/** The wire Tools whose cards this plugin renders. */
|
|
550
|
+
const MUTATION_TOOLS = ["edit", "write"];
|
|
551
|
+
/** The dictionary namespace the row's copy is translated through. */
|
|
552
|
+
const NS = "conversation";
|
|
553
|
+
/**
|
|
554
|
+
* Mount the diff rows over the shipped file-mutation rows.
|
|
555
|
+
* @param ctx - client root context.
|
|
556
|
+
*/
|
|
557
|
+
function apply(ctx) {
|
|
558
|
+
ctx.slots.inject("tool.call.toolview", function* () {
|
|
559
|
+
for (const key of MUTATION_TOOLS) try {
|
|
560
|
+
yield ctx.slots.register({
|
|
561
|
+
name: "tool.call.toolview",
|
|
562
|
+
key,
|
|
563
|
+
locale: NS,
|
|
564
|
+
priority: -1
|
|
565
|
+
}, PtcDiffRow);
|
|
566
|
+
} catch (error) {
|
|
567
|
+
console.error(`[dsh-ptc-diff-card] cannot take over the "${key}" tool view; the shipped row stays in charge`, error);
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
exports.apply = apply;
|
|
572
|
+
exports.inject = inject;
|
|
573
|
+
return module.exports;
|
|
574
|
+
}
|
|
575
|
+
});
|
package/lib/index.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,78 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-ptc-diff-card",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"author": "GrounzerLiu",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/GrounzerLiu/dsh-ptc-diff-card.git"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/GrounzerLiu/dsh-ptc-diff-card#readme",
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/GrounzerLiu/dsh-ptc-diff-card/issues"
|
|
12
|
+
},
|
|
13
|
+
"description": "DSH Web 插件:PTC 模式(run_code 内的嵌套调用)里的 edit/write 显示标准 diff 卡片 | DSH Web GUI plugin: standard diff cards for edit/write calls made inside PTC programs",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"main": "lib/index.js",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": "./lib/index.js",
|
|
18
|
+
"./client": "./lib/client.js",
|
|
19
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
20
|
+
"./package.json": "./package.json"
|
|
21
|
+
},
|
|
22
|
+
"dsh": {
|
|
23
|
+
"bundle": {
|
|
24
|
+
"patch": "./cordis.patch.yml"
|
|
25
|
+
},
|
|
26
|
+
"client": {
|
|
27
|
+
"inject": [],
|
|
28
|
+
"platform": "web"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"dsh": ">=0.2.1-rc.2"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"lib",
|
|
36
|
+
"cordis.patch.yml",
|
|
37
|
+
"README.md",
|
|
38
|
+
"README.en.md",
|
|
39
|
+
"VERIFY.md",
|
|
40
|
+
"LICENSE"
|
|
41
|
+
],
|
|
42
|
+
"scripts": {
|
|
43
|
+
"prepublishOnly": "pnpm run check",
|
|
44
|
+
"pack:check": "npm pack --dry-run",
|
|
45
|
+
"build": "tsdown",
|
|
46
|
+
"typecheck": "tsc -p tsconfig.json --noUnusedLocals --noUnusedParameters",
|
|
47
|
+
"check:model": "node --experimental-strip-types scripts/model-check.ts",
|
|
48
|
+
"check": "pnpm run typecheck && pnpm run check:model && pnpm run build"
|
|
49
|
+
},
|
|
50
|
+
"keywords": [
|
|
51
|
+
"deepseek",
|
|
52
|
+
"harness",
|
|
53
|
+
"dsh",
|
|
54
|
+
"dsh-plugin",
|
|
55
|
+
"ptc",
|
|
56
|
+
"diff",
|
|
57
|
+
"ui"
|
|
58
|
+
],
|
|
59
|
+
"license": "MIT",
|
|
60
|
+
"dependencies": {
|
|
61
|
+
"clsx": "^2.1.1"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@deepseek-ai/cordis": "4.0.5",
|
|
65
|
+
"@deepseek-ai/dsh-client-ui-chat": "0.2.1-rc.2",
|
|
66
|
+
"@deepseek-ai/dsh-client-ui-conversation": "0.2.1-rc.2",
|
|
67
|
+
"@deepseek-ai/dsh-client-ui-primitives": "0.2.1-rc.2",
|
|
68
|
+
"@deepseek-ai/dsh-client-ui-renderer": "0.2.1-rc.2",
|
|
69
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.2.1-rc.2",
|
|
70
|
+
"@deepseek-ai/dsh-client-ui-tool": "0.2.1-rc.2",
|
|
71
|
+
"@deepseek-ai/dsh-util-values": "0.2.1-rc.2",
|
|
72
|
+
"@types/react": "~18.3.1",
|
|
73
|
+
"lightningcss": "^1.30.0",
|
|
74
|
+
"react": "^18.2.0",
|
|
75
|
+
"tsdown": "^0.22.2",
|
|
76
|
+
"typescript": "^5.9.3"
|
|
77
|
+
}
|
|
78
|
+
}
|