@xia-sc/dsh-git 0.5.2 → 0.5.3
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 +26 -2
- package/README.md +19 -3
- package/lib/client.js +222 -29
- package/lib/index.js +27 -7
- package/package.json +3 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 xia-sc
|
|
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
CHANGED
|
@@ -21,7 +21,13 @@ new-branch-from-base.
|
|
|
21
21
|
the affected files instead of switching, with a "switch anyway" escape
|
|
22
22
|
hatch, fetch/pull actions, a **commit area** — stage-all button, draft-basis
|
|
23
23
|
picker, "✨ AI draft" button, message input and commit button — collapsible
|
|
24
|
-
changes and recent-commit lists
|
|
24
|
+
changes and recent-commit lists). **Operation feedback is pinned to the very
|
|
25
|
+
top of the panel and stays one line tall** — the busy label and the last
|
|
26
|
+
operation's result are the first rows of the body. The result is a localized
|
|
27
|
+
phrase ("Pushed", "Staged everything", "Switched to x"); git's own output (a
|
|
28
|
+
push sideband banner, the `LF will be replaced by CRLF` advice) sits behind a
|
|
29
|
+
"Details ▾" toggle on the right, so the change list and the log can never push
|
|
30
|
+
the notification out of sight. The panel is
|
|
25
31
|
**draggable by its header bar**
|
|
26
32
|
(the top row with the Git title — press, drag, release; it stays where
|
|
27
33
|
dropped and is clamped inside the viewport; header buttons/inputs never
|
|
@@ -40,6 +46,11 @@ new-branch-from-base.
|
|
|
40
46
|
- Both seats share one store, so they always agree, and both re-bind when the
|
|
41
47
|
current session (and its cwd) changes. Rebinding clears the selected file —
|
|
42
48
|
the panel never shows another repository's content.
|
|
49
|
+
- **ANSI colour codes are stripped from git's text output**: remotes colour
|
|
50
|
+
their own banner (gitee's `Powered by GITEE.COM` does), and the panel is a DOM,
|
|
51
|
+
not a terminal — the ESC byte has no glyph, so only the parameters survived
|
|
52
|
+
and read as `[0[01;33m`. Only `message` text is stripped; **diff bodies and
|
|
53
|
+
paths stay byte-faithful**.
|
|
43
54
|
|
|
44
55
|
### The diff viewer
|
|
45
56
|
|
|
@@ -202,11 +213,16 @@ shell metacharacters, and leading dashes are all recorded verbatim.
|
|
|
202
213
|
| `commit` | `{ cwd, message }` | `{ message }`; `missing-author` error when `user.name/email` unset |
|
|
203
214
|
| `push` | `{ cwd }` | `{ message }` (120s timeout) |
|
|
204
215
|
| `log` | `{ cwd, count? }` | `{ repo, commits: [{sha, author, subject, refs}] }` (clamped 1..50) |
|
|
205
|
-
| `generateMessage` | `{ cwd, mode?, provider?, model? }` | `{ message, mode, provider, model }`. `mode` is `staged` (default) / `unstaged` / `all`; anything else is `invalid-mode`. Failure code in `error.details.code`: `no-changes`, `no-provider`, `no-model`, `llm-empty`, `cancelled`, `llm-failed`. |
|
|
216
|
+
| `generateMessage` | `{ cwd, mode?, provider?, model? }` | `{ message, mode, provider, model }`. `mode` is `staged` (default) / `unstaged` / `all`; anything else is `invalid-mode`. Failure code in `error.details.code`: `no-changes`, `no-provider`, `no-model`, `llm-truncated` (the output cap ran out before any text was written), `llm-empty`, `cancelled`, `llm-failed`. |
|
|
206
217
|
|
|
207
218
|
> A failed result carries `error.code === "internal"` on the wire (the Connection
|
|
208
219
|
> envelope only requires a string), with the plugin's own diagnostic in
|
|
209
220
|
> `error.details.code`; the client localizes from that code.
|
|
221
|
+
>
|
|
222
|
+
> The `message` of `fetch`/`pull`/`push`/`stage`/`commit` is **git's own output**
|
|
223
|
+
> (ANSI colour codes stripped): an empty string when git said nothing. The panel
|
|
224
|
+
> notifies with an `output.<action>` phrase and keeps that text behind its
|
|
225
|
+
> expandable "Details".
|
|
210
226
|
|
|
211
227
|
## Design decisions & boundaries
|
|
212
228
|
|
|
@@ -219,6 +235,10 @@ shell metacharacters, and leading dashes are all recorded verbatim.
|
|
|
219
235
|
configured** — possibly a third-party gateway. It only happens when you click
|
|
220
236
|
"✨ AI draft"; the plugin itself never calls the network. The diff is
|
|
221
237
|
truncated to 12000 characters, and nothing outside the repository is sent.
|
|
238
|
+
The output cap is 8192 tokens: a reasoning model shares that completion budget
|
|
239
|
+
between its thinking and the message, so a cap sized for the message alone
|
|
240
|
+
produces "hit the cap before writing anything" — reported as `llm-truncated`
|
|
241
|
+
rather than a vague `llm-empty`.
|
|
222
242
|
- **push/pull credentials** come from the system (Git Credential Manager /
|
|
223
243
|
SSH agent); the plugin never touches credential storage. AI drafting never
|
|
224
244
|
touches credentials either — the model adapter resolves its own API key.
|
|
@@ -282,3 +302,7 @@ shell metacharacters, and leading dashes are all recorded verbatim.
|
|
|
282
302
|
- The last two need piped child-process stdio, so they are deliberately **not**
|
|
283
303
|
part of `npm test` — run them from an ordinary terminal.
|
|
284
304
|
- The git command set is verified end-to-end against the running server.
|
|
305
|
+
|
|
306
|
+
## License
|
|
307
|
+
|
|
308
|
+
MIT — see [LICENSE](LICENSE).
|
package/README.md
CHANGED
|
@@ -18,7 +18,10 @@ DeepSeek Harness Web GUI 的完整 Git 管理插件,形态为一个**可折叠
|
|
|
18
18
|
"仍要切换"按钮;fetch/pull 操作;**提交区**——"暂存全部"按钮、AI 生成依据
|
|
19
19
|
选择器、"✨ AI 生成"按钮、提交信息输入框(多行文本框,回车换行、
|
|
20
20
|
Ctrl/Cmd+Enter 提交)与提交按钮;可折叠的变更列表与
|
|
21
|
-
|
|
21
|
+
最近提交列表)。**操作反馈固定钉在面板最上边,而且只有一行**——"推送中…"和上次操作的结果都在
|
|
22
|
+
body 的第一行;结果是一个中文短句("已推送"/"已暂存全部"/"已切换到 xxx"),git 自己的输出
|
|
23
|
+
(push 的 sideband banner、`LF will be replaced by CRLF` 这类提示)收在右侧的"详情 ▾"里,点开才展开,
|
|
24
|
+
不会被变更列表和最近提交顶到看不见的地方。面板**可通过顶栏拖动**(按住带 Git 标题的
|
|
22
25
|
那一行,拖到哪里就停在哪里,不会拖出视口;顶栏上的按钮/输入框不会触发
|
|
23
26
|
拖动;双击顶栏回到居中位置)。
|
|
24
27
|
分支切换器旁边的"+ 新建分支"按钮会展开一个内联表单:新分支名 + 基分支
|
|
@@ -31,6 +34,10 @@ DeepSeek Harness Web GUI 的完整 Git 管理插件,形态为一个**可折叠
|
|
|
31
34
|
状态胶囊(分支摘要,或"当前工作区不是 Git 仓库");点击它展开/收起悬浮面板。
|
|
32
35
|
- 两处界面共享同一个 store,状态永远一致,并都会随当前会话(及其 cwd)
|
|
33
36
|
切换而重新绑定。切换工作区会清空已选中的文件——绝不让面板显示别的仓库的内容。
|
|
37
|
+
- **git 的文字输出会剥掉 ANSI 颜色码**:远端会给自己的 banner 上色(gitee 的
|
|
38
|
+
`Powered by GITEE.COM` 就是),面板是 DOM 不是终端,ESC 没有字形可渲染,
|
|
39
|
+
不剥就只剩 `[0[01;33m` 这类参数当正文显示。只剥"消息"文本(`message`),
|
|
40
|
+
**diff 内容与文件名一律逐字节保真**。
|
|
34
41
|
|
|
35
42
|
### 差异查看
|
|
36
43
|
|
|
@@ -163,10 +170,13 @@ dsh plugin --profile web remove @xia-sc/dsh-git
|
|
|
163
170
|
| `commit` | `{ cwd, message }` | `{ message }`;未配置 `user.name/email` 时报 `missing-author` 错误 |
|
|
164
171
|
| `push` | `{ cwd }` | `{ message }`(120s 超时) |
|
|
165
172
|
| `log` | `{ cwd, count? }` | `{ repo, commits: [{sha, author, subject, refs}] }`(钳制 1..50) |
|
|
166
|
-
| `generateMessage` | `{ cwd, mode?, provider?, model? }` | `{ message, mode, provider, model }`。`mode` 为 `staged`(默认)/`unstaged`/`all`,非法值报 `invalid-mode`;失败码见 `error.details.code`:`no-changes`、`no-provider`、`no-model`、`llm-empty`、`cancelled`、`llm-failed`。 |
|
|
173
|
+
| `generateMessage` | `{ cwd, mode?, provider?, model? }` | `{ message, mode, provider, model }`。`mode` 为 `staged`(默认)/`unstaged`/`all`,非法值报 `invalid-mode`;失败码见 `error.details.code`:`no-changes`、`no-provider`、`no-model`、`llm-truncated`(输出上限用尽、一个字都没写出来)、`llm-empty`、`cancelled`、`llm-failed`。 |
|
|
167
174
|
|
|
168
175
|
> 失败结果的 `error.code` 在线路上固定为 `"internal"`(Connection 信封只要求它是字符串),
|
|
169
176
|
> 插件自己的诊断码放在 `error.details.code`;客户端按该码做本地化文案。
|
|
177
|
+
>
|
|
178
|
+
> `fetch`/`pull`/`push`/`stage`/`commit` 的 `message` 就是 **git 自己的输出**(已剥掉 ANSI 颜色码):
|
|
179
|
+
> git 什么都没说时是空串,界面用 `output.<action>` 的中文短句做通知、把这段原文放进可展开的"详情"。
|
|
170
180
|
|
|
171
181
|
## 设计决策与边界
|
|
172
182
|
|
|
@@ -176,7 +186,9 @@ dsh plugin --profile web remove @xia-sc/dsh-git
|
|
|
176
186
|
"暂存全部"按钮(`git add --all`),而不是让提交隐式暂存。
|
|
177
187
|
- **AI 生成会把改动的 diff 发给你配置的模型提供方**——可能是第三方网关。
|
|
178
188
|
这是显式点击"✨ AI 生成"才会发生的联网行为;插件本身不联网。diff 截断到
|
|
179
|
-
12000
|
|
189
|
+
12000 字符后发送,且不发送任何仓库外的内容。输出上限 8192 token:思考型
|
|
190
|
+
模型的 reasoning 与正文共用同一份 completion 预算,上限太小会"一个字都没
|
|
191
|
+
写就超限",此时报 `llm-truncated` 而不是含糊的 `llm-empty`。
|
|
180
192
|
- **push/pull 凭据**来自系统(Git Credential Manager / SSH agent);插件
|
|
181
193
|
绝不碰凭据存储。AI 生成同样不接触凭据——API key 由模型适配器自己解析。
|
|
182
194
|
- **插件绝不修改 git config**;缺 author 时给出明确错误而不是悄悄补写。
|
|
@@ -223,3 +235,7 @@ dsh plugin --profile web remove @xia-sc/dsh-git
|
|
|
223
235
|
- 后两个都必须 spawn git 的管道 stdio,故**不在 `npm test` 内**——请在没有
|
|
224
236
|
该限制的环境(普通终端)单独运行。
|
|
225
237
|
git 命令集对照运行中的服务端做端到端验证。
|
|
238
|
+
|
|
239
|
+
## 许可
|
|
240
|
+
|
|
241
|
+
MIT,见 [LICENSE](LICENSE)。
|
package/lib/client.js
CHANGED
|
@@ -72,6 +72,7 @@ window.__ModuleLoader__.load({
|
|
|
72
72
|
"generate.noProvider": "尚未配置模型,请先在 设置 > 模型 中添加",
|
|
73
73
|
"generate.noModel": "该提供方没有可用模型",
|
|
74
74
|
"generate.empty": "模型没有返回提交信息",
|
|
75
|
+
"generate.truncated": "模型还没写出提交信息就用完了 token 上限(思考模型会先花掉这份预算),再点一次通常即可",
|
|
75
76
|
"generate.cancelled": "生成已取消",
|
|
76
77
|
"generate.failed": "生成失败",
|
|
77
78
|
"commit.placeholder": "提交信息(可多行,Ctrl+Enter 提交)…",
|
|
@@ -109,6 +110,17 @@ window.__ModuleLoader__.load({
|
|
|
109
110
|
"busy.checkout": "切换中…",
|
|
110
111
|
"busy.createBranch": "创建分支中…",
|
|
111
112
|
"output.ok": "操作成功",
|
|
113
|
+
"output.failed": "操作失败",
|
|
114
|
+
"output.detail": "详情",
|
|
115
|
+
"output.detailHide": "收起",
|
|
116
|
+
"output.fetch": "已拉取更新",
|
|
117
|
+
"output.pull": "已拉取合并",
|
|
118
|
+
"output.push": "已推送",
|
|
119
|
+
"output.stage": "已暂存全部",
|
|
120
|
+
"output.commit": "已提交",
|
|
121
|
+
"output.checkout": "已切换到 {branch}",
|
|
122
|
+
"output.createBranch": "已创建并切换到 {branch}",
|
|
123
|
+
"output.generate": "已生成提交信息",
|
|
112
124
|
"dock.aria": "打开 Git 面板",
|
|
113
125
|
"status.modified": "修改",
|
|
114
126
|
"status.added": "新增",
|
|
@@ -159,6 +171,7 @@ window.__ModuleLoader__.load({
|
|
|
159
171
|
"generate.noProvider": "No model is configured — add one in Settings > Models",
|
|
160
172
|
"generate.noModel": "That provider advertises no model",
|
|
161
173
|
"generate.empty": "The model returned no commit message",
|
|
174
|
+
"generate.truncated": "The model hit its token cap before writing anything (a reasoning model spends that budget thinking first) — trying again usually works",
|
|
162
175
|
"generate.cancelled": "Draft cancelled",
|
|
163
176
|
"generate.failed": "Draft failed",
|
|
164
177
|
"commit.placeholder": "Commit message (multi-line allowed, Ctrl+Enter commits)…",
|
|
@@ -196,6 +209,17 @@ window.__ModuleLoader__.load({
|
|
|
196
209
|
"busy.checkout": "Switching…",
|
|
197
210
|
"busy.createBranch": "Creating branch…",
|
|
198
211
|
"output.ok": "Operation succeeded",
|
|
212
|
+
"output.failed": "Operation failed",
|
|
213
|
+
"output.detail": "Details",
|
|
214
|
+
"output.detailHide": "Hide",
|
|
215
|
+
"output.fetch": "Fetched",
|
|
216
|
+
"output.pull": "Pulled",
|
|
217
|
+
"output.push": "Pushed",
|
|
218
|
+
"output.stage": "Staged everything",
|
|
219
|
+
"output.commit": "Committed",
|
|
220
|
+
"output.checkout": "Switched to {branch}",
|
|
221
|
+
"output.createBranch": "Created and switched to {branch}",
|
|
222
|
+
"output.generate": "Draft ready",
|
|
199
223
|
"dock.aria": "Open Git panel",
|
|
200
224
|
"status.modified": "modified",
|
|
201
225
|
"status.added": "added",
|
|
@@ -606,11 +630,47 @@ window.__ModuleLoader__.load({
|
|
|
606
630
|
fontFamily: "monospace"
|
|
607
631
|
},
|
|
608
632
|
output: {
|
|
609
|
-
|
|
633
|
+
// Sits at the top of the body, so the breathing room belongs below it.
|
|
634
|
+
// The card itself is one localized summary line tall; git's own words
|
|
635
|
+
// live in `outputDetail`, behind the toggle.
|
|
636
|
+
marginBottom: 8,
|
|
610
637
|
padding: "6px 10px",
|
|
611
638
|
borderRadius: 8,
|
|
612
639
|
fontSize: 12,
|
|
640
|
+
lineHeight: "18px"
|
|
641
|
+
},
|
|
642
|
+
outputLine: {
|
|
643
|
+
display: "flex",
|
|
644
|
+
alignItems: "center",
|
|
645
|
+
gap: 8
|
|
646
|
+
},
|
|
647
|
+
outputSummary: {
|
|
648
|
+
flex: "1",
|
|
649
|
+
minWidth: 0,
|
|
650
|
+
overflow: "hidden",
|
|
651
|
+
textOverflow: "ellipsis",
|
|
652
|
+
whiteSpace: "nowrap"
|
|
653
|
+
},
|
|
654
|
+
outputToggle: {
|
|
655
|
+
flex: "none",
|
|
656
|
+
padding: "0 6px",
|
|
657
|
+
height: 20,
|
|
658
|
+
border: "1px solid currentColor",
|
|
659
|
+
borderRadius: 6,
|
|
660
|
+
background: "transparent",
|
|
661
|
+
color: "inherit",
|
|
662
|
+
font: "inherit",
|
|
663
|
+
fontSize: 11,
|
|
613
664
|
lineHeight: "18px",
|
|
665
|
+
cursor: "pointer"
|
|
666
|
+
},
|
|
667
|
+
outputDetail: {
|
|
668
|
+
marginTop: 6,
|
|
669
|
+
paddingTop: 6,
|
|
670
|
+
borderTop: "1px solid var(--dsw-alias-border-secondary, rgba(127,127,127,0.35))",
|
|
671
|
+
fontFamily: "monospace",
|
|
672
|
+
fontSize: 11,
|
|
673
|
+
lineHeight: "16px",
|
|
614
674
|
wordBreak: "break-word",
|
|
615
675
|
maxHeight: 160,
|
|
616
676
|
overflowY: "auto",
|
|
@@ -645,9 +705,128 @@ window.__ModuleLoader__.load({
|
|
|
645
705
|
return text.length > max ? text.slice(0, max - 1) + "…" : text;
|
|
646
706
|
}
|
|
647
707
|
|
|
708
|
+
/**
|
|
709
|
+
* Strip ANSI/VT escape sequences from git's human-readable output.
|
|
710
|
+
*
|
|
711
|
+
* `git push` forwards the remote's own sideband banner verbatim — Gitee,
|
|
712
|
+
* for one, decorates "Powered by GITEE.COM" with SGR codes, and its first
|
|
713
|
+
* sequence is even truncated (`ESC[0`) right before a complete one. A DOM
|
|
714
|
+
* is not a terminal: the ESC byte has no glyph, so only the parameters
|
|
715
|
+
* survived and the banner read `Powered by [0[01;33mGITEE.COM [0[m…`.
|
|
716
|
+
*
|
|
717
|
+
* Only `message` text passes through here. Diff bodies and paths must not:
|
|
718
|
+
* a file may legitimately contain ESC bytes and the pane shows it verbatim.
|
|
719
|
+
*
|
|
720
|
+
* @param text - a git message string (or anything else).
|
|
721
|
+
* @returns the text with escape sequences removed.
|
|
722
|
+
*/
|
|
723
|
+
function stripAnsiEscapes(text) {
|
|
724
|
+
return (text === undefined || text === null ? "" : String(text))
|
|
725
|
+
// OSC (window title / hyperlink): ESC ] … BEL or ESC \
|
|
726
|
+
.replace(/\u001B\][^\u0007\u001B]*(?:\u0007|\u001B\\)/g, "")
|
|
727
|
+
// A complete CSI sequence: ESC [ params/intermediates final byte
|
|
728
|
+
.replace(/\u001B\[[0-9;?]*[ -/]*[@-~]/g, "")
|
|
729
|
+
// A truncated CSI (the remote's malformed banner): ESC [ params only
|
|
730
|
+
.replace(/\u001B\[[0-9;?]*/g, "")
|
|
731
|
+
// Any other two-character escape, then any surviving ESC.
|
|
732
|
+
.replace(/\u001B[@-Z\\-_]/g, "")
|
|
733
|
+
.replace(/\u001B/g, "");
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/** Sanitize one RPC result's human-readable text, leaving data untouched. */
|
|
737
|
+
function sanitizeResult(result) {
|
|
738
|
+
if (result === null || typeof result !== "object") return result;
|
|
739
|
+
var message = function (holder) {
|
|
740
|
+
return holder !== null && typeof holder === "object" && typeof holder.message === "string"
|
|
741
|
+
? Object.assign({}, holder, { message: stripAnsiEscapes(holder.message) })
|
|
742
|
+
: holder;
|
|
743
|
+
};
|
|
744
|
+
var value = message(result.value);
|
|
745
|
+
var error = message(result.error);
|
|
746
|
+
if (value === result.value && error === result.error) return result;
|
|
747
|
+
return Object.assign({}, result, { value: value, error: error });
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/** Longest one-line summary an operation card shows before folding. */
|
|
751
|
+
var OUTPUT_SUMMARY_MAX = 160;
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Word one operation result the default way: a summary the panel can
|
|
755
|
+
* localize from `action` (the store holds no translator), plus git's own
|
|
756
|
+
* output as collapsible detail.
|
|
757
|
+
*
|
|
758
|
+
* git's output is often several lines of advice — the LF/CRLF warning on
|
|
759
|
+
* every staged file, a remote's push banner — and showing it as the
|
|
760
|
+
* notification buries the answer to "did it work?". So success keeps git's
|
|
761
|
+
* words behind the toggle, and a failure shows the head of the diagnostic
|
|
762
|
+
* with the whole thing one click away.
|
|
763
|
+
*
|
|
764
|
+
* @param action - the operation name (`push`, `stage`, …).
|
|
765
|
+
* @param res - the raw RPC result.
|
|
766
|
+
* @param params - interpolation for the localized summary (e.g. `{ branch }`).
|
|
767
|
+
* @returns `{ kind, action, params, text, detail }`.
|
|
768
|
+
*/
|
|
769
|
+
function summarizeResult(action, res, params) {
|
|
770
|
+
if (res && res.ok === true) {
|
|
771
|
+
var raw = res.value && typeof res.value.message === "string" ? res.value.message.trim() : "";
|
|
772
|
+
return { kind: "ok", action: action, params: params || null, text: null, detail: raw };
|
|
773
|
+
}
|
|
774
|
+
var message = res && res.error && res.error.message ? String(res.error.message).trim() : "";
|
|
775
|
+
return {
|
|
776
|
+
kind: "error",
|
|
777
|
+
action: action,
|
|
778
|
+
params: params || null,
|
|
779
|
+
text: message === "" ? null : shortText(message, OUTPUT_SUMMARY_MAX),
|
|
780
|
+
detail: message.length > OUTPUT_SUMMARY_MAX ? message : ""
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* One operation-feedback card: the localized summary line and, when git
|
|
786
|
+
* said anything, git's own output behind a toggle. Pure and exported, so
|
|
787
|
+
* the render test can drive both states (SSR cannot click).
|
|
788
|
+
*
|
|
789
|
+
* @param result - `state.lastResult` (`{ kind, action, params, text, detail }`).
|
|
790
|
+
* @param t - the locale translator.
|
|
791
|
+
* @param open - whether the detail block is expanded.
|
|
792
|
+
* @param onToggle - called when the detail toggle is clicked.
|
|
793
|
+
* @returns the card element, or null without a result.
|
|
794
|
+
*/
|
|
795
|
+
function outputView(result, t, open, onToggle) {
|
|
796
|
+
if (!result) return null;
|
|
797
|
+
var summary = result.text !== undefined && result.text !== null && String(result.text) !== ""
|
|
798
|
+
? String(result.text)
|
|
799
|
+
: result.action
|
|
800
|
+
? t("output." + result.action, result.params || undefined)
|
|
801
|
+
: t(result.kind === "error" ? "output.failed" : "output.ok");
|
|
802
|
+
var detail = typeof result.detail === "string" ? result.detail.trim() : "";
|
|
803
|
+
var line = [
|
|
804
|
+
h("span", { key: "text", style: S.outputSummary, title: summary }, summary)
|
|
805
|
+
];
|
|
806
|
+
if (detail !== "") {
|
|
807
|
+
line.push(h("button", {
|
|
808
|
+
key: "toggle",
|
|
809
|
+
type: "button",
|
|
810
|
+
"data-dsh-git": "output-toggle",
|
|
811
|
+
style: S.outputToggle,
|
|
812
|
+
"aria-expanded": open ? "true" : "false",
|
|
813
|
+
onClick: onToggle
|
|
814
|
+
}, (open ? "▴ " : "▾ ") + t(open ? "output.detailHide" : "output.detail")));
|
|
815
|
+
}
|
|
816
|
+
var children = [h("div", { key: "line", style: S.outputLine }, line)];
|
|
817
|
+
if (open && detail !== "") {
|
|
818
|
+
children.push(h("div", { key: "detail", "data-dsh-git": "output-detail", style: S.outputDetail }, detail));
|
|
819
|
+
}
|
|
820
|
+
return h("div", {
|
|
821
|
+
key: "output",
|
|
822
|
+
"data-dsh-git": "output",
|
|
823
|
+
style: Object.assign({}, S.output, result.kind === "ok" ? S.outputOk : S.outputError)
|
|
824
|
+
}, children);
|
|
825
|
+
}
|
|
826
|
+
|
|
648
827
|
/** Call one /dsh-git-rpc endpoint. */
|
|
649
828
|
function rpc(ctx, endpoint, args) {
|
|
650
|
-
return ctx.connection.rpc.call("/dsh-git-rpc", endpoint, { args: args || {} });
|
|
829
|
+
return ctx.connection.rpc.call("/dsh-git-rpc", endpoint, { args: args || {} }).then(sanitizeResult);
|
|
651
830
|
}
|
|
652
831
|
|
|
653
832
|
/**
|
|
@@ -823,17 +1002,20 @@ window.__ModuleLoader__.load({
|
|
|
823
1002
|
* Run one action; on success force a fresh load; surfaces lastResult.
|
|
824
1003
|
* `describe` lets a caller own the wording of both outcomes (used by
|
|
825
1004
|
* generation, where the success value is the message itself and a
|
|
826
|
-
* failure code maps to a localized string).
|
|
827
|
-
*
|
|
1005
|
+
* failure code maps to a localized string). Without it the result is
|
|
1006
|
+
* summarized by {@link summarizeResult}: a localized one-liner plus
|
|
1007
|
+
* git's own output as detail. `params` interpolates that summary (e.g.
|
|
1008
|
+
* `{ branch }`); the panel localizes it, since the store holds no `t`.
|
|
1009
|
+
* Resolves with the raw RPC result so the caller can also act on it.
|
|
1010
|
+
* @param name - the operation name (`push`, `commit`, …).
|
|
1011
|
+
* @param run - performs the call.
|
|
1012
|
+
* @param describe - optional wording override for both outcomes.
|
|
1013
|
+
* @param params - optional interpolation for the default summary.
|
|
828
1014
|
*/
|
|
829
|
-
function act(name, run, describe) {
|
|
1015
|
+
function act(name, run, describe, params) {
|
|
830
1016
|
emit(Object.assign({}, state, { busy: name, lastResult: null }));
|
|
831
1017
|
return run().then(function (res) {
|
|
832
|
-
var result = describe
|
|
833
|
-
? describe(res)
|
|
834
|
-
: res && res.ok === true
|
|
835
|
-
? { kind: "ok", text: res.value && res.value.message ? String(res.value.message) : null }
|
|
836
|
-
: { kind: "error", text: res && res.error && res.error.message ? String(res.error.message) : "operation failed" };
|
|
1018
|
+
var result = describe ? describe(res) : summarizeResult(name, res, params);
|
|
837
1019
|
emit(Object.assign({}, state, { busy: null, lastResult: result }));
|
|
838
1020
|
if (res && res.ok === true) {
|
|
839
1021
|
var cwd = state.cwd;
|
|
@@ -841,10 +1023,10 @@ window.__ModuleLoader__.load({
|
|
|
841
1023
|
}
|
|
842
1024
|
return res;
|
|
843
1025
|
}).catch(function (error) {
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
}));
|
|
1026
|
+
// A transport failure is worded exactly like a git failure: the
|
|
1027
|
+
// same summary/detail split, so the card never grows a second shape.
|
|
1028
|
+
var failure = { ok: false, error: { message: error && error.message ? String(error.message) : String(error) } };
|
|
1029
|
+
emit(Object.assign({}, state, { busy: null, lastResult: summarizeResult(name, failure, params) }));
|
|
848
1030
|
});
|
|
849
1031
|
}
|
|
850
1032
|
|
|
@@ -1377,6 +1559,15 @@ window.__ModuleLoader__.load({
|
|
|
1377
1559
|
var logOpenRef = React.useState(false);
|
|
1378
1560
|
var logOpen = logOpenRef[0];
|
|
1379
1561
|
var setLogOpen = logOpenRef[1];
|
|
1562
|
+
// Whether the last operation's git output is expanded. A new result
|
|
1563
|
+
// always starts collapsed: the summary line is the notification, the
|
|
1564
|
+
// raw output is opt-in.
|
|
1565
|
+
var outputOpenRef = React.useState(false);
|
|
1566
|
+
var outputOpen = outputOpenRef[0];
|
|
1567
|
+
var setOutputOpen = outputOpenRef[1];
|
|
1568
|
+
React.useEffect(function () {
|
|
1569
|
+
setOutputOpen(false);
|
|
1570
|
+
}, [state.lastResult]);
|
|
1380
1571
|
|
|
1381
1572
|
// Branch the user picked while the tree is dirty: the pre-check
|
|
1382
1573
|
// shows a warning instead of switching immediately; the switch only
|
|
@@ -1688,6 +1879,16 @@ window.__ModuleLoader__.load({
|
|
|
1688
1879
|
]);
|
|
1689
1880
|
|
|
1690
1881
|
var bodyChildren = [];
|
|
1882
|
+
// Operation feedback sits at the very TOP of the body. A push banner is
|
|
1883
|
+
// several lines long, and at the bottom of a 400px panel it landed under
|
|
1884
|
+
// the change list and the log — below the fold on any repo with a few
|
|
1885
|
+
// files, which is exactly where nobody looks after clicking a button.
|
|
1886
|
+
if (state.lastResult) {
|
|
1887
|
+
bodyChildren.push(outputView(state.lastResult, t, outputOpen, function () { setOutputOpen(!outputOpen); }));
|
|
1888
|
+
}
|
|
1889
|
+
if (busy) {
|
|
1890
|
+
bodyChildren.push(h("div", { key: "busy", style: S.meta }, busyLabel));
|
|
1891
|
+
}
|
|
1691
1892
|
if (state.phase === "loading" || state.phase === "idle") {
|
|
1692
1893
|
bodyChildren.push(h("div", { key: "loading", style: S.meta }, t("loading")));
|
|
1693
1894
|
} else if (state.phase === "error") {
|
|
@@ -1728,7 +1929,7 @@ window.__ModuleLoader__.load({
|
|
|
1728
1929
|
var value = event.target.value;
|
|
1729
1930
|
if (!value || value === state.branch) { setPendingBranch(null); return; }
|
|
1730
1931
|
if (state.dirty > 0) { setPendingBranch(value); return; }
|
|
1731
|
-
store.act("checkout", function () { return store.verbs.checkout(state.cwd, value); });
|
|
1932
|
+
store.act("checkout", function () { return store.verbs.checkout(state.cwd, value); }, null, { branch: value });
|
|
1732
1933
|
},
|
|
1733
1934
|
"aria-label": t("switch.aria")
|
|
1734
1935
|
}, selectChildren),
|
|
@@ -1799,7 +2000,7 @@ window.__ModuleLoader__.load({
|
|
|
1799
2000
|
if (nameText === "" || baseText === "" || busy !== null) return;
|
|
1800
2001
|
setNewBranchName("");
|
|
1801
2002
|
setNewBranchOpen(false);
|
|
1802
|
-
store.act("createBranch", function () { return store.verbs.createBranch(state.cwd, nameText, baseText); });
|
|
2003
|
+
store.act("createBranch", function () { return store.verbs.createBranch(state.cwd, nameText, baseText); }, null, { branch: nameText });
|
|
1803
2004
|
}
|
|
1804
2005
|
}
|
|
1805
2006
|
if (pendingBranch !== null && state.dirty > 0) {
|
|
@@ -1817,7 +2018,7 @@ window.__ModuleLoader__.load({
|
|
|
1817
2018
|
onClick: function () {
|
|
1818
2019
|
var target = pendingBranch;
|
|
1819
2020
|
setPendingBranch(null);
|
|
1820
|
-
store.act("checkout", function () { return store.verbs.checkout(state.cwd, target); });
|
|
2021
|
+
store.act("checkout", function () { return store.verbs.checkout(state.cwd, target); }, null, { branch: target });
|
|
1821
2022
|
}
|
|
1822
2023
|
}, busy === "checkout" ? t("busy.checkout") : t("switch.force"))
|
|
1823
2024
|
]));
|
|
@@ -1935,6 +2136,7 @@ window.__ModuleLoader__.load({
|
|
|
1935
2136
|
"no-provider": t("generate.noProvider"),
|
|
1936
2137
|
"no-model": t("generate.noModel"),
|
|
1937
2138
|
"llm-empty": t("generate.empty"),
|
|
2139
|
+
"llm-truncated": t("generate.truncated"),
|
|
1938
2140
|
"cancelled": t("generate.cancelled")
|
|
1939
2141
|
};
|
|
1940
2142
|
var text = known[details.code];
|
|
@@ -2036,17 +2238,6 @@ window.__ModuleLoader__.load({
|
|
|
2036
2238
|
bodyChildren.push(h("div", { key: "logBody" }, logRows));
|
|
2037
2239
|
}
|
|
2038
2240
|
|
|
2039
|
-
// last operation output
|
|
2040
|
-
if (state.lastResult) {
|
|
2041
|
-
bodyChildren.push(h("div", {
|
|
2042
|
-
key: "output",
|
|
2043
|
-
style: Object.assign({}, S.output, state.lastResult.kind === "ok" ? S.outputOk : S.outputError)
|
|
2044
|
-
}, state.lastResult.text || t("output.ok")));
|
|
2045
|
-
}
|
|
2046
|
-
if (busy) {
|
|
2047
|
-
bodyChildren.push(h("div", { key: "busy", style: S.meta }, busyLabel));
|
|
2048
|
-
}
|
|
2049
|
-
|
|
2050
2241
|
// Everything the open diff depends on, as one comparable string: a
|
|
2051
2242
|
// commit moves the oid, a branch switch moves several of these, a
|
|
2052
2243
|
// session switch changes the cwd, and STAGING moves the per-file index
|
|
@@ -2279,7 +2470,9 @@ window.__ModuleLoader__.load({
|
|
|
2279
2470
|
parseDiffSide: parseDiffSide,
|
|
2280
2471
|
diffRowView: diffRowView,
|
|
2281
2472
|
GitDiffPane: GitDiffPane,
|
|
2282
|
-
DIFF_ROW: DIFF_ROW
|
|
2473
|
+
DIFF_ROW: DIFF_ROW,
|
|
2474
|
+
stripAnsiEscapes: stripAnsiEscapes,
|
|
2475
|
+
outputView: outputView
|
|
2283
2476
|
};
|
|
2284
2477
|
return module.exports;
|
|
2285
2478
|
}
|
package/lib/index.js
CHANGED
|
@@ -84,8 +84,15 @@ const LLM_TIMEOUT_MS = 120000;
|
|
|
84
84
|
const LLM_DIFF_MAX_CHARS = 12000;
|
|
85
85
|
/** Diff-stat characters sent to the model. */
|
|
86
86
|
const LLM_STAT_MAX_CHARS = 2000;
|
|
87
|
-
/**
|
|
88
|
-
|
|
87
|
+
/**
|
|
88
|
+
* Output cap for one generated commit message. A reasoning model spends this
|
|
89
|
+
* budget on its thinking BEFORE any text exists and reports the two together as
|
|
90
|
+
* `completion_tokens`, so a cap sized for the message alone truncates every
|
|
91
|
+
* call that thinks a little. 8192 leaves room for a long think plus a
|
|
92
|
+
* subject+body; the model still stops on its own well before it (an unset cap
|
|
93
|
+
* would fall back to the adapter's own default, which is larger still).
|
|
94
|
+
*/
|
|
95
|
+
const LLM_MAX_TOKENS = 8192;
|
|
89
96
|
/** Accepted `generateMessage` modes. */
|
|
90
97
|
const GENERATE_MODES = ["staged", "unstaged", "all"];
|
|
91
98
|
/** Default mode: the only one whose content is what `commit` will actually record. */
|
|
@@ -535,7 +542,7 @@ async function gitFetch(rawCwd, rawRemote, signal) {
|
|
|
535
542
|
return fail("fetch-failed", cleanMessage(error), {});
|
|
536
543
|
}
|
|
537
544
|
const detail = [out.stdout, out.stderr].join("\n").trim();
|
|
538
|
-
return ok({ message: detail
|
|
545
|
+
return ok({ message: detail.slice(0, 800) });
|
|
539
546
|
}
|
|
540
547
|
|
|
541
548
|
/**
|
|
@@ -555,7 +562,7 @@ async function gitPull(rawCwd, signal) {
|
|
|
555
562
|
return fail("pull-failed", cleanMessage(error), {});
|
|
556
563
|
}
|
|
557
564
|
const detail = [out.stdout, out.stderr].join("\n").trim();
|
|
558
|
-
return ok({ message: detail
|
|
565
|
+
return ok({ message: detail.slice(0, 800) });
|
|
559
566
|
}
|
|
560
567
|
|
|
561
568
|
/** Read the repo's configured author (user.name / user.email) or null. */
|
|
@@ -624,7 +631,7 @@ async function gitPush(rawCwd, signal) {
|
|
|
624
631
|
return fail("push-failed", cleanMessage(error), {});
|
|
625
632
|
}
|
|
626
633
|
const detail = [out.stdout, out.stderr].join("\n").trim();
|
|
627
|
-
return ok({ message: detail
|
|
634
|
+
return ok({ message: detail.slice(0, 800) });
|
|
628
635
|
}
|
|
629
636
|
|
|
630
637
|
/**
|
|
@@ -759,7 +766,9 @@ async function gitStage(rawCwd, signal) {
|
|
|
759
766
|
return fail("stage-failed", cleanMessage(error), {});
|
|
760
767
|
}
|
|
761
768
|
const detail = [out.stdout, out.stderr].join("\n").trim();
|
|
762
|
-
|
|
769
|
+
// `message` is git's own output and nothing else: an operation git says
|
|
770
|
+
// nothing about reports an empty string, and the panel words its own summary.
|
|
771
|
+
return ok({ message: detail.slice(0, 800) });
|
|
763
772
|
}
|
|
764
773
|
|
|
765
774
|
/**
|
|
@@ -1049,7 +1058,8 @@ function generationMessage(text) {
|
|
|
1049
1058
|
* @param route - `{ provider, model }` from {@link resolveLlmRoute}.
|
|
1050
1059
|
* @param signal - cancellation (browser abort plus the server ceiling).
|
|
1051
1060
|
* @returns the trimmed message.
|
|
1052
|
-
* @throws on a terminal stream failure
|
|
1061
|
+
* @throws on a terminal stream failure, an output cap hit before any text
|
|
1062
|
+
* existed (`llm-truncated`), or genuinely empty output (`llm-empty`).
|
|
1053
1063
|
*/
|
|
1054
1064
|
async function requestCommitMessage(ctx, prompt, route, signal) {
|
|
1055
1065
|
const options = {
|
|
@@ -1076,6 +1086,16 @@ async function requestCommitMessage(ctx, prompt, route, signal) {
|
|
|
1076
1086
|
const ordered = (map) => [...map.entries()].sort((left, right) => left[0] - right[0]).map((entry) => entry[1]);
|
|
1077
1087
|
const message = (blocks.size > 0 ? ordered(blocks) : ordered(deltas)).join("").trim();
|
|
1078
1088
|
if (message === "") {
|
|
1089
|
+
// "Nothing came back" and "the budget ran out before any text did" are
|
|
1090
|
+
// different failures with different fixes; a reasoning model hits the
|
|
1091
|
+
// second one routinely (its thinking shares `completion_tokens` with the
|
|
1092
|
+
// message), so report the cap instead of a misleading generic empty.
|
|
1093
|
+
if (finish !== undefined && finish.kind === "max-tokens") {
|
|
1094
|
+
throw Object.assign(
|
|
1095
|
+
new Error(`the model hit the ${LLM_MAX_TOKENS}-token output cap before writing any commit message (a reasoning model spends that budget on its thinking first)`),
|
|
1096
|
+
{ pluginCode: "llm-truncated" }
|
|
1097
|
+
);
|
|
1098
|
+
}
|
|
1079
1099
|
throw Object.assign(new Error("the model returned no commit message"), { pluginCode: "llm-empty" });
|
|
1080
1100
|
}
|
|
1081
1101
|
return message;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xia-sc/dsh-git",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"description": "Complete Git management for the DeepSeek Harness Web GUI: branch switch, new-branch-from-base, fetch, pull, stage-all, commit (with AI-drafted message), push, status, and a click-to-open diff viewer (staged/unstaged, resizable) — as a collapsible floating panel that follows the current session's workspace.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"files": [
|
|
13
13
|
"lib",
|
|
14
14
|
"cordis.patch.yml",
|
|
15
|
-
"README.en.md"
|
|
15
|
+
"README.en.md",
|
|
16
|
+
"LICENSE"
|
|
16
17
|
],
|
|
17
18
|
"license": "MIT",
|
|
18
19
|
"author": "xia-sc",
|