@yangzhe1991/dsh-code-review 0.1.4 → 0.2.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 +18 -0
- package/README.zh.md +13 -0
- package/lib/client.js +363 -11
- package/lib/client.js.map +4 -4
- package/lib/index.js +77 -2
- package/lib/index.js.map +2 -2
- package/lib/types/index.d.ts +18 -2
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -11,6 +11,13 @@
|
|
|
11
11
|
|
|
12
12
|
The conversation itself stays untouched: when a git diff shows up, a small **"open in new tab"** button appears on the block (a markdown diff code block, or the bash tool row that produced the diff — even while collapsed). One click opens the review page.
|
|
13
13
|
|
|
14
|
+
When the conversation mentions a **local `.diff` / `.patch` file path**, the path automatically becomes a **view link button** — a path inside backticks gets an inline pill right after it, a bare-text path gets a right-aligned button row under the message. One click opens that file's code review page (the browser cannot read local files, so the plugin host half reads them — absolute paths only, `.diff`/`.patch` suffix only, ≤ 5 MB).
|
|
15
|
+
|
|
16
|
+
## Compatibility
|
|
17
|
+
|
|
18
|
+
- **dsh ≥ 0.1.2-alpha.4** — fully supported since **0.1.5**. The web UI restructure (chat snapshot moved from `useSession((s) => s.chat)` to the session-standard `useChat` hook; markdown code-block DOM wrapped in a `bannerWrap` layer) is handled on both detection paths: markdown diff blocks and collapsed bash tool rows.
|
|
19
|
+
- **dsh 0.1.0-rc.x** — still supported via the legacy snapshot path.
|
|
20
|
+
|
|
14
21
|
## Features
|
|
15
22
|
|
|
16
23
|
- ⬅️➡️ **Two-column side-by-side view** — the old file on the left, the new file on the right, each with its own line numbers, exactly like a GitHub split review.
|
|
@@ -23,6 +30,7 @@ The conversation itself stays untouched: when a git diff shows up, a small **"op
|
|
|
23
30
|
- 🎨 **Theme-matched** — the page inherits the DSH theme colors at open time, so light/dark mode stays consistent.
|
|
24
31
|
- 📜 **Full-page scrolling** — no height caps; thousands of lines scroll naturally, and long lines wrap.
|
|
25
32
|
- 💬 **Inline comments** — hover any line number and click to add a comment right on that line (like GitHub reviews). Comments accumulate with a per-line count badge; when done, submit them all at once — as **Looks Good To Me ✓** or **plain comments** — and they are written into the DSH composer as `file:line — comment` lines, ready to send to the agent.
|
|
33
|
+
- 🔗 **One-click local diff paths** — absolute `…/xxx.diff` / `…/xxx.patch` paths mentioned in the conversation become link buttons: backtick-wrapped paths get an inline pill right after them, bare-text paths get a right-aligned button row under the message, and paths listed in bash tool output join the tool row's button row. Clicking opens that file's code review page; on read failure (missing / not a diff / over 5 MB) the button briefly turns red.
|
|
26
34
|
- 🌐 **Bilingual UI** — follows the DSH interface language (official locale service): a Chinese UI gets Chinese buttons and review page, everything else gets English.
|
|
27
35
|
|
|
28
36
|
## Screenshots
|
|
@@ -37,9 +45,15 @@ The conversation itself stays untouched: when a git diff shows up, a small **"op
|
|
|
37
45
|
|
|
38
46
|
1. **Markdown code blocks** in assistant messages — a ```` ```diff ```` / ```` ```patch ```` fence, or any code block whose content looks like a git diff (e.g. pasted inside a `text` fence). The block is **collapsed by default** (just the title row + buttons), with an "expand/collapse" toggle if you want to read the raw text inline.
|
|
39
47
|
2. **Bash tool rows** — when the agent runs `git diff` in a bash tool, the button is added to the tool row (visible even while the row is collapsed; detected from the conversation data, since a collapsed row has no output in the DOM). The terminal card in the details panel gets one too.
|
|
48
|
+
3. **Local diff paths (since 0.2.0)** — absolute `.diff`/`.patch` paths in assistant or user messages:
|
|
49
|
+
- backtick-wrapped paths (e.g. `` `/Users/me/proj/patch.diff` ``) get an **inline pill button** right after the path;
|
|
50
|
+
- bare-text paths get a **right-aligned button row under the message** (one pill per path); paths inside reasoning blocks and code blocks are not captured (collapsed by default / have their own button mechanism);
|
|
51
|
+
- paths listed in bash tool output join the **tool row's button row**.
|
|
40
52
|
|
|
41
53
|
Streaming output is handled gracefully: the button only appears once the content has been stable for ~1 second, so half-streamed diffs never produce a broken page.
|
|
42
54
|
|
|
55
|
+
When a path button is clicked, the plugin's host half (a route registered on `webServer`) reads the local file (absolute paths only, `~` expansion, `.diff`/`.patch` suffix, ≤ 5 MB, no CORS), and the browser half renders it into the standalone review page; on read failure the button briefly turns red. **The route registration requires a `dsh web` restart** (the host half registers it at startup); browser-half changes only need a hard refresh.
|
|
56
|
+
|
|
43
57
|
## Install (30 seconds)
|
|
44
58
|
|
|
45
59
|
```sh
|
|
@@ -58,6 +72,8 @@ dsh plugin --profile web add link:/path/to/@yangzhe1991/dsh-code-review
|
|
|
58
72
|
|
|
59
73
|
The plugin registers a root-scope `shell.overlay` seat that renders nothing and watches the whole document with a `MutationObserver`. For every `.md-code-block` and `[data-terminal]` element it runs a stability-debounced check; candidates get a small button appended to the block's banner/header (never replacing official nodes, so React's reconciliation stays safe). Collapsed bash tool rows are handled from the data layer: a session-scope seat subscribes to the conversation snapshot and records settled bash results that look like a diff.
|
|
60
74
|
|
|
75
|
+
Local path chips are split across the two layers: message rows (assistant/user) are probed by the same scanner — backtick-wrapped paths get an inline button inserted right after the `code` element, bare-text paths get a button row appended to the row; paths in bash tool results go through the data layer (same pipeline as the diff buttons). Text nodes are only read, never split (splitting React-managed text nodes would break streaming rendering), so bare-text paths get row-level buttons instead of in-place edits. On click, the browser half first opens a placeholder tab synchronously (beats popup blockers), then fetches `GET /dsh-code-review/diff?path=…` from the host route and navigates the tab to the standalone review page.
|
|
76
|
+
|
|
61
77
|
Clicking the button synchronously builds a self-contained HTML page (inline styles + inherited theme variables + the two-column content, all HTML-escaped) and opens it via a **Blob URL** — no server route needed, and the synchronous `window.open` inside the click gesture is not popup-blocked. The parser is a pure module (`src/client/diff-parse.ts`, unit-tested in `test/parse.test.mjs`) and supports: multi-file diffs, hunks with line-number tracking, delete/add pairing into changed rows (uneven counts degrade to single-side rows), `` markers, new/deleted/renamed/binary files, bare patches without a `diff --git` header, and commit-message prefixes (`git show` output).
|
|
62
78
|
|
|
63
79
|
## Development
|
|
@@ -66,6 +82,8 @@ Clicking the button synchronously builds a self-contained HTML page (inline styl
|
|
|
66
82
|
npm install
|
|
67
83
|
npm run build # esbuild → lib/index.js (host half) + lib/client.js (browser half)
|
|
68
84
|
node test/parse.test.mjs # parser + standalone HTML unit tests
|
|
85
|
+
node test/path-detect.test.mjs # local path-detection regex unit tests
|
|
86
|
+
node test/node-route.test.mjs # host-half route (read/validation/error codes) tests
|
|
69
87
|
node test/client-smoke.test.mjs # bundle smoke test (mock __ModuleLoader__)
|
|
70
88
|
```
|
|
71
89
|
|
package/README.zh.md
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
|
|
12
12
|
聊天窗口本身保持原样:只要出现 git diff,对应块(markdown diff 代码块、或产生 diff 的 bash 工具行——折叠状态也显示)上就自动出现一个**「在新标签页打开」**小按钮,点一下即打开审查页。
|
|
13
13
|
|
|
14
|
+
对话里提到**本地 .diff / .patch 文件路径**时,路径会自动变成**查看链接按钮**——反引号里的路径原地变成小胶囊按钮,纯文本路径在消息下方补一行按钮;点击直接打开该文件的 Code Review 审查页(浏览器读不了本地文件,由插件宿主侧代读,只读绝对路径、仅限 .diff/.patch 后缀、≤5MB)。
|
|
15
|
+
|
|
14
16
|
## 功能
|
|
15
17
|
|
|
16
18
|
- ⬅️➡️ **双列并排视图** —— 旧文件在左、新文件在右,两侧各自带行号,类似 GitHub 的 split review。
|
|
@@ -23,6 +25,7 @@
|
|
|
23
25
|
- 🎨 **主题一致** —— 打开时继承 DSH 当前主题色,深浅色模式自动跟随。
|
|
24
26
|
- 📜 **整页滚动** —— 无高度限制,几千行自然滚动,超长行折行显示。
|
|
25
27
|
- 💬 **行内评论** —— 悬停任意行号点击即可在该行提评论(类似 GitHub review),行号旁显示评论数徽章;攒齐后一次性提交 —— 可选 **Looks Good To Me ✓** 或**纯评论**,提交后自动写入 DSH 对话框(`文件名:行号 — 评论` 逐条列出),直接发给 agent。
|
|
28
|
+
- 🔗 **本地路径一键查看** —— 对话里出现 `…/xxx.diff`/`…/xxx.patch` 绝对路径时自动变成链接按钮:反引号内的路径原地变成小胶囊按钮,纯文本路径在消息下方补一行右对齐按钮;bash 工具输出里列出的路径也会出现在工具行按钮排上。点击后打开该文件的 Code Review 页,读取失败(文件不存在/非 diff/超 5MB)时按钮短暂变红提示。
|
|
26
29
|
- 🌐 **中英双语** —— 跟随 DSH 界面语言(官方 locale 服务):中文界面显示中文按钮与审查页,其他语言一律英文。
|
|
27
30
|
|
|
28
31
|
## 截图
|
|
@@ -37,9 +40,15 @@
|
|
|
37
40
|
|
|
38
41
|
1. **assistant 消息里的 markdown 代码块** —— ```` ```diff ```` / ```` ```patch ```` 语言标签,或内容形似 git diff 的任意代码块(比如贴在 `text` 围栏里)。代码块**默认折叠**(只留标题行 + 按钮),带「展开/收起」开关,想看原始文本就内联展开。
|
|
39
42
|
2. **bash 工具行** —— agent 在 bash 工具里跑 `git diff` 后,按钮加在工具行上(折叠状态也可见;折叠行 DOM 里没有输出内容,从会话数据层检测)。详情面板里的终端卡片同样支持。
|
|
43
|
+
3. **本地 diff 路径(0.2.0 起)** —— assistant / 用户消息里出现绝对路径的 `.diff`/`.patch` 文件时:
|
|
44
|
+
- 反引号内的路径(如 `` `/Users/me/proj/patch.diff` ``)原地变成**内联胶囊按钮**,跟在路径后面;
|
|
45
|
+
- 纯文本路径在**消息下方补一行右对齐按钮**(多条路径逐个列出);思考块与代码块内的路径不捕获(前者默认折叠、后者已有自身机制);
|
|
46
|
+
- bash 工具输出里列出的路径加在**工具行按钮排**上。
|
|
40
47
|
|
|
41
48
|
流式输出有专门处理:内容稳定约 1 秒后按钮才出现,半截 diff 不会产生残缺页面。
|
|
42
49
|
|
|
50
|
+
点击路径按钮时:插件宿主侧(`webServer` 注册的路由)读取本地文件内容(仅绝对路径、支持 `~` 展开、仅 `.diff`/`.patch` 后缀、≤5MB、不设 CORS),浏览器侧再把内容渲染成独立审查页;读取失败时按钮短暂变红提示。**读取需重启 `dsh web` 才生效**(node 半在宿主启动时注册路由),浏览器半改动硬刷新即可。
|
|
51
|
+
|
|
43
52
|
## 安装(30 秒)
|
|
44
53
|
|
|
45
54
|
```sh
|
|
@@ -58,6 +67,8 @@ dsh plugin --profile web add link:/path/to/@yangzhe1991/dsh-code-review
|
|
|
58
67
|
|
|
59
68
|
插件注册一个 root 作用域的 `shell.overlay` 席位,自身渲染 null,用 `MutationObserver` 监听全文档。对每个 `.md-code-block` 和 `[data-terminal]` 元素做「稳定去抖」检测,候选块会在官方 banner/header 上追加一个小按钮(只追加、不改写官方节点,React 协调安全)。折叠的 bash 工具行走数据层:session 作用域组件订阅会话快照,把输出形似 diff 的已落地 bash 结果记录进文本表,扫描器给对应工具行加按钮。
|
|
60
69
|
|
|
70
|
+
本地路径 chip 的分工:消息行(assistant/用户)由同一扫描器按行探测,反引号内的路径在 code 元素后原地插内联按钮,纯文本路径在行尾追加一排按钮;bash 工具结果里的路径走数据层(与 diff 按钮同一条数据链路)。文本节点只读不拆(对 React 管理的文本节点做拆分会破坏流式渲染),所以纯文本路径不做原地改造。点击后浏览器半先同步开占位窗(防弹窗拦截),再向宿主路由 `GET /dsh-code-review/diff?path=…` 拉取内容,成功即导航到独立审查页。
|
|
71
|
+
|
|
61
72
|
点击按钮时,**在用户手势内同步**构建自包含 HTML 页面(内联样式 + 继承的主题变量 + 双列内容,全部 HTML 转义),通过 **Blob URL** 打开 —— 不依赖服务器任何路由,同步调用的 `window.open` 也不会被弹窗拦截。解析器是纯模块(`src/client/diff-parse.ts`,`test/parse.test.mjs` 有单测),支持:多文件 diff、带行号跟踪的 hunk、删除/新增配对成变更行(数量不等时多出的行退化为单侧行)、`` 标记、新增/删除/重命名/二进制文件、无 `diff --git` 头的裸 patch、`git show` 的提交说明前缀。
|
|
62
73
|
|
|
63
74
|
## 开发
|
|
@@ -66,6 +77,8 @@ dsh plugin --profile web add link:/path/to/@yangzhe1991/dsh-code-review
|
|
|
66
77
|
npm install
|
|
67
78
|
npm run build # esbuild → lib/index.js(node 半)+ lib/client.js(浏览器半)
|
|
68
79
|
node test/parse.test.mjs # 解析器 + 独立页 HTML 单测
|
|
80
|
+
node test/path-detect.test.mjs # 本地路径检测正则单测
|
|
81
|
+
node test/node-route.test.mjs # node 半路由(读取/校验/错误码)单测
|
|
69
82
|
node test/client-smoke.test.mjs # bundle 冒烟测试(mock __ModuleLoader__)
|
|
70
83
|
```
|
|
71
84
|
|
package/lib/client.js
CHANGED
|
@@ -231,6 +231,60 @@ function countStats(files) {
|
|
|
231
231
|
return { added, removed, files: files.length };
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
+
// src/client/diff-path.ts
|
|
235
|
+
var ABS_ALT = "(?<![\\w/])/(?!/)[^\\s]*?\\.(?:diff|patch)";
|
|
236
|
+
var TILDE_ALT = "(?<![\\w/])~/[^\\s]*?\\.(?:diff|patch)";
|
|
237
|
+
var SUFFIX_BOUNDARY = "(?![\\w\\-/]|\\.(?=\\w))";
|
|
238
|
+
var PATH_RE = new RegExp(`(?:${ABS_ALT}|${TILDE_ALT})${SUFFIX_BOUNDARY}`, "gi");
|
|
239
|
+
var WRAPPERS = [
|
|
240
|
+
['"', '"'],
|
|
241
|
+
["'", "'"],
|
|
242
|
+
["`", "`"],
|
|
243
|
+
["(", ")"],
|
|
244
|
+
["[", "]"],
|
|
245
|
+
["{", "}"],
|
|
246
|
+
["<", ">"],
|
|
247
|
+
["\u300C", "\u300D"],
|
|
248
|
+
["\u300E", "\u300F"],
|
|
249
|
+
["\uFF08", "\uFF09"],
|
|
250
|
+
["\u3010", "\u3011"]
|
|
251
|
+
];
|
|
252
|
+
function unwrap(s) {
|
|
253
|
+
let t = s;
|
|
254
|
+
let changed = true;
|
|
255
|
+
while (changed) {
|
|
256
|
+
changed = false;
|
|
257
|
+
for (const [left, right] of WRAPPERS) {
|
|
258
|
+
if (t.length > left.length + right.length && t.startsWith(left) && t.endsWith(right)) {
|
|
259
|
+
t = t.slice(left.length, -right.length);
|
|
260
|
+
changed = true;
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return t;
|
|
266
|
+
}
|
|
267
|
+
function findDiffPaths(text) {
|
|
268
|
+
const out = [];
|
|
269
|
+
const seen = /* @__PURE__ */ new Set();
|
|
270
|
+
for (const match of text.matchAll(PATH_RE)) {
|
|
271
|
+
const p = match[0];
|
|
272
|
+
if (seen.has(p)) continue;
|
|
273
|
+
seen.add(p);
|
|
274
|
+
out.push(p);
|
|
275
|
+
}
|
|
276
|
+
return out;
|
|
277
|
+
}
|
|
278
|
+
function cleanDiffPath(raw) {
|
|
279
|
+
const cleaned = unwrap(raw.trim());
|
|
280
|
+
if (cleaned === "" || !cleaned.startsWith("/") && !cleaned.startsWith("~/")) {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
const matches = findDiffPaths(cleaned);
|
|
284
|
+
if (matches.length !== 1 || matches[0] !== cleaned) return null;
|
|
285
|
+
return cleaned;
|
|
286
|
+
}
|
|
287
|
+
|
|
234
288
|
// src/client/diff-highlight.ts
|
|
235
289
|
var KEYWORDS = /* @__PURE__ */ new Set([
|
|
236
290
|
// JS/TS
|
|
@@ -739,6 +793,9 @@ var CR_TEXTS = {
|
|
|
739
793
|
collapse: "\u6536\u8D77",
|
|
740
794
|
expandTitle: "\u5C55\u5F00\u539F\u59CB diff \u6587\u672C",
|
|
741
795
|
collapseTitle: "\u6536\u8D77\u539F\u59CB diff \u6587\u672C",
|
|
796
|
+
viewPath: "\u67E5\u770B",
|
|
797
|
+
viewPathTitle: (path) => `\u5728\u72EC\u7ACB\u6807\u7B7E\u9875\u67E5\u770B\u672C\u5730 diff \u6587\u4EF6:${path}`,
|
|
798
|
+
pathReadFailed: "\u8BFB\u53D6\u5931\u8D25",
|
|
742
799
|
reportLgtmHeader: (count) => `Looks good to me \u2713 \u2014 Code Review \u5171 ${count} \u6761\u8BC4\u8BBA:`,
|
|
743
800
|
reportCommentHeader: (count) => `Code Review \u610F\u89C1,\u5171 ${count} \u6761:`,
|
|
744
801
|
reportLgtmBare: "Looks good to me \u2713",
|
|
@@ -766,6 +823,9 @@ var CR_TEXTS = {
|
|
|
766
823
|
collapse: "Collapse",
|
|
767
824
|
expandTitle: "Show raw diff text",
|
|
768
825
|
collapseTitle: "Hide raw diff text",
|
|
826
|
+
viewPath: "View",
|
|
827
|
+
viewPathTitle: (path) => `Open the local diff file in a separate tab: ${path}`,
|
|
828
|
+
pathReadFailed: "Failed to read",
|
|
769
829
|
reportLgtmHeader: (count) => `Looks good to me \u2713 \u2014 Code Review with ${count} comment${count === 1 ? "" : "s"}:`,
|
|
770
830
|
reportCommentHeader: (count) => `Code Review comments (${count}):`,
|
|
771
831
|
reportLgtmBare: "Looks good to me \u2713",
|
|
@@ -1118,6 +1178,55 @@ var BUTTON_CSS = `
|
|
|
1118
1178
|
color: var(--dsw-alias-label-primary);
|
|
1119
1179
|
border-color: var(--dsw-alias-border-l3);
|
|
1120
1180
|
}
|
|
1181
|
+
/* \u6309\u94AE\u884C\u5BB9\u5668:\u4E24\u4E2A\u6309\u94AE\u88C5\u8FDB\u4E00\u4E2A\u53F3\u5BF9\u9F50\u7684\u72EC\u7ACB\u884C\u3002
|
|
1182
|
+
* \u65B0\u524D\u7AEF\u4EE3\u7801\u5757\u7ED3\u6784\u662F bannerWrap(\u5757\u7EA7 sticky)\u5305\u7740 banner,\u76F4\u63A5 append
|
|
1183
|
+
* \u4F1A\u88AB\u6324\u5728 banner \u4E4B\u4E0B\u4E14\u9760\u5DE6;\u5B98\u65B9\u590D\u5236\u6309\u94AE\u6240\u5728 .action \u4E5F\u662F flex \u5E76\u6392,
|
|
1184
|
+
* \u63D2\u4EF6\u6309\u94AE\u585E\u8FDB\u53BB\u4F1A\u8DDF\u590D\u5236\u6309\u94AE\u4E89\u4F4D\u3002\u505A\u6210\u72EC\u7ACB\u884C\u540E:\u65E7 banner(flex)\u91CC\u5B83\u662F
|
|
1185
|
+
* \u9760\u53F3\u7684 flex \u9879,\u65B0 bannerWrap(\u5757\u7EA7)\u91CC\u5B83\u6574\u884C\u53F3\u5BF9\u9F50,\u907F\u514D\u4E89\u4F4D\u3002 */
|
|
1186
|
+
.dsh-cr-row {
|
|
1187
|
+
display: flex;
|
|
1188
|
+
justify-content: flex-end;
|
|
1189
|
+
align-items: center;
|
|
1190
|
+
gap: 8px;
|
|
1191
|
+
flex: none;
|
|
1192
|
+
margin-left: auto;
|
|
1193
|
+
padding: 4px 12px 7px;
|
|
1194
|
+
}
|
|
1195
|
+
/* \u672C\u5730\u8DEF\u5F84\u67E5\u770B chip:\u53CD\u5F15\u53F7\u5185\u8054\u5F62\u6001\u4E0E\u6D88\u606F\u7EA7/\u5DE5\u5177\u884C\u5F62\u6001\u5171\u7528,
|
|
1196
|
+
* \u4E3B\u8272\u63CF\u8FB9\u7684\u5C0F\u80F6\u56CA;\u5185\u8054\u5F62\u6001\u66F4\u5C0F\u66F4\u9690\u8EAB,\u4E0D\u62A2\u6B63\u6587\u89C6\u7EBF\u3002 */
|
|
1197
|
+
.dsh-cr-pathchip {
|
|
1198
|
+
display: inline-flex;
|
|
1199
|
+
align-items: center;
|
|
1200
|
+
gap: 4px;
|
|
1201
|
+
max-width: min(320px, 100%);
|
|
1202
|
+
font-size: 12px;
|
|
1203
|
+
line-height: 18px;
|
|
1204
|
+
color: var(--dsw-alias-state-business-primary, #3b6fe0);
|
|
1205
|
+
background: var(--dsw-alias-bg-base, #ffffff);
|
|
1206
|
+
border: 1px solid var(--dsw-alias-border-l2, #e5e5e7);
|
|
1207
|
+
border-radius: 8px;
|
|
1208
|
+
padding: 1px 8px;
|
|
1209
|
+
cursor: pointer;
|
|
1210
|
+
white-space: nowrap;
|
|
1211
|
+
overflow: hidden;
|
|
1212
|
+
text-overflow: ellipsis;
|
|
1213
|
+
vertical-align: middle;
|
|
1214
|
+
}
|
|
1215
|
+
.dsh-cr-pathchip:hover {
|
|
1216
|
+
border-color: var(--dsw-alias-state-business-primary, #3b6fe0);
|
|
1217
|
+
background: var(--dsw-alias-bg-layer-1, #f7f7f8);
|
|
1218
|
+
}
|
|
1219
|
+
.dsh-cr-pathchip[data-error='true'] {
|
|
1220
|
+
color: var(--dsw-alias-state-error-primary, #d92d20);
|
|
1221
|
+
border-color: var(--dsw-alias-state-error-primary, #d92d20);
|
|
1222
|
+
}
|
|
1223
|
+
/* \u5185\u8054\u5F62\u6001:\u8DDF\u5728 code \u5143\u7D20\u540E,\u4E0D\u6539\u6BB5\u843D\u884C\u9AD8 */
|
|
1224
|
+
.dsh-cr-pathchip[data-inline='true'] {
|
|
1225
|
+
margin: 0 2px 0 4px;
|
|
1226
|
+
padding: 0 6px;
|
|
1227
|
+
font-size: 11px;
|
|
1228
|
+
line-height: 16px;
|
|
1229
|
+
}
|
|
1121
1230
|
`;
|
|
1122
1231
|
function ensureStyle() {
|
|
1123
1232
|
if (styleInjected || typeof document === "undefined") return;
|
|
@@ -1209,7 +1318,15 @@ function handleReviewMessage(event) {
|
|
|
1209
1318
|
}
|
|
1210
1319
|
function ensureButtons(host, getText, fold) {
|
|
1211
1320
|
const texts = CR_TEXTS[detectLang()];
|
|
1212
|
-
|
|
1321
|
+
let row = host.querySelector("[data-dsh-cr-row]");
|
|
1322
|
+
if (row === null) {
|
|
1323
|
+
const created = document.createElement("div");
|
|
1324
|
+
created.className = "dsh-cr-row";
|
|
1325
|
+
created.dataset.dshCrRow = "";
|
|
1326
|
+
host.appendChild(created);
|
|
1327
|
+
row = created;
|
|
1328
|
+
}
|
|
1329
|
+
if (row.querySelector("[data-dsh-cr-btn]") === null) {
|
|
1213
1330
|
const btn = document.createElement("button");
|
|
1214
1331
|
btn.type = "button";
|
|
1215
1332
|
btn.className = "dsh-cr-openbtn";
|
|
@@ -1222,9 +1339,9 @@ function ensureButtons(host, getText, fold) {
|
|
|
1222
1339
|
const text = getText();
|
|
1223
1340
|
if (text !== null) openReviewTab(text);
|
|
1224
1341
|
});
|
|
1225
|
-
|
|
1342
|
+
row.appendChild(btn);
|
|
1226
1343
|
}
|
|
1227
|
-
if (fold !== null &&
|
|
1344
|
+
if (fold !== null && row.querySelector("[data-dsh-cr-fold]") === null) {
|
|
1228
1345
|
const btn = document.createElement("button");
|
|
1229
1346
|
btn.type = "button";
|
|
1230
1347
|
btn.className = "dsh-cr-foldbtn";
|
|
@@ -1236,13 +1353,163 @@ function ensureButtons(host, getText, fold) {
|
|
|
1236
1353
|
event.preventDefault();
|
|
1237
1354
|
fold.onToggle();
|
|
1238
1355
|
});
|
|
1239
|
-
|
|
1356
|
+
row.appendChild(btn);
|
|
1240
1357
|
}
|
|
1241
1358
|
}
|
|
1242
1359
|
function removeButtons(host) {
|
|
1243
1360
|
for (const btn of host.querySelectorAll("[data-dsh-cr-btn], [data-dsh-cr-fold]")) {
|
|
1244
1361
|
btn.remove();
|
|
1245
1362
|
}
|
|
1363
|
+
const row = host.querySelector("[data-dsh-cr-row]");
|
|
1364
|
+
if (row instanceof HTMLElement && row.querySelector("[data-dsh-cr-btn], [data-dsh-cr-fold], [data-dsh-cr-pathchip]") === null) {
|
|
1365
|
+
row.remove();
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
function basenameOf(path) {
|
|
1369
|
+
const i = path.lastIndexOf("/");
|
|
1370
|
+
return i >= 0 ? path.slice(i + 1) : path;
|
|
1371
|
+
}
|
|
1372
|
+
function openPathReviewTab(path, chip) {
|
|
1373
|
+
void (async () => {
|
|
1374
|
+
let html;
|
|
1375
|
+
try {
|
|
1376
|
+
const res = await fetch(`/dsh-code-review/diff?path=${encodeURIComponent(path)}`, { cache: "no-store" });
|
|
1377
|
+
if (!res.ok) throw new Error(`http ${res.status}`);
|
|
1378
|
+
const data = await res.json();
|
|
1379
|
+
if (data.ok !== true || typeof data.text !== "string") throw new Error(data.error ?? "bad-response");
|
|
1380
|
+
if (parseGitDiff(data.text).length === 0) throw new Error("not-a-diff");
|
|
1381
|
+
html = buildStandaloneHtml(parseGitDiff(data.text), collectThemeVars(), data.text, detectLang());
|
|
1382
|
+
} catch (err) {
|
|
1383
|
+
console.warn("[dsh-code-review] \u672C\u5730 diff \u8BFB\u53D6\u5931\u8D25:", path, err);
|
|
1384
|
+
markPathChipError(chip);
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
const url = URL.createObjectURL(new Blob([html], { type: "text/html" }));
|
|
1388
|
+
const win = window.open(url, "_blank");
|
|
1389
|
+
if (win !== null) {
|
|
1390
|
+
reviewWindows.add(win);
|
|
1391
|
+
window.setTimeout(() => URL.revokeObjectURL(url), 6e4);
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
const fallback = window.open("", "_blank");
|
|
1395
|
+
if (fallback !== null) {
|
|
1396
|
+
try {
|
|
1397
|
+
fallback.document.open();
|
|
1398
|
+
fallback.document.write(html);
|
|
1399
|
+
fallback.document.close();
|
|
1400
|
+
reviewWindows.add(fallback);
|
|
1401
|
+
return;
|
|
1402
|
+
} catch (err) {
|
|
1403
|
+
console.warn("[dsh-code-review] \u5360\u4F4D\u7A97\u5199\u5165\u5931\u8D25:", path, err);
|
|
1404
|
+
fallback.close();
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
markPathChipError(chip);
|
|
1408
|
+
})();
|
|
1409
|
+
}
|
|
1410
|
+
function markPathChipError(chip) {
|
|
1411
|
+
if (chip === null || !chip.isConnected) return;
|
|
1412
|
+
const texts = CR_TEXTS[detectLang()];
|
|
1413
|
+
const path = chip.dataset.path ?? "";
|
|
1414
|
+
chip.dataset.error = "true";
|
|
1415
|
+
chip.title = texts.pathReadFailed;
|
|
1416
|
+
chip.textContent = texts.pathReadFailed;
|
|
1417
|
+
window.setTimeout(() => {
|
|
1418
|
+
if (!chip.isConnected) return;
|
|
1419
|
+
delete chip.dataset.error;
|
|
1420
|
+
chip.title = texts.viewPathTitle(path);
|
|
1421
|
+
chip.textContent = `${basenameOf(path)} ${texts.viewPath}`;
|
|
1422
|
+
}, 3e3);
|
|
1423
|
+
}
|
|
1424
|
+
function createPathChip(path, texts, inline) {
|
|
1425
|
+
const chip = document.createElement("button");
|
|
1426
|
+
chip.type = "button";
|
|
1427
|
+
chip.className = "dsh-cr-pathchip";
|
|
1428
|
+
chip.dataset.dshCrPathchip = "";
|
|
1429
|
+
chip.dataset.path = path;
|
|
1430
|
+
if (inline) chip.dataset.inline = "true";
|
|
1431
|
+
chip.textContent = `${basenameOf(path)} ${texts.viewPath}`;
|
|
1432
|
+
chip.title = texts.viewPathTitle(path);
|
|
1433
|
+
chip.addEventListener("click", (event) => {
|
|
1434
|
+
event.stopPropagation();
|
|
1435
|
+
event.preventDefault();
|
|
1436
|
+
openPathReviewTab(path, chip);
|
|
1437
|
+
});
|
|
1438
|
+
return chip;
|
|
1439
|
+
}
|
|
1440
|
+
function rowPathSig(m) {
|
|
1441
|
+
return JSON.stringify([...m.inline.map((i) => i.path), ...[...m.plain].sort()]);
|
|
1442
|
+
}
|
|
1443
|
+
function collectRowPathMatches(row) {
|
|
1444
|
+
const inline = [];
|
|
1445
|
+
const plain = [];
|
|
1446
|
+
const inlineSeen = /* @__PURE__ */ new Set();
|
|
1447
|
+
const walker = document.createTreeWalker(row, NodeFilter.SHOW_TEXT, {
|
|
1448
|
+
acceptNode(node2) {
|
|
1449
|
+
const parent = node2.parentElement;
|
|
1450
|
+
if (parent === null) return NodeFilter.FILTER_REJECT;
|
|
1451
|
+
if (parent.closest('[data-variant="think"], .md-code-block, [data-dsh-cr-pathchip], [data-dsh-cr-pathrow]') !== null) {
|
|
1452
|
+
return NodeFilter.FILTER_REJECT;
|
|
1453
|
+
}
|
|
1454
|
+
return NodeFilter.FILTER_ACCEPT;
|
|
1455
|
+
}
|
|
1456
|
+
});
|
|
1457
|
+
let node = walker.nextNode();
|
|
1458
|
+
while (node !== null) {
|
|
1459
|
+
const parent = node.parentElement;
|
|
1460
|
+
if (parent !== null && parent.tagName === "CODE" && !inlineSeen.has(parent)) {
|
|
1461
|
+
const path = cleanDiffPath(parent.textContent ?? "");
|
|
1462
|
+
if (path !== null) {
|
|
1463
|
+
inline.push({ el: parent, path });
|
|
1464
|
+
inlineSeen.add(parent);
|
|
1465
|
+
node = walker.nextNode();
|
|
1466
|
+
continue;
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
for (const path of findDiffPaths(node.nodeValue ?? "")) plain.push(path);
|
|
1470
|
+
node = walker.nextNode();
|
|
1471
|
+
}
|
|
1472
|
+
const inlinePaths = new Set(inline.map((i) => i.path));
|
|
1473
|
+
return { inline, plain: [...new Set(plain)].filter((p) => !inlinePaths.has(p)) };
|
|
1474
|
+
}
|
|
1475
|
+
function removeRowPathChips(row) {
|
|
1476
|
+
for (const chip of row.querySelectorAll("[data-dsh-cr-pathchip]")) chip.remove();
|
|
1477
|
+
for (const container of row.querySelectorAll("[data-dsh-cr-pathrow]")) container.remove();
|
|
1478
|
+
}
|
|
1479
|
+
function applyRowPathChips(row, matches, texts) {
|
|
1480
|
+
removeRowPathChips(row);
|
|
1481
|
+
for (const { el, path } of matches.inline) {
|
|
1482
|
+
if (!el.isConnected) continue;
|
|
1483
|
+
el.after(createPathChip(path, texts, true));
|
|
1484
|
+
}
|
|
1485
|
+
if (matches.plain.length > 0) {
|
|
1486
|
+
const container = document.createElement("div");
|
|
1487
|
+
container.className = "dsh-cr-row dsh-cr-pathrow";
|
|
1488
|
+
container.dataset.dshCrPathrow = "";
|
|
1489
|
+
for (const path of matches.plain) container.appendChild(createPathChip(path, texts, false));
|
|
1490
|
+
row.appendChild(container);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
function attachPathChips(host, paths, texts) {
|
|
1494
|
+
for (const chip of host.querySelectorAll("[data-dsh-cr-pathchip]")) {
|
|
1495
|
+
if (!paths.includes(chip.dataset.path ?? "")) chip.remove();
|
|
1496
|
+
}
|
|
1497
|
+
if (paths.length === 0) return;
|
|
1498
|
+
let container = host.querySelector(".dsh-cr-row");
|
|
1499
|
+
if (container === null) {
|
|
1500
|
+
container = document.createElement("div");
|
|
1501
|
+
container.className = "dsh-cr-row";
|
|
1502
|
+
container.dataset.dshCrRow = "";
|
|
1503
|
+
host.appendChild(container);
|
|
1504
|
+
}
|
|
1505
|
+
const existing = /* @__PURE__ */ new Set();
|
|
1506
|
+
for (const chip of container.querySelectorAll("[data-dsh-cr-pathchip]")) {
|
|
1507
|
+
if (chip.dataset.path !== void 0) existing.add(chip.dataset.path);
|
|
1508
|
+
}
|
|
1509
|
+
for (const path of paths) {
|
|
1510
|
+
if (existing.has(path)) continue;
|
|
1511
|
+
container.appendChild(createPathChip(path, texts, false));
|
|
1512
|
+
}
|
|
1246
1513
|
}
|
|
1247
1514
|
function findCodeBlockBody(el) {
|
|
1248
1515
|
const first = el.firstElementChild;
|
|
@@ -1263,7 +1530,14 @@ function probeCodeBlock(el) {
|
|
|
1263
1530
|
const banner = el.firstElementChild;
|
|
1264
1531
|
const body = findCodeBlockBody(el);
|
|
1265
1532
|
if (!(banner instanceof HTMLElement) || body === null) return null;
|
|
1266
|
-
|
|
1533
|
+
let lang = "";
|
|
1534
|
+
const innermost = banner.firstElementChild?.firstElementChild;
|
|
1535
|
+
if (innermost instanceof HTMLElement) {
|
|
1536
|
+
lang = innermost.textContent ?? "";
|
|
1537
|
+
} else {
|
|
1538
|
+
lang = banner.firstElementChild?.textContent ?? "";
|
|
1539
|
+
}
|
|
1540
|
+
lang = lang.trim().toLowerCase();
|
|
1267
1541
|
const text = body.textContent ?? "";
|
|
1268
1542
|
const candidate = lang === "diff" || lang === "patch" || isDiffText(text);
|
|
1269
1543
|
return { text, candidate, running: false };
|
|
@@ -1309,6 +1583,7 @@ function buttonHostFor(el) {
|
|
|
1309
1583
|
return first instanceof HTMLElement ? first : null;
|
|
1310
1584
|
}
|
|
1311
1585
|
var pendingTexts = /* @__PURE__ */ new Map();
|
|
1586
|
+
var pendingPaths = /* @__PURE__ */ new Map();
|
|
1312
1587
|
function readToolRoot(data) {
|
|
1313
1588
|
if (typeof data !== "object" || data === null) return null;
|
|
1314
1589
|
return data.root ?? null;
|
|
@@ -1334,6 +1609,7 @@ function toolResultText(root) {
|
|
|
1334
1609
|
}
|
|
1335
1610
|
function syncDiffToolTexts(sessionId, chat) {
|
|
1336
1611
|
const found = /* @__PURE__ */ new Map();
|
|
1612
|
+
const foundPaths = /* @__PURE__ */ new Map();
|
|
1337
1613
|
for (const key of chat.order) {
|
|
1338
1614
|
const node = chat.nodes.get(key);
|
|
1339
1615
|
if (node === void 0 || node.kind !== "tool-call") continue;
|
|
@@ -1341,13 +1617,18 @@ function syncDiffToolTexts(sessionId, chat) {
|
|
|
1341
1617
|
if (root === null || !isSettledToolResult(root)) continue;
|
|
1342
1618
|
if (root.call === null || root.call.name !== "bash") continue;
|
|
1343
1619
|
const text = toolResultText(root);
|
|
1344
|
-
if (
|
|
1345
|
-
|
|
1620
|
+
if (isDiffText(text)) found.set(key, text);
|
|
1621
|
+
const paths = findDiffPaths(text);
|
|
1622
|
+
if (paths.length > 0) foundPaths.set(key, paths);
|
|
1346
1623
|
}
|
|
1347
1624
|
for (const [key, entry] of pendingTexts) {
|
|
1348
1625
|
if (entry.sessionId === sessionId && !found.has(key)) pendingTexts.delete(key);
|
|
1349
1626
|
}
|
|
1627
|
+
for (const [key, entry] of pendingPaths) {
|
|
1628
|
+
if (entry.sessionId === sessionId && !foundPaths.has(key)) pendingPaths.delete(key);
|
|
1629
|
+
}
|
|
1350
1630
|
for (const [key, text] of found) pendingTexts.set(key, { sessionId, text });
|
|
1631
|
+
for (const [key, paths] of foundPaths) pendingPaths.set(key, { sessionId, paths });
|
|
1351
1632
|
}
|
|
1352
1633
|
function findRowByKey(key) {
|
|
1353
1634
|
for (const el of document.querySelectorAll("[data-chat-anchor-key]")) {
|
|
@@ -1363,10 +1644,19 @@ function attachToolRowButtons() {
|
|
|
1363
1644
|
if (!(sample instanceof HTMLElement)) continue;
|
|
1364
1645
|
ensureButtons(sample, () => pendingTexts.get(key)?.text ?? entry.text, null);
|
|
1365
1646
|
}
|
|
1647
|
+
for (const [key, entry] of pendingPaths) {
|
|
1648
|
+
const row = findRowByKey(key);
|
|
1649
|
+
if (row === null) continue;
|
|
1650
|
+
const sample = row.querySelector('[data-sample="bash"]');
|
|
1651
|
+
if (!(sample instanceof HTMLElement)) continue;
|
|
1652
|
+
attachPathChips(sample, entry.paths, CR_TEXTS[detectLang()]);
|
|
1653
|
+
}
|
|
1366
1654
|
}
|
|
1367
1655
|
var DiffScanner = class {
|
|
1368
1656
|
constructor() {
|
|
1369
1657
|
__publicField(this, "states", /* @__PURE__ */ new WeakMap());
|
|
1658
|
+
/** 消息行路径 chip 的行状态(与 BlockState 并列,策略同款:去抖 + 签名)。 */
|
|
1659
|
+
__publicField(this, "pathStates", /* @__PURE__ */ new WeakMap());
|
|
1370
1660
|
__publicField(this, "observer", null);
|
|
1371
1661
|
__publicField(this, "scanTimer", null);
|
|
1372
1662
|
__publicField(this, "safetyTimer", null);
|
|
@@ -1398,12 +1688,15 @@ var DiffScanner = class {
|
|
|
1398
1688
|
this.scan();
|
|
1399
1689
|
}, SCAN_DEBOUNCE);
|
|
1400
1690
|
}
|
|
1401
|
-
/** 全量扫描:数据层工具行按钮 + DOM
|
|
1691
|
+
/** 全量扫描:数据层工具行按钮 + DOM 候选块 + 消息行路径 chip。 */
|
|
1402
1692
|
scan() {
|
|
1403
1693
|
attachToolRowButtons();
|
|
1404
1694
|
for (const el of document.querySelectorAll(".md-code-block, [data-terminal]")) {
|
|
1405
1695
|
this.inspect(el);
|
|
1406
1696
|
}
|
|
1697
|
+
for (const row of document.querySelectorAll("[data-chat-anchor-key]")) {
|
|
1698
|
+
this.inspectPathRow(row);
|
|
1699
|
+
}
|
|
1407
1700
|
}
|
|
1408
1701
|
/** 检查一个候选块:内容未变则不动,变了则排去抖,不再形似 diff 则移除按钮。 */
|
|
1409
1702
|
inspect(el) {
|
|
@@ -1481,6 +1774,65 @@ var DiffScanner = class {
|
|
|
1481
1774
|
btn.textContent = st.folded ? texts.expand : texts.collapse;
|
|
1482
1775
|
}
|
|
1483
1776
|
}
|
|
1777
|
+
/**
|
|
1778
|
+
* 检查一个消息行的本地 diff 路径:
|
|
1779
|
+
* - 工具行(含 data-sample)走数据层,这里跳过;
|
|
1780
|
+
* - 没有路径/不再有路径 → 移除 chip 并清态;
|
|
1781
|
+
* - 签名变更 → 移除旧 chip、排稳定去抖(流式期间 chip 不闪现);
|
|
1782
|
+
* - 签名未变 → 校验已挂 chip 与当前 DOM 是否一致(React 可能重建过
|
|
1783
|
+
* code 元素),不一致直接重建(内容稳定,不需要再去抖)。
|
|
1784
|
+
*/
|
|
1785
|
+
inspectPathRow(row) {
|
|
1786
|
+
if (row.querySelector("[data-sample]") !== null) return;
|
|
1787
|
+
if (!/\.(?:diff|patch)/i.test(row.textContent ?? "")) {
|
|
1788
|
+
removeRowPathChips(row);
|
|
1789
|
+
this.pathStates.delete(row);
|
|
1790
|
+
return;
|
|
1791
|
+
}
|
|
1792
|
+
const matches = collectRowPathMatches(row);
|
|
1793
|
+
const sig = rowPathSig(matches);
|
|
1794
|
+
if (matches.inline.length === 0 && matches.plain.length === 0) {
|
|
1795
|
+
removeRowPathChips(row);
|
|
1796
|
+
this.pathStates.delete(row);
|
|
1797
|
+
return;
|
|
1798
|
+
}
|
|
1799
|
+
const st = this.pathStates.get(row);
|
|
1800
|
+
if (st !== void 0 && st.sig === sig) {
|
|
1801
|
+
if (this.pathChipsConsistent(row, matches)) return;
|
|
1802
|
+
if (st.timer === null) applyRowPathChips(row, matches, CR_TEXTS[detectLang()]);
|
|
1803
|
+
return;
|
|
1804
|
+
}
|
|
1805
|
+
removeRowPathChips(row);
|
|
1806
|
+
if (st !== void 0 && st.timer !== null) clearTimeout(st.timer);
|
|
1807
|
+
const timer = window.setTimeout(() => {
|
|
1808
|
+
const cur = this.pathStates.get(row);
|
|
1809
|
+
if (cur === void 0 || cur.timer !== timer) return;
|
|
1810
|
+
cur.timer = null;
|
|
1811
|
+
const m2 = collectRowPathMatches(row);
|
|
1812
|
+
if (rowPathSig(m2) !== cur.sig) return;
|
|
1813
|
+
applyRowPathChips(row, m2, CR_TEXTS[detectLang()]);
|
|
1814
|
+
}, STABLE_MS);
|
|
1815
|
+
this.pathStates.set(row, { sig, timer });
|
|
1816
|
+
}
|
|
1817
|
+
/** 校验消息行上已挂的 chip 与当前匹配是否一致(数量 + 路径 + 锚点)。 */
|
|
1818
|
+
pathChipsConsistent(row, matches) {
|
|
1819
|
+
const chips = row.querySelectorAll("[data-dsh-cr-pathchip]");
|
|
1820
|
+
if (chips.length !== matches.inline.length + matches.plain.length) return false;
|
|
1821
|
+
const inlineOk = new Set(matches.inline.map((m) => m.path));
|
|
1822
|
+
const plainOk = new Set(matches.plain);
|
|
1823
|
+
for (const chip of chips) {
|
|
1824
|
+
const path = chip.dataset.path ?? "";
|
|
1825
|
+
if (chip.dataset.inline === "true") {
|
|
1826
|
+
if (!inlineOk.has(path)) return false;
|
|
1827
|
+
const sib = chip.previousElementSibling;
|
|
1828
|
+
if (!(sib instanceof HTMLElement) || sib.tagName !== "CODE") return false;
|
|
1829
|
+
if ((sib.textContent ?? "") !== path) return false;
|
|
1830
|
+
} else if (!plainOk.has(path)) {
|
|
1831
|
+
return false;
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
return true;
|
|
1835
|
+
}
|
|
1484
1836
|
};
|
|
1485
1837
|
var inject = ["slots", "locale"];
|
|
1486
1838
|
function apply(ctx) {
|
|
@@ -1520,13 +1872,13 @@ function CodeReviewEnhancer() {
|
|
|
1520
1872
|
}, []);
|
|
1521
1873
|
return null;
|
|
1522
1874
|
}
|
|
1523
|
-
function DiffToolButtonWatcher({ useSession, sessionId, inputActions }) {
|
|
1524
|
-
const chat = useSession((state) => state.chat);
|
|
1875
|
+
function DiffToolButtonWatcher({ useSession, useChat, sessionId, inputActions }) {
|
|
1876
|
+
const chat = useChat !== void 0 ? useChat((state) => state) : useSession((state) => state.chat);
|
|
1525
1877
|
(0, import_react.useEffect)(() => {
|
|
1526
1878
|
if (chat !== void 0) syncDiffToolTexts(String(sessionId), chat);
|
|
1527
1879
|
}, [chat, sessionId]);
|
|
1528
1880
|
(0, import_react.useEffect)(() => {
|
|
1529
|
-
const writer = (text) => inputActions
|
|
1881
|
+
const writer = (text) => inputActions?.setDraft(text);
|
|
1530
1882
|
composerWriter = writer;
|
|
1531
1883
|
return () => {
|
|
1532
1884
|
if (composerWriter === writer) composerWriter = null;
|