@xia-sc/dsh-git 0.6.1 → 0.7.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.en.md +27 -2
- package/README.md +20 -3
- package/lib/client.js +395 -6
- package/lib/index.js +62 -9
- package/package.json +2 -1
package/README.en.md
CHANGED
|
@@ -35,6 +35,9 @@ new-branch-from-base.
|
|
|
35
35
|
(the top row with the Git title — press, drag, release; it stays where
|
|
36
36
|
dropped and is clamped inside the viewport; header buttons/inputs never
|
|
37
37
|
start a drag; double-click the header to snap back to center).
|
|
38
|
+
The header's right side is, in order, **⚙ Settings** (the commit-message
|
|
39
|
+
language — see "The commit area and AI drafting"), **↻ Refresh** and
|
|
40
|
+
**– Collapse**.
|
|
38
41
|
A "+ New branch" button beside the branch switcher opens an inline form:
|
|
39
42
|
new branch name + base-branch picker (local branches or full remote refs
|
|
40
43
|
like `origin/feature/x`) — confirming creates the branch from the base and
|
|
@@ -119,6 +122,20 @@ The commit area follows the real order of operations: **stage all → draft →
|
|
|
119
122
|
- **✨ AI draft**: sends the selected change set (diffstat + diff, truncated)
|
|
120
123
|
to the model the current session has selected and drops the generated
|
|
121
124
|
message into the input. Edit it, or just write your own.
|
|
125
|
+
- **Commit-message language (⚙ in the header)**: `Auto` by default — the model
|
|
126
|
+
follows the language already used by the repository's commits and comments
|
|
127
|
+
(the historical behaviour). Pick one of the presets (简体中文 / 繁體中文 /
|
|
128
|
+
English / 日本語 / 한국어 / Français / Deutsch / Español / Русский) or
|
|
129
|
+
`Custom…` and type a language name, and the draft is **forced** to write the
|
|
130
|
+
whole message — subject and body — in that language, with the system prompt
|
|
131
|
+
saying outright that it overrides the code comments, the past commit subjects
|
|
132
|
+
and the diff. A repository whose comments are English is exactly what drags
|
|
133
|
+
"Auto" off course, which is what this switch is for. The choice lives in the
|
|
134
|
+
browser's `localStorage` (key `dsh-git.commitLanguage`), applies to every
|
|
135
|
+
workspace and survives a reload; when storage is unavailable it silently
|
|
136
|
+
falls back to `Auto`. A custom name is validated by the host for shape and
|
|
137
|
+
length (≤ 60 chars, no line feed, quote or colon that could open a prompt
|
|
138
|
+
rule of its own) and reports `invalid-language` when it does not fit.
|
|
122
139
|
|
|
123
140
|
The model route comes from the current session's `modelSelection` projection
|
|
124
141
|
(pending pick first, then last used), falling back to the host's first
|
|
@@ -237,7 +254,7 @@ shell metacharacters, and leading dashes are all recorded verbatim.
|
|
|
237
254
|
| `commit` | `{ cwd, message }` | `{ message }`; `missing-author` error when `user.name/email` unset |
|
|
238
255
|
| `push` | `{ cwd }` | `{ message }` (120s timeout) |
|
|
239
256
|
| `log` | `{ cwd, count? }` | `{ repo, commits: [{sha, author, subject, refs}] }` (clamped 1..50) |
|
|
240
|
-
| `generateMessage` | `{ cwd, mode?, provider?, model?, sessionId? }` | `{ message, mode, provider, model }`. `mode` is `staged` (default) / `unstaged` / `all`; anything else is `invalid-mode`. `sessionId` is an optional non-empty string (over 200 chars is `invalid-session`); the panel sends the current session id so session-affine gateways can route the call. 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`. |
|
|
257
|
+
| `generateMessage` | `{ cwd, mode?, provider?, model?, sessionId?, language? }` | `{ message, mode, provider, model }`. `mode` is `staged` (default) / `unstaged` / `all`; anything else is `invalid-mode`. `sessionId` is an optional non-empty string (over 200 chars is `invalid-session`); the panel sends the current session id so session-affine gateways can route the call. `language` is an optional **language name** (the ⚙ setting; absent, `null`, an empty string and `auto` all mean "follow the repository") written into the system prompt to force the whole message into that language; a name that is too long (> 60 chars) or carries a line feed, quote or colon is `invalid-language`. 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`. |
|
|
241
258
|
|
|
242
259
|
> A failed result carries `error.code === "internal"` on the wire (the Connection
|
|
243
260
|
> envelope only requires a string), with the plugin's own diagnostic in
|
|
@@ -298,7 +315,7 @@ shell metacharacters, and leading dashes are all recorded verbatim.
|
|
|
298
315
|
- The browser bundle is hand-written (no build step); edits to `lib/client.js`
|
|
299
316
|
are picked up on refresh (no-cache), host-side edits need a `dsh web`
|
|
300
317
|
restart.
|
|
301
|
-
- Tests (`npm test` runs all
|
|
318
|
+
- Tests (`npm test` runs all five):
|
|
302
319
|
- `node test/smoke.mjs` — route, envelope, endpoint dispatch and input
|
|
303
320
|
validation (no git spawn: the session sandbox blocks child-process piped
|
|
304
321
|
stdio);
|
|
@@ -313,6 +330,14 @@ shell metacharacters, and leading dashes are all recorded verbatim.
|
|
|
313
330
|
removed line starting with `--`), the row renderer, the diff pane's header,
|
|
314
331
|
and `act()`'s result plumbing and localization (needs a react/react-dom copy,
|
|
315
332
|
e.g. via `DSH_GIT_REACT_ROOT`; SKIPs without one).
|
|
333
|
+
- `node test/slot-mount.mjs` — mounts both seats on the real
|
|
334
|
+
`SlotCore`/`SlotRegistry` with the real renderer (SKIPs without a host profile).
|
|
335
|
+
- `npm run test:ui:settings` — **real browser** offline regression
|
|
336
|
+
(playwright-core + local Chrome; needs neither `dsh web` nor authentication):
|
|
337
|
+
the script serves this checkout over loopback, mounts the client half with the
|
|
338
|
+
React UMD build, and then really clicks the ⚙ settings popover, switches the
|
|
339
|
+
language, and checks both the `localStorage` persistence and the `language`
|
|
340
|
+
the draft request carries; it writes `test/ui/settings-popover.png`.
|
|
316
341
|
- `npm run test:commit` — **end-to-end**: really spawns git in a throwaway
|
|
317
342
|
repository, commits through the plugin's own `/dsh-git-rpc/commit` route, and
|
|
318
343
|
reads the message back with `git log --format=%B` (multi-line, CRLF, non-ASCII,
|
package/README.md
CHANGED
|
@@ -24,7 +24,8 @@ DeepSeek Harness Web GUI 的完整 Git 管理插件,形态为一个**可折叠
|
|
|
24
24
|
(push 的 sideband banner、`LF will be replaced by CRLF` 这类提示)收在右侧的"详情 ▾"里,点开才展开,
|
|
25
25
|
不会被变更列表和最近提交顶到看不见的地方。面板**可通过顶栏拖动**(按住带 Git 标题的
|
|
26
26
|
那一行,拖到哪里就停在哪里,不会拖出视口;顶栏上的按钮/输入框不会触发
|
|
27
|
-
|
|
27
|
+
拖动;双击顶栏回到居中位置)。顶栏右侧依次是 **⚙ 设置**(提交信息语言,
|
|
28
|
+
见下文「提交区与 AI 起草」)、**↻ 刷新**、**– 收起**。
|
|
28
29
|
分支切换器旁边的"+ 新建分支"按钮会展开一个内联表单:新分支名 + 基分支
|
|
29
30
|
选择器(本地分支或 `origin/feature/x` 这样的完整远端引用)——确认后从
|
|
30
31
|
该基分支创建新分支并切换过去。
|
|
@@ -83,6 +84,15 @@ DeepSeek Harness Web GUI 的完整 Git 管理插件,形态为一个**可折叠
|
|
|
83
84
|
提交真正会记录的内容**——用其他依据生成的描述可能与实际提交不符。
|
|
84
85
|
- **✨ AI 生成**:把选中的改动(diffstat + diff,截断后)交给当前会话所选
|
|
85
86
|
模型,生成的提交信息直接填入输入框;不满意可改,也可以直接手写。
|
|
87
|
+
- **提交信息语言(顶栏 ⚙ 设置)**:默认「自动」——让模型跟随仓库现有提交与
|
|
88
|
+
注释的语言(历史行为)。选中某个语言(简体中文 / 繁體中文 / English / 日本語 /
|
|
89
|
+
한국어 / Français / Deutsch / Español / Русский)或「自定义…」手填一个语言名后,
|
|
90
|
+
生成时会在 system prompt 里**强制**整条提交信息(主题 + 正文)只用该语言,
|
|
91
|
+
并明确写清它覆盖代码注释、历史提交与 diff 的语言——仓库注释是英文时,
|
|
92
|
+
「自动」会被它们带跑,这正是这个开关要解决的问题。设置存在浏览器的
|
|
93
|
+
`localStorage`(键 `dsh-git.commitLanguage`),对所有工作区生效、刷新后保留;
|
|
94
|
+
存储不可用时静默回落到「自动」。自定义语言名由宿主按形状与长度(≤ 60 字符、
|
|
95
|
+
无换行/引号/冒号等可自开规则的字符)校验,非法时报 `invalid-language`。
|
|
86
96
|
|
|
87
97
|
生成用的模型路由取当前会话的 `modelSelection` 投影(待生效的选择优先,
|
|
88
98
|
其次是上次实际使用),取不到时回落到宿主注册的第一条路由。请求会带上**当前会话 id**
|
|
@@ -183,7 +193,7 @@ dsh plugin --profile web remove @xia-sc/dsh-git
|
|
|
183
193
|
| `commit` | `{ cwd, message }` | `{ message }`;未配置 `user.name/email` 时报 `missing-author` 错误 |
|
|
184
194
|
| `push` | `{ cwd }` | `{ message }`(120s 超时) |
|
|
185
195
|
| `log` | `{ cwd, count? }` | `{ repo, commits: [{sha, author, subject, refs}] }`(钳制 1..50) |
|
|
186
|
-
| `generateMessage` | `{ cwd, mode?, provider?, model?, sessionId? }` | `{ message, mode, provider, model }`。`mode` 为 `staged`(默认)/`unstaged`/`all`,非法值报 `invalid-mode`;`sessionId` 为可选的非空字符串(超过 200 字符报 `invalid-session`),面板会带上当前会话 id
|
|
196
|
+
| `generateMessage` | `{ cwd, mode?, provider?, model?, sessionId?, language? }` | `{ message, mode, provider, model }`。`mode` 为 `staged`(默认)/`unstaged`/`all`,非法值报 `invalid-mode`;`sessionId` 为可选的非空字符串(超过 200 字符报 `invalid-session`),面板会带上当前会话 id 供需要会话亲和的网关路由。`language` 是可选的**语言名**(顶栏 ⚙ 里的设置;缺失、`null`、空串与 `auto` 都表示「跟随仓库」),它会被写进 system prompt 强制整条信息只用该语言;形状或长度不合法(> 60 字符、含换行/引号/冒号等)报 `invalid-language`。失败码见 `error.details.code`:`no-changes`、`no-provider`、`no-model`、`llm-truncated`(输出上限用尽、一个字都没写出来)、`llm-empty`、`cancelled`、`llm-failed`。 |
|
|
187
197
|
|
|
188
198
|
> 失败结果的 `error.code` 在线路上固定为 `"internal"`(Connection 信封只要求它是字符串),
|
|
189
199
|
> 插件自己的诊断码放在 `error.details.code`;客户端按该码做本地化文案。
|
|
@@ -237,7 +247,14 @@ dsh plugin --profile web remove @xia-sc/dsh-git
|
|
|
237
247
|
diff 解析器(行号 / 分类 / `--` 开头的删除行)、行渲染、差异面板标题栏与
|
|
238
248
|
`act()` 的结果回传/本地化(需要一份 react/react-dom,可用
|
|
239
249
|
`DSH_GIT_REACT_ROOT` 指定,找不到则 SKIP)。
|
|
240
|
-
-
|
|
250
|
+
- `node test/slot-mount.mjs` —— 用真实 `SlotCore`/`SlotRegistry` 与真渲染器把两个
|
|
251
|
+
座位挂起来(找不到宿主包则 SKIP)。
|
|
252
|
+
- `npm run test:ui:settings` —— **真实浏览器**离线回归(playwright-core + 本机
|
|
253
|
+
Chrome,既不需要 `dsh web` 也不需要认证):脚本自己起回环 http 服务,用
|
|
254
|
+
React UMD 挂载客户端半边,真点 ⚙ 设置弹层、切换语言、验证 `localStorage`
|
|
255
|
+
持久化与「✨ AI 生成」请求里带的 `language`;截图落在
|
|
256
|
+
`test/ui/settings-popover.png`。
|
|
257
|
+
- 也提供 `npm test`(依次跑五个)。
|
|
241
258
|
- `npm run test:commit` —— **端到端**:在临时仓库里真起 git,走插件的
|
|
242
259
|
`/dsh-git-rpc/commit` 路由提交,再用 `git log --format=%B` 逐字节比对提交
|
|
243
260
|
信息(多行、CRLF、中文、前导 `-`、shell 元字符等),并确认非法信息被拒且
|
package/lib/client.js
CHANGED
|
@@ -78,6 +78,26 @@ window.__ModuleLoader__.load({
|
|
|
78
78
|
"generate.failed": "生成失败",
|
|
79
79
|
"generate.failedDetail": "生成失败:{message}",
|
|
80
80
|
"generate.failedRoute": "生成失败:{message}(模型路由 {provider} / {model})",
|
|
81
|
+
// Settings popover (the header's ⚙). The commit-message language is a
|
|
82
|
+
// forced output rule, not a hint: the host turns it into an explicit
|
|
83
|
+
// override in the system prompt.
|
|
84
|
+
"settings.aria": "Git 设置",
|
|
85
|
+
"settings.title": "设置",
|
|
86
|
+
"settings.close": "关闭",
|
|
87
|
+
"settings.language": "提交信息语言",
|
|
88
|
+
"settings.language.auto": "自动(跟随仓库)",
|
|
89
|
+
"settings.language.zh-CN": "简体中文",
|
|
90
|
+
"settings.language.zh-TW": "繁體中文",
|
|
91
|
+
"settings.language.en": "English",
|
|
92
|
+
"settings.language.ja": "日本語",
|
|
93
|
+
"settings.language.ko": "한국어",
|
|
94
|
+
"settings.language.fr": "Français",
|
|
95
|
+
"settings.language.de": "Deutsch",
|
|
96
|
+
"settings.language.es": "Español",
|
|
97
|
+
"settings.language.ru": "Русский",
|
|
98
|
+
"settings.language.custom": "自定义…",
|
|
99
|
+
"settings.language.customPlaceholder": "例如:Esperanto",
|
|
100
|
+
"settings.language.hint": "「AI 生成」只用所选语言写提交信息,不受代码注释与历史提交影响;「自动」跟随仓库。",
|
|
81
101
|
"commit.placeholder": "提交信息(可多行,Ctrl+Enter 提交)…",
|
|
82
102
|
"commit.empty": "请输入提交信息",
|
|
83
103
|
"changes.title": "变更",
|
|
@@ -184,6 +204,26 @@ window.__ModuleLoader__.load({
|
|
|
184
204
|
"generate.failed": "Draft failed",
|
|
185
205
|
"generate.failedDetail": "Draft failed: {message}",
|
|
186
206
|
"generate.failedRoute": "Draft failed: {message} (route {provider} / {model})",
|
|
207
|
+
// Settings popover (the header's ⚙). The commit-message language is a
|
|
208
|
+
// forced output rule, not a hint: the host turns it into an explicit
|
|
209
|
+
// override in the system prompt.
|
|
210
|
+
"settings.aria": "Git settings",
|
|
211
|
+
"settings.title": "Settings",
|
|
212
|
+
"settings.close": "Close",
|
|
213
|
+
"settings.language": "Commit message language",
|
|
214
|
+
"settings.language.auto": "Auto (follow the repository)",
|
|
215
|
+
"settings.language.zh-CN": "简体中文",
|
|
216
|
+
"settings.language.zh-TW": "繁體中文",
|
|
217
|
+
"settings.language.en": "English",
|
|
218
|
+
"settings.language.ja": "日本語",
|
|
219
|
+
"settings.language.ko": "한국어",
|
|
220
|
+
"settings.language.fr": "Français",
|
|
221
|
+
"settings.language.de": "Deutsch",
|
|
222
|
+
"settings.language.es": "Español",
|
|
223
|
+
"settings.language.ru": "Русский",
|
|
224
|
+
"settings.language.custom": "Custom…",
|
|
225
|
+
"settings.language.customPlaceholder": "e.g. Esperanto",
|
|
226
|
+
"settings.language.hint": "The AI draft is written in the chosen language only, whatever the comments and past commits use; Auto follows the repository.",
|
|
187
227
|
"commit.placeholder": "Commit message (multi-line allowed, Ctrl+Enter commits)…",
|
|
188
228
|
"commit.empty": "Enter a commit message",
|
|
189
229
|
"changes.title": "Changes",
|
|
@@ -244,6 +284,90 @@ window.__ModuleLoader__.load({
|
|
|
244
284
|
"status.changed": "changed"
|
|
245
285
|
};
|
|
246
286
|
|
|
287
|
+
/**
|
|
288
|
+
* Commit-message languages the settings panel offers: first the `auto`
|
|
289
|
+
* entry (the historical "follow the repository" behaviour), then the
|
|
290
|
+
* presets.
|
|
291
|
+
*
|
|
292
|
+
* `directive` is the language NAME handed to the host, which turns it
|
|
293
|
+
* into an explicit override in the system prompt. The panel owns this
|
|
294
|
+
* mapping so the host needs no language table of its own and a custom
|
|
295
|
+
* entry can send any name at all. The names are bilingual on purpose:
|
|
296
|
+
* the model reads them unambiguously whichever UI locale is active.
|
|
297
|
+
*/
|
|
298
|
+
var LANGUAGE_PRESETS = [
|
|
299
|
+
{ id: "auto", directive: undefined },
|
|
300
|
+
{ id: "zh-CN", directive: "Simplified Chinese (简体中文)" },
|
|
301
|
+
{ id: "zh-TW", directive: "Traditional Chinese (繁體中文)" },
|
|
302
|
+
{ id: "en", directive: "English" },
|
|
303
|
+
{ id: "ja", directive: "Japanese (日本語)" },
|
|
304
|
+
{ id: "ko", directive: "Korean (한국어)" },
|
|
305
|
+
{ id: "fr", directive: "French (français)" },
|
|
306
|
+
{ id: "de", directive: "German (Deutsch)" },
|
|
307
|
+
{ id: "es", directive: "Spanish (español)" },
|
|
308
|
+
{ id: "ru", directive: "Russian (русский)" }
|
|
309
|
+
];
|
|
310
|
+
/** The custom entry: not a preset, its directive is the user's own text. */
|
|
311
|
+
var LANGUAGE_CUSTOM_ID = "custom";
|
|
312
|
+
/** Every id the select can carry, `custom` included. */
|
|
313
|
+
var LANGUAGE_IDS = LANGUAGE_PRESETS.map(function (preset) { return preset.id; }).concat([LANGUAGE_CUSTOM_ID]);
|
|
314
|
+
/**
|
|
315
|
+
* Where the setting lives. Global by design: it is a user preference about
|
|
316
|
+
* how the panel writes, not state of any one repository.
|
|
317
|
+
*/
|
|
318
|
+
var LANGUAGE_STORAGE_KEY = "dsh-git.commitLanguage";
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* The page's `localStorage`, or null when there is none.
|
|
322
|
+
*
|
|
323
|
+
* Reached through `window`, never as a bare `localStorage`: a test or
|
|
324
|
+
* server-side render has no storage (Node >= 22 even ships an experimental
|
|
325
|
+
* global that logs a warning and then throws), and a browser can refuse it
|
|
326
|
+
* outright (private mode, blocked site storage). None of that is an error
|
|
327
|
+
* here — the setting falls back to `auto`.
|
|
328
|
+
*/
|
|
329
|
+
function pageStorage() {
|
|
330
|
+
try {
|
|
331
|
+
if (typeof window === "undefined" || window === null) return null;
|
|
332
|
+
var store = window.localStorage;
|
|
333
|
+
return store === undefined || store === null ? null : store;
|
|
334
|
+
} catch (error) {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Read the persisted language setting.
|
|
341
|
+
* @returns `{ id, text }`, or null when nothing is stored/readable.
|
|
342
|
+
*/
|
|
343
|
+
function readStoredLanguage() {
|
|
344
|
+
try {
|
|
345
|
+
var store = pageStorage();
|
|
346
|
+
if (store === null) return null;
|
|
347
|
+
var raw = store.getItem(LANGUAGE_STORAGE_KEY);
|
|
348
|
+
if (raw === null || raw === "") return null;
|
|
349
|
+
var parsed = JSON.parse(raw);
|
|
350
|
+
if (parsed === null || typeof parsed !== "object") return null;
|
|
351
|
+
return {
|
|
352
|
+
id: typeof parsed.id === "string" ? parsed.id : "auto",
|
|
353
|
+
text: typeof parsed.text === "string" ? parsed.text : ""
|
|
354
|
+
};
|
|
355
|
+
} catch (error) {
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** Persist the language setting, ignoring a storage that refuses to write. */
|
|
361
|
+
function writeStoredLanguage(id, text) {
|
|
362
|
+
try {
|
|
363
|
+
var store = pageStorage();
|
|
364
|
+
if (store === null) return;
|
|
365
|
+
store.setItem(LANGUAGE_STORAGE_KEY, JSON.stringify({ id: id, text: text }));
|
|
366
|
+
} catch (error) {
|
|
367
|
+
// Quota or private mode: the choice still holds for this page.
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
247
371
|
/** React.createElement shorthand. */
|
|
248
372
|
function h(type, props) {
|
|
249
373
|
var children = Array.prototype.slice.call(arguments, 2);
|
|
@@ -335,12 +459,85 @@ window.__ModuleLoader__.load({
|
|
|
335
459
|
boxSizing: "border-box",
|
|
336
460
|
cursor: "pointer"
|
|
337
461
|
},
|
|
462
|
+
// The same ghost button while its popover is open: an accent fill, not a
|
|
463
|
+
// second button shape, so the header keeps one visual language.
|
|
464
|
+
iconButtonActive: {
|
|
465
|
+
background: "color-mix(in srgb, var(--dsw-alias-state-business-primary, #4d6bfe) 16%, transparent)",
|
|
466
|
+
border: "1px solid color-mix(in srgb, var(--dsw-alias-state-business-primary, #4d6bfe) 45%, transparent)",
|
|
467
|
+
color: "var(--dsw-alias-state-business-primary, #4d6bfe)"
|
|
468
|
+
},
|
|
338
469
|
body: {
|
|
339
470
|
flex: "1",
|
|
340
471
|
minHeight: 0,
|
|
341
472
|
padding: "10px 12px 12px",
|
|
342
473
|
overflowY: "auto"
|
|
343
474
|
},
|
|
475
|
+
// Covers the whole panel — header included — while the popover is open, so
|
|
476
|
+
// a click on anything else inside the panel closes the settings. Kept
|
|
477
|
+
// `absolute` on purpose: the panel rides `transform: translateX(-50%)`
|
|
478
|
+
// while centered, and a transformed ancestor both re-parents a `fixed`
|
|
479
|
+
// child and clips it to the panel's `overflow: hidden` anyway.
|
|
480
|
+
settingsBackdrop: {
|
|
481
|
+
position: "absolute",
|
|
482
|
+
top: 0,
|
|
483
|
+
right: 0,
|
|
484
|
+
bottom: 0,
|
|
485
|
+
left: 0,
|
|
486
|
+
zIndex: 2,
|
|
487
|
+
background: "transparent"
|
|
488
|
+
},
|
|
489
|
+
// Anchored under the header's right edge, inside the panel's own fixed
|
|
490
|
+
// box. Absolute (not fixed) so it moves with the panel when dragged.
|
|
491
|
+
settingsPopover: {
|
|
492
|
+
position: "absolute",
|
|
493
|
+
top: 42,
|
|
494
|
+
right: 8,
|
|
495
|
+
zIndex: 3,
|
|
496
|
+
width: 236,
|
|
497
|
+
// Never taller than the panel that holds it (the short-window case),
|
|
498
|
+
// in which case the popover scrolls instead of being clipped.
|
|
499
|
+
maxHeight: "calc(100% - 52px)",
|
|
500
|
+
overflowY: "auto",
|
|
501
|
+
display: "flex",
|
|
502
|
+
flexDirection: "column",
|
|
503
|
+
gap: 8,
|
|
504
|
+
padding: 10,
|
|
505
|
+
border: "1px solid var(--dsw-alias-border-l1, rgba(127,127,127,0.3))",
|
|
506
|
+
borderRadius: 10,
|
|
507
|
+
background: "var(--dsw-alias-bg-base, #1e1e1e)",
|
|
508
|
+
boxShadow: "var(--dsw-shadow-lv2, 0 8px 32px rgba(0,0,0,0.45))",
|
|
509
|
+
boxSizing: "border-box",
|
|
510
|
+
cursor: "default",
|
|
511
|
+
textAlign: "left"
|
|
512
|
+
},
|
|
513
|
+
settingsHead: {
|
|
514
|
+
display: "flex",
|
|
515
|
+
alignItems: "center",
|
|
516
|
+
gap: 8
|
|
517
|
+
},
|
|
518
|
+
settingsTitle: {
|
|
519
|
+
flex: "1",
|
|
520
|
+
minWidth: 0,
|
|
521
|
+
fontSize: 12,
|
|
522
|
+
fontWeight: 600,
|
|
523
|
+
color: "var(--dsw-alias-label-primary, #e5e7eb)"
|
|
524
|
+
},
|
|
525
|
+
settingsRow: {
|
|
526
|
+
display: "flex",
|
|
527
|
+
alignItems: "center",
|
|
528
|
+
gap: 8,
|
|
529
|
+
minWidth: 0
|
|
530
|
+
},
|
|
531
|
+
settingsLabel: {
|
|
532
|
+
flex: "none",
|
|
533
|
+
fontSize: 12,
|
|
534
|
+
color: "var(--dsw-alias-label-secondary, #c8ccd2)"
|
|
535
|
+
},
|
|
536
|
+
settingsHint: {
|
|
537
|
+
fontSize: 11,
|
|
538
|
+
lineHeight: "16px",
|
|
539
|
+
color: "var(--dsw-alias-label-secondary, #c8ccd2)"
|
|
540
|
+
},
|
|
344
541
|
row: {
|
|
345
542
|
display: "flex",
|
|
346
543
|
alignItems: "center",
|
|
@@ -1136,11 +1333,13 @@ window.__ModuleLoader__.load({
|
|
|
1136
1333
|
commit: function (cwd, message) { return rpc(ctx, "commit", { cwd: cwd, message: message }); },
|
|
1137
1334
|
push: function (cwd) { return rpc(ctx, "push", { cwd: cwd }); },
|
|
1138
1335
|
log: function (cwd) { return rpc(ctx, "log", { cwd: cwd, count: 10 }); },
|
|
1139
|
-
generateMessage: function (cwd, mode, provider, model, sessionId) {
|
|
1336
|
+
generateMessage: function (cwd, mode, provider, model, sessionId, language) {
|
|
1140
1337
|
// The session travels with the request: session-labelled
|
|
1141
1338
|
// providers reject an unidentified call, and the host forwards
|
|
1142
1339
|
// a session id to its adapter only when one is present.
|
|
1143
|
-
|
|
1340
|
+
// `language` is the forced output language from the settings
|
|
1341
|
+
// popover; absent means "let the model follow the repository".
|
|
1342
|
+
return rpc(ctx, "generateMessage", { cwd: cwd, mode: mode, provider: provider, model: model, sessionId: sessionId, language: language });
|
|
1144
1343
|
}
|
|
1145
1344
|
};
|
|
1146
1345
|
|
|
@@ -1152,6 +1351,16 @@ window.__ModuleLoader__.load({
|
|
|
1152
1351
|
*/
|
|
1153
1352
|
var sessionId = null;
|
|
1154
1353
|
|
|
1354
|
+
// The commit-message language setting, read once from storage. It is
|
|
1355
|
+
// deliberately NOT state of one Session: it is a preference of the user
|
|
1356
|
+
// about how the panel writes, shared by every workspace and kept across
|
|
1357
|
+
// reloads. idleState() republishes it on every emit, so a refresh, a
|
|
1358
|
+
// workspace switch and a status read all keep it — a hard-coded `auto`
|
|
1359
|
+
// in idleState() would silently reset the setting on the next read.
|
|
1360
|
+
var storedLanguage = readStoredLanguage();
|
|
1361
|
+
var languageId = storedLanguage !== null && LANGUAGE_IDS.indexOf(storedLanguage.id) >= 0 ? storedLanguage.id : "auto";
|
|
1362
|
+
var languageText = storedLanguage !== null ? storedLanguage.text : "";
|
|
1363
|
+
|
|
1155
1364
|
function idleState() {
|
|
1156
1365
|
return {
|
|
1157
1366
|
sessionId: sessionId,
|
|
@@ -1173,7 +1382,10 @@ window.__ModuleLoader__.load({
|
|
|
1173
1382
|
busy: null,
|
|
1174
1383
|
refreshing: false,
|
|
1175
1384
|
lastResult: null,
|
|
1176
|
-
panelOpen: false
|
|
1385
|
+
panelOpen: false,
|
|
1386
|
+
settingsOpen: false,
|
|
1387
|
+
generateLanguage: languageId,
|
|
1388
|
+
generateLanguageText: languageText
|
|
1177
1389
|
};
|
|
1178
1390
|
}
|
|
1179
1391
|
|
|
@@ -1208,6 +1420,7 @@ window.__ModuleLoader__.load({
|
|
|
1208
1420
|
phase: "error",
|
|
1209
1421
|
error: status && status.error ? String(status.error.message || "git status failed") : "git status failed",
|
|
1210
1422
|
panelOpen: state.panelOpen,
|
|
1423
|
+
settingsOpen: state.settingsOpen,
|
|
1211
1424
|
lastResult: state.lastResult
|
|
1212
1425
|
});
|
|
1213
1426
|
}
|
|
@@ -1227,6 +1440,7 @@ window.__ModuleLoader__.load({
|
|
|
1227
1440
|
remote: bv !== null && Array.isArray(bv.remote) ? bv.remote : [],
|
|
1228
1441
|
commits: lv !== null && Array.isArray(lv.commits) ? lv.commits : [],
|
|
1229
1442
|
panelOpen: state.panelOpen,
|
|
1443
|
+
settingsOpen: state.settingsOpen,
|
|
1230
1444
|
lastResult: state.lastResult
|
|
1231
1445
|
});
|
|
1232
1446
|
}
|
|
@@ -1261,6 +1475,7 @@ window.__ModuleLoader__.load({
|
|
|
1261
1475
|
phase: "error",
|
|
1262
1476
|
error: error && error.message ? String(error.message) : String(error),
|
|
1263
1477
|
panelOpen: state.panelOpen,
|
|
1478
|
+
settingsOpen: state.settingsOpen,
|
|
1264
1479
|
lastResult: state.lastResult
|
|
1265
1480
|
}));
|
|
1266
1481
|
});
|
|
@@ -1333,8 +1548,57 @@ window.__ModuleLoader__.load({
|
|
|
1333
1548
|
});
|
|
1334
1549
|
}
|
|
1335
1550
|
|
|
1551
|
+
/**
|
|
1552
|
+
* Open/close the workbench. Closing it also drops the settings popover:
|
|
1553
|
+
* the panel component keeps its state while collapsed (it renders
|
|
1554
|
+
* nothing), so a popover left open would spring back over the next open.
|
|
1555
|
+
*/
|
|
1336
1556
|
function setPanelOpen(open) {
|
|
1337
|
-
emit(Object.assign({}, state, {
|
|
1557
|
+
emit(Object.assign({}, state, {
|
|
1558
|
+
panelOpen: open === true,
|
|
1559
|
+
settingsOpen: open === true ? state.settingsOpen : false
|
|
1560
|
+
}));
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
/** Open/close the header's settings popover. */
|
|
1564
|
+
function setSettingsOpen(open) {
|
|
1565
|
+
emit(Object.assign({}, state, { settingsOpen: open === true }));
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
/**
|
|
1569
|
+
* Persist the commit-message language. The settings popover is the only
|
|
1570
|
+
* writer; both seats read it back through the shared state, so the select
|
|
1571
|
+
* always shows what the next draft will actually ask for.
|
|
1572
|
+
* @param id - a preset id, or `custom`.
|
|
1573
|
+
* @param text - the custom language name (read only for `custom`).
|
|
1574
|
+
*/
|
|
1575
|
+
function setGenerateLanguage(id, text) {
|
|
1576
|
+
languageId = LANGUAGE_IDS.indexOf(id) >= 0 ? id : "auto";
|
|
1577
|
+
// Kept verbatim: this value is echoed back into a controlled text
|
|
1578
|
+
// field, and trimming on every keystroke eats the space a user just
|
|
1579
|
+
// typed. The directive is trimmed where it is actually read.
|
|
1580
|
+
languageText = typeof text === "string" ? text : "";
|
|
1581
|
+
writeStoredLanguage(languageId, languageText);
|
|
1582
|
+
emit(Object.assign({}, state, {
|
|
1583
|
+
generateLanguage: languageId,
|
|
1584
|
+
generateLanguageText: languageText
|
|
1585
|
+
}));
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
/**
|
|
1589
|
+
* The language directive to send with a draft. `undefined` means "follow
|
|
1590
|
+
* the repository" — which is also what an EMPTY custom field means: an
|
|
1591
|
+
* empty string would be rejected by the host's validation instead of
|
|
1592
|
+
* drafting, and "I typed nothing yet" must not break the button.
|
|
1593
|
+
* @returns the language NAME, or undefined.
|
|
1594
|
+
*/
|
|
1595
|
+
function languageDirective() {
|
|
1596
|
+
if (languageId === LANGUAGE_CUSTOM_ID) {
|
|
1597
|
+
var trimmed = languageText.trim();
|
|
1598
|
+
return trimmed === "" ? undefined : trimmed;
|
|
1599
|
+
}
|
|
1600
|
+
var preset = LANGUAGE_PRESETS.filter(function (entry) { return entry.id === languageId; })[0];
|
|
1601
|
+
return preset === undefined ? undefined : preset.directive;
|
|
1338
1602
|
}
|
|
1339
1603
|
|
|
1340
1604
|
return {
|
|
@@ -1344,6 +1608,9 @@ window.__ModuleLoader__.load({
|
|
|
1344
1608
|
bindSession: bindSession,
|
|
1345
1609
|
act: act,
|
|
1346
1610
|
setPanelOpen: setPanelOpen,
|
|
1611
|
+
setSettingsOpen: setSettingsOpen,
|
|
1612
|
+
setGenerateLanguage: setGenerateLanguage,
|
|
1613
|
+
languageDirective: languageDirective,
|
|
1347
1614
|
verbs: verbs
|
|
1348
1615
|
};
|
|
1349
1616
|
}
|
|
@@ -1359,6 +1626,73 @@ window.__ModuleLoader__.load({
|
|
|
1359
1626
|
fontWeight: 600
|
|
1360
1627
|
};
|
|
1361
1628
|
|
|
1629
|
+
/**
|
|
1630
|
+
* The settings popover: one row per setting, anchored under the header.
|
|
1631
|
+
*
|
|
1632
|
+
* Pure and exported, so the render test can drive it directly — SSR runs no
|
|
1633
|
+
* effects and the popover starts closed, so a static panel render never
|
|
1634
|
+
* contains it. Everything it needs arrives through `spec`; the store's
|
|
1635
|
+
* `generateLanguage` stays the single source of truth (the select is
|
|
1636
|
+
* controlled), so the popover cannot drift from what the next draft sends.
|
|
1637
|
+
*
|
|
1638
|
+
* @param spec - `{ t, language, customText, onLanguage, onCustomText, onClose }`.
|
|
1639
|
+
* @returns the popover element.
|
|
1640
|
+
*/
|
|
1641
|
+
function settingsView(spec) {
|
|
1642
|
+
var t = spec.t;
|
|
1643
|
+
var children = [
|
|
1644
|
+
h("div", { key: "head", style: S.settingsHead }, [
|
|
1645
|
+
h("span", { key: "title", style: S.settingsTitle }, t("settings.title")),
|
|
1646
|
+
h("button", {
|
|
1647
|
+
key: "close",
|
|
1648
|
+
type: "button",
|
|
1649
|
+
style: S.iconButton,
|
|
1650
|
+
onClick: spec.onClose,
|
|
1651
|
+
title: t("settings.close"),
|
|
1652
|
+
"aria-label": t("settings.close"),
|
|
1653
|
+
"data-dsh-git": "settings-close"
|
|
1654
|
+
}, "×")
|
|
1655
|
+
]),
|
|
1656
|
+
h("div", { key: "language", style: S.settingsRow }, [
|
|
1657
|
+
h("span", { key: "label", style: S.settingsLabel }, t("settings.language")),
|
|
1658
|
+
h("select", {
|
|
1659
|
+
key: "select",
|
|
1660
|
+
style: S.select,
|
|
1661
|
+
value: spec.language,
|
|
1662
|
+
title: t("settings.language"),
|
|
1663
|
+
"aria-label": t("settings.language"),
|
|
1664
|
+
"data-dsh-git": "settings-language",
|
|
1665
|
+
onChange: function (event) { spec.onLanguage(event.target.value); }
|
|
1666
|
+
}, LANGUAGE_IDS.map(function (id) {
|
|
1667
|
+
return h("option", { key: id, value: id, style: OPTION_STYLE }, t("settings.language." + id));
|
|
1668
|
+
}))
|
|
1669
|
+
])
|
|
1670
|
+
];
|
|
1671
|
+
// The free-form name only exists behind its own entry: a text field
|
|
1672
|
+
// shown beside the preset list would read as "type here too".
|
|
1673
|
+
if (spec.language === LANGUAGE_CUSTOM_ID) {
|
|
1674
|
+
children.push(h("input", {
|
|
1675
|
+
key: "custom",
|
|
1676
|
+
type: "text",
|
|
1677
|
+
style: S.input,
|
|
1678
|
+
value: spec.customText,
|
|
1679
|
+
placeholder: t("settings.language.customPlaceholder"),
|
|
1680
|
+
title: t("settings.language.customPlaceholder"),
|
|
1681
|
+
"aria-label": t("settings.language.customPlaceholder"),
|
|
1682
|
+
"data-dsh-git": "settings-language-text",
|
|
1683
|
+
onChange: function (event) { spec.onCustomText(event.target.value); }
|
|
1684
|
+
}));
|
|
1685
|
+
}
|
|
1686
|
+
children.push(h("div", { key: "hint", style: S.settingsHint }, t("settings.language.hint")));
|
|
1687
|
+
return h("div", {
|
|
1688
|
+
key: "settings",
|
|
1689
|
+
style: S.settingsPopover,
|
|
1690
|
+
role: "dialog",
|
|
1691
|
+
"aria-label": t("settings.title"),
|
|
1692
|
+
"data-dsh-git": "settings-popover"
|
|
1693
|
+
}, children);
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1362
1696
|
/** One branch option list; remote picks carry the DWIM-able short name. */
|
|
1363
1697
|
function branchOptions(state, t) {
|
|
1364
1698
|
var local = state.local.map(function (entry) {
|
|
@@ -1906,6 +2240,13 @@ window.__ModuleLoader__.load({
|
|
|
1906
2240
|
setOutputOpen(false);
|
|
1907
2241
|
}, [state.lastResult]);
|
|
1908
2242
|
|
|
2243
|
+
// Whether the header's ⚙ popover is open. It lives in the STORE, not in
|
|
2244
|
+
// component state: the store already owns `panelOpen`, and closing the
|
|
2245
|
+
// panel has to take the popover down with it (the component keeps its
|
|
2246
|
+
// state while collapsed, since it renders null) — one owner, one rule.
|
|
2247
|
+
var settingsOpen = state.settingsOpen === true;
|
|
2248
|
+
function setSettingsOpen(open) { store.setSettingsOpen(open); }
|
|
2249
|
+
|
|
1909
2250
|
// Branch the user picked while the tree is dirty: the pre-check
|
|
1910
2251
|
// shows a warning instead of switching immediately; the switch only
|
|
1911
2252
|
// runs via the "switch anyway" button (or once the tree is clean).
|
|
@@ -2166,6 +2507,11 @@ window.__ModuleLoader__.load({
|
|
|
2166
2507
|
transform: "none"
|
|
2167
2508
|
});
|
|
2168
2509
|
}
|
|
2510
|
+
// The settings popover lives inside the panel's own box, and the
|
|
2511
|
+
// single-column panel is content-sized: with a short body (the initial
|
|
2512
|
+
// loading line, an error, an empty repository) `overflow: hidden` would
|
|
2513
|
+
// clip it. 240px is the popover's own height plus the header band.
|
|
2514
|
+
if (settingsOpen && panelStyle.height === undefined) panelStyle.minHeight = 240;
|
|
2169
2515
|
|
|
2170
2516
|
if (cwd === null) {
|
|
2171
2517
|
// No Session bound (nothing to float over the input).
|
|
@@ -2192,6 +2538,19 @@ window.__ModuleLoader__.load({
|
|
|
2192
2538
|
}, [
|
|
2193
2539
|
h("span", { key: "title", style: S.title }, "⎇ " + t("panel.title")),
|
|
2194
2540
|
h("span", { key: "cwd", style: S.cwdPath, title: cwd }, shortText(cwd, 80)),
|
|
2541
|
+
h("button", {
|
|
2542
|
+
key: "settings",
|
|
2543
|
+
type: "button",
|
|
2544
|
+
// The panel's own settings live behind this gear rather than in
|
|
2545
|
+
// the body: they configure how the panel writes (the commit
|
|
2546
|
+
// message's language), not what this repository contains.
|
|
2547
|
+
style: Object.assign({}, S.iconButton, settingsOpen ? S.iconButtonActive : null),
|
|
2548
|
+
onClick: function () { setSettingsOpen(!settingsOpen); },
|
|
2549
|
+
"aria-label": t("settings.aria"),
|
|
2550
|
+
"aria-expanded": settingsOpen === true ? "true" : "false",
|
|
2551
|
+
title: t("settings.aria"),
|
|
2552
|
+
"data-dsh-git": "settings"
|
|
2553
|
+
}, "⚙"),
|
|
2195
2554
|
h("button", {
|
|
2196
2555
|
key: "refresh",
|
|
2197
2556
|
type: "button",
|
|
@@ -2519,7 +2878,11 @@ window.__ModuleLoader__.load({
|
|
|
2519
2878
|
generateMode,
|
|
2520
2879
|
route ? route.provider : undefined,
|
|
2521
2880
|
route ? route.model : undefined,
|
|
2522
|
-
state.sessionId === null ? undefined : state.sessionId
|
|
2881
|
+
state.sessionId === null ? undefined : state.sessionId,
|
|
2882
|
+
// The forced output language from the settings popover;
|
|
2883
|
+
// undefined ("auto", or an empty custom field) lets the
|
|
2884
|
+
// model follow the repository instead.
|
|
2885
|
+
store.languageDirective()
|
|
2523
2886
|
);
|
|
2524
2887
|
}, describeGenerate).then(function (res) {
|
|
2525
2888
|
if (res && res.ok === true && res.value && res.value.message) {
|
|
@@ -2661,7 +3024,29 @@ window.__ModuleLoader__.load({
|
|
|
2661
3024
|
onMouseLeave: function () { setResizeHover(false); }
|
|
2662
3025
|
});
|
|
2663
3026
|
|
|
2664
|
-
|
|
3027
|
+
// The settings popover and its click catcher sit ABOVE the body in the
|
|
3028
|
+
// panel's own box: the backdrop closes the popover on any click that is
|
|
3029
|
+
// not on the popover itself, which is what makes dismissal work without
|
|
3030
|
+
// a document-level listener (SSR runs no effects, so a listener would
|
|
3031
|
+
// also be untestable).
|
|
3032
|
+
var settingsLayer = settingsOpen ? [
|
|
3033
|
+
h("div", {
|
|
3034
|
+
key: "settings-backdrop",
|
|
3035
|
+
style: S.settingsBackdrop,
|
|
3036
|
+
onClick: function () { setSettingsOpen(false); },
|
|
3037
|
+
"data-dsh-git": "settings-backdrop"
|
|
3038
|
+
}),
|
|
3039
|
+
settingsView({
|
|
3040
|
+
t: t,
|
|
3041
|
+
language: state.generateLanguage,
|
|
3042
|
+
customText: state.generateLanguageText,
|
|
3043
|
+
onLanguage: function (value) { store.setGenerateLanguage(value, state.generateLanguageText); },
|
|
3044
|
+
onCustomText: function (value) { store.setGenerateLanguage(LANGUAGE_CUSTOM_ID, value); },
|
|
3045
|
+
onClose: function () { setSettingsOpen(false); }
|
|
3046
|
+
})
|
|
3047
|
+
] : [];
|
|
3048
|
+
|
|
3049
|
+
return h("div", { "data-dsh-git": "panel", style: panelStyle, ref: panelRef }, [header].concat(settingsLayer, [body, resizeHandle]));
|
|
2665
3050
|
}
|
|
2666
3051
|
|
|
2667
3052
|
/**
|
|
@@ -2832,6 +3217,10 @@ window.__ModuleLoader__.load({
|
|
|
2832
3217
|
DIFF_ROW: DIFF_ROW,
|
|
2833
3218
|
stripAnsiEscapes: stripAnsiEscapes,
|
|
2834
3219
|
outputView: outputView,
|
|
3220
|
+
settingsView: settingsView,
|
|
3221
|
+
LANGUAGE_PRESETS: LANGUAGE_PRESETS,
|
|
3222
|
+
LANGUAGE_CUSTOM_ID: LANGUAGE_CUSTOM_ID,
|
|
3223
|
+
LANGUAGE_STORAGE_KEY: LANGUAGE_STORAGE_KEY,
|
|
2835
3224
|
sectionView: sectionView,
|
|
2836
3225
|
changeTagStyle: changeTagStyle,
|
|
2837
3226
|
panelBoxStyle: panelBoxStyle,
|
package/lib/index.js
CHANGED
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
* - `push` → git push (current branch's upstream).
|
|
25
25
|
* - `log` → recent commit summary lines.
|
|
26
26
|
* - `generateMessage` → draft a commit message from the working tree through
|
|
27
|
-
* the shared LLM service (see the endpoint's own docs)
|
|
27
|
+
* the shared LLM service (see the endpoint's own docs); the
|
|
28
|
+
* caller may pin the message's language with `language`.
|
|
28
29
|
*
|
|
29
30
|
* Every git run goes through execFile with a fixed argument array (no shell),
|
|
30
31
|
* a timeout, and strict input validation.
|
|
@@ -102,6 +103,20 @@ const GENERATE_MODES = ["staged", "unstaged", "all"];
|
|
|
102
103
|
const SESSION_ID_MAX_CHARS = 200;
|
|
103
104
|
/** Default mode: the only one whose content is what `commit` will actually record. */
|
|
104
105
|
const GENERATE_MODE_DEFAULT = "staged";
|
|
106
|
+
/**
|
|
107
|
+
* Longest commit-message language directive the panel may send. The setting is
|
|
108
|
+
* a language NAME ("Simplified Chinese (简体中文)"), never a sentence, so a
|
|
109
|
+
* tight cap plus {@link LANGUAGE_DIRECTIVE} keep a caller from smuggling extra
|
|
110
|
+
* prompt rules into the system message.
|
|
111
|
+
*/
|
|
112
|
+
const LANGUAGE_MAX_CHARS = 60;
|
|
113
|
+
/**
|
|
114
|
+
* Shape of an acceptable language directive: a letter or digit, then letters,
|
|
115
|
+
* digits, spaces and the punctuation real language names use. Deliberately no
|
|
116
|
+
* line feed, no control character and no `:` / `;` / quote — the characters
|
|
117
|
+
* that would let the value open a rule of its own inside the system prompt.
|
|
118
|
+
*/
|
|
119
|
+
const LANGUAGE_DIRECTIVE = /^[\p{L}\p{N}][\p{L}\p{N} .,()()·'\-/+&]*$/u;
|
|
105
120
|
|
|
106
121
|
/** A successful RPC result. */
|
|
107
122
|
function ok(value) {
|
|
@@ -207,6 +222,27 @@ function validCwd(value) {
|
|
|
207
222
|
return typeof value === "string" && value.length > 0 && isAbsolute(value);
|
|
208
223
|
}
|
|
209
224
|
|
|
225
|
+
/**
|
|
226
|
+
* Read the caller's commit-message language setting.
|
|
227
|
+
*
|
|
228
|
+
* `undefined`/absent/`"auto"` all mean "follow the repository" (the historical
|
|
229
|
+
* behaviour), so the panel can send whatever its settings field holds without
|
|
230
|
+
* the host needing a language table of its own: the value is the language NAME
|
|
231
|
+
* the model should answer in, validated only for shape and length.
|
|
232
|
+
*
|
|
233
|
+
* @param raw - the caller's `language` argument.
|
|
234
|
+
* @returns `{ ok: true, value }` with the directive (or undefined for auto), or
|
|
235
|
+
* `{ ok: false }` for anything that is not a plain language name.
|
|
236
|
+
*/
|
|
237
|
+
function readLanguage(raw) {
|
|
238
|
+
if (raw === undefined || raw === null) return { ok: true, value: undefined };
|
|
239
|
+
if (typeof raw !== "string") return { ok: false };
|
|
240
|
+
const trimmed = raw.trim();
|
|
241
|
+
if (trimmed === "" || trimmed.toLowerCase() === "auto") return { ok: true, value: undefined };
|
|
242
|
+
if (trimmed.length > LANGUAGE_MAX_CHARS || !LANGUAGE_DIRECTIVE.test(trimmed)) return { ok: false };
|
|
243
|
+
return { ok: true, value: trimmed };
|
|
244
|
+
}
|
|
245
|
+
|
|
210
246
|
/**
|
|
211
247
|
* Validate a caller-supplied remote name (origin, upstream, …).
|
|
212
248
|
* Plain segment: letters/digits/._- only.
|
|
@@ -980,19 +1016,29 @@ async function readChangesForMode(rawCwd, mode, signal) {
|
|
|
980
1016
|
}
|
|
981
1017
|
|
|
982
1018
|
/**
|
|
983
|
-
* Frame one commit-message request.
|
|
984
|
-
*
|
|
1019
|
+
* Frame one commit-message request.
|
|
1020
|
+
*
|
|
1021
|
+
* Without `language` the model is told to match the codebase rather than to
|
|
1022
|
+
* prefer either of this panel's locales. With one, that rule is REPLACED by an
|
|
1023
|
+
* explicit override: the user asked for a fixed language, and "follow the
|
|
1024
|
+
* repository" is exactly the instruction a repository whose comments and past
|
|
1025
|
+
* commits are in another language would otherwise win against.
|
|
1026
|
+
*
|
|
985
1027
|
* @param stat - diffstat text (may be empty).
|
|
986
1028
|
* @param diff - unified diff text.
|
|
1029
|
+
* @param language - language NAME the message must be written in, from
|
|
1030
|
+
* {@link readLanguage}; undefined follows the repository.
|
|
987
1031
|
*/
|
|
988
|
-
function generationPrompt(stat, diff) {
|
|
1032
|
+
function generationPrompt(stat, diff, language = undefined) {
|
|
989
1033
|
const system = [
|
|
990
1034
|
"You write one git commit message for the change set below.",
|
|
991
1035
|
"Rules:",
|
|
992
1036
|
"- Reply with the commit message only: no preamble, no quotes, no Markdown fences.",
|
|
993
1037
|
"- One imperative subject line under 72 characters; add a short body only when the change needs it.",
|
|
994
1038
|
"- Describe what changed and why, not how.",
|
|
995
|
-
|
|
1039
|
+
language === undefined
|
|
1040
|
+
? "- Write in the language already used by the codebase's comments and existing commit subjects."
|
|
1041
|
+
: `- Write the entire commit message — subject and body — in ${language}, and in no other language. The user chose this language explicitly, so it overrides the diff, the code comments and the existing commit subjects even when those are written in a different one. Do not translate the message back.`
|
|
996
1042
|
].join("\n");
|
|
997
1043
|
const body = [
|
|
998
1044
|
"## Diffstat",
|
|
@@ -1130,9 +1176,11 @@ async function requestCommitMessage(ctx, prompt, route, signal, sessionId = unde
|
|
|
1130
1176
|
* @param rawModel - caller's model id (optional).
|
|
1131
1177
|
* @param rawSessionId - caller's session id (optional; see
|
|
1132
1178
|
* {@link requestCommitMessage} for why a session-scoped caller must send it).
|
|
1179
|
+
* @param rawLanguage - language NAME the message must be written in (optional;
|
|
1180
|
+
* absent/`"auto"` follows the repository).
|
|
1133
1181
|
* @param signal - transport cancellation.
|
|
1134
1182
|
*/
|
|
1135
|
-
async function gitGenerateMessage(ctx, rawCwd, rawMode, rawProvider, rawModel, rawSessionId, signal) {
|
|
1183
|
+
async function gitGenerateMessage(ctx, rawCwd, rawMode, rawProvider, rawModel, rawSessionId, rawLanguage, signal) {
|
|
1136
1184
|
if (!validCwd(rawCwd)) return fail("invalid-cwd", "a valid absolute working directory is required");
|
|
1137
1185
|
if (rawMode !== undefined && rawMode !== null && !GENERATE_MODES.includes(rawMode)) {
|
|
1138
1186
|
return fail("invalid-mode", `mode must be one of ${GENERATE_MODES.join(", ")}`);
|
|
@@ -1141,6 +1189,10 @@ async function gitGenerateMessage(ctx, rawCwd, rawMode, rawProvider, rawModel, r
|
|
|
1141
1189
|
&& (typeof rawSessionId !== "string" || rawSessionId === "" || rawSessionId.length > SESSION_ID_MAX_CHARS)) {
|
|
1142
1190
|
return fail("invalid-session", "sessionId must be a non-empty string when present");
|
|
1143
1191
|
}
|
|
1192
|
+
const language = readLanguage(rawLanguage);
|
|
1193
|
+
if (language.ok !== true) {
|
|
1194
|
+
return fail("invalid-language", `language must be a plain language name of at most ${LANGUAGE_MAX_CHARS} characters`);
|
|
1195
|
+
}
|
|
1144
1196
|
const sessionId = typeof rawSessionId === "string" ? rawSessionId : undefined;
|
|
1145
1197
|
const mode = rawMode ?? GENERATE_MODE_DEFAULT;
|
|
1146
1198
|
const changes = await readChangesForMode(rawCwd, mode, signal);
|
|
@@ -1157,7 +1209,8 @@ async function gitGenerateMessage(ctx, rawCwd, rawMode, rawProvider, rawModel, r
|
|
|
1157
1209
|
}
|
|
1158
1210
|
const prompt = generationPrompt(
|
|
1159
1211
|
clampForModel(changes.value.stat, LLM_STAT_MAX_CHARS),
|
|
1160
|
-
clampForModel(changes.value.diff, LLM_DIFF_MAX_CHARS)
|
|
1212
|
+
clampForModel(changes.value.diff, LLM_DIFF_MAX_CHARS),
|
|
1213
|
+
language.value
|
|
1161
1214
|
);
|
|
1162
1215
|
// Bound the call even if the browser goes away without hanging up; the
|
|
1163
1216
|
// turn's own signal still cancels immediately.
|
|
@@ -1220,7 +1273,7 @@ async function dispatch(ctx, endpoint, payload, signal) {
|
|
|
1220
1273
|
case "log":
|
|
1221
1274
|
return gitLog(args.cwd, args.count, signal);
|
|
1222
1275
|
case "generateMessage":
|
|
1223
|
-
return gitGenerateMessage(ctx, args.cwd, args.mode, args.provider, args.model, args.sessionId, signal);
|
|
1276
|
+
return gitGenerateMessage(ctx, args.cwd, args.mode, args.provider, args.model, args.sessionId, args.language, signal);
|
|
1224
1277
|
default:
|
|
1225
1278
|
return fail("unknown-endpoint", `unknown git endpoint ${JSON.stringify(endpoint)}`);
|
|
1226
1279
|
}
|
|
@@ -1332,4 +1385,4 @@ function apply(ctx) {
|
|
|
1332
1385
|
// diff through git first, so `generateMessage` needs a real repository to reach
|
|
1333
1386
|
// them) and the commit-message normalizer (pure, and the exact place the
|
|
1334
1387
|
// subject+body regression lived).
|
|
1335
|
-
export { apply, clampForModel, generationPrompt, inject, name, normalizeCommitMessage, requestCommitMessage, resolveLlmRoute };
|
|
1388
|
+
export { apply, clampForModel, generationPrompt, inject, name, normalizeCommitMessage, readLanguage, requestCommitMessage, resolveLlmRoute };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xia-sc/dsh-git",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
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",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"test": "node test/smoke.mjs && node test/host-mount.mjs && node test/slot-mount.mjs && node test/generate.mjs && node test/render.mjs",
|
|
43
43
|
"test:commit": "node test/commit.mjs",
|
|
44
44
|
"test:diff": "node test/diff.mjs",
|
|
45
|
+
"test:ui:settings": "node test/ui/verify-settings.mjs",
|
|
45
46
|
"prepublishOnly": "npm test"
|
|
46
47
|
},
|
|
47
48
|
"dsh": {
|