@xia-sc/dsh-git 0.6.0 → 0.6.1
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 +8 -3
- package/README.md +6 -3
- package/lib/client.js +90 -26
- package/lib/index.js +63 -20
- package/package.json +6 -3
package/README.en.md
CHANGED
|
@@ -122,8 +122,13 @@ The commit area follows the real order of operations: **stage all → draft →
|
|
|
122
122
|
|
|
123
123
|
The model route comes from the current session's `modelSelection` projection
|
|
124
124
|
(pending pick first, then last used), falling back to the host's first
|
|
125
|
-
registered route.
|
|
126
|
-
|
|
125
|
+
registered route. The request carries the **current session id** (`sessionId`):
|
|
126
|
+
some gateways (the opencode-style routes on this machine, for one) require a
|
|
127
|
+
session-affinity header, and the host forwards a session id to its adapter only
|
|
128
|
+
when the request has one — without it the gateway answers `MissingSessionID`.
|
|
129
|
+
Failures are reported in the last-operation output with a localized sentence
|
|
130
|
+
(no changes / no model configured / draft failed, …); an unrecognized failure
|
|
131
|
+
code leads with the localized sentence and appends the host's own diagnosis.
|
|
127
132
|
|
|
128
133
|
## Architecture
|
|
129
134
|
|
|
@@ -232,7 +237,7 @@ shell metacharacters, and leading dashes are all recorded verbatim.
|
|
|
232
237
|
| `commit` | `{ cwd, message }` | `{ message }`; `missing-author` error when `user.name/email` unset |
|
|
233
238
|
| `push` | `{ cwd }` | `{ message }` (120s timeout) |
|
|
234
239
|
| `log` | `{ cwd, count? }` | `{ repo, commits: [{sha, author, subject, refs}] }` (clamped 1..50) |
|
|
235
|
-
| `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`. |
|
|
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`. |
|
|
236
241
|
|
|
237
242
|
> A failed result carries `error.code === "internal"` on the wire (the Connection
|
|
238
243
|
> envelope only requires a string), with the plugin's own diagnostic in
|
package/README.md
CHANGED
|
@@ -85,8 +85,11 @@ DeepSeek Harness Web GUI 的完整 Git 管理插件,形态为一个**可折叠
|
|
|
85
85
|
模型,生成的提交信息直接填入输入框;不满意可改,也可以直接手写。
|
|
86
86
|
|
|
87
87
|
生成用的模型路由取当前会话的 `modelSelection` 投影(待生效的选择优先,
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
其次是上次实际使用),取不到时回落到宿主注册的第一条路由。请求会带上**当前会话 id**
|
|
89
|
+
(`sessionId`),因为部分网关(例如本机配置的 opencode 系路由)要求会话亲和头,
|
|
90
|
+
而宿主只在请求带 `sessionId` 时才把它转给适配器——不带就会被网关以
|
|
91
|
+
`MissingSessionID` 拒掉。失败会以本地化文案显示在"上次操作输出"里
|
|
92
|
+
(无可用改动 / 未配置模型 / 生成失败等;未知失败码先给本地化短句,再附宿主的原始诊断)。
|
|
90
93
|
|
|
91
94
|
## 架构
|
|
92
95
|
|
|
@@ -180,7 +183,7 @@ dsh plugin --profile web remove @xia-sc/dsh-git
|
|
|
180
183
|
| `commit` | `{ cwd, message }` | `{ message }`;未配置 `user.name/email` 时报 `missing-author` 错误 |
|
|
181
184
|
| `push` | `{ cwd }` | `{ message }`(120s 超时) |
|
|
182
185
|
| `log` | `{ cwd, count? }` | `{ repo, commits: [{sha, author, subject, refs}] }`(钳制 1..50) |
|
|
183
|
-
| `generateMessage` | `{ cwd, mode?, provider?, model? }` | `{ message, mode, provider, model }`。`mode` 为 `staged`(默认)/`unstaged`/`all`,非法值报 `invalid-mode
|
|
186
|
+
| `generateMessage` | `{ cwd, mode?, provider?, model?, sessionId? }` | `{ message, mode, provider, model }`。`mode` 为 `staged`(默认)/`unstaged`/`all`,非法值报 `invalid-mode`;`sessionId` 为可选的非空字符串(超过 200 字符报 `invalid-session`),面板会带上当前会话 id 供需要会话亲和的网关路由。失败码见 `error.details.code`:`no-changes`、`no-provider`、`no-model`、`llm-truncated`(输出上限用尽、一个字都没写出来)、`llm-empty`、`cancelled`、`llm-failed`。 |
|
|
184
187
|
|
|
185
188
|
> 失败结果的 `error.code` 在线路上固定为 `"internal"`(Connection 信封只要求它是字符串),
|
|
186
189
|
> 插件自己的诊断码放在 `error.details.code`;客户端按该码做本地化文案。
|
package/lib/client.js
CHANGED
|
@@ -76,6 +76,8 @@ window.__ModuleLoader__.load({
|
|
|
76
76
|
"generate.truncated": "模型还没写出提交信息就用完了 token 上限(思考模型会先花掉这份预算),再点一次通常即可",
|
|
77
77
|
"generate.cancelled": "生成已取消",
|
|
78
78
|
"generate.failed": "生成失败",
|
|
79
|
+
"generate.failedDetail": "生成失败:{message}",
|
|
80
|
+
"generate.failedRoute": "生成失败:{message}(模型路由 {provider} / {model})",
|
|
79
81
|
"commit.placeholder": "提交信息(可多行,Ctrl+Enter 提交)…",
|
|
80
82
|
"commit.empty": "请输入提交信息",
|
|
81
83
|
"changes.title": "变更",
|
|
@@ -180,6 +182,8 @@ window.__ModuleLoader__.load({
|
|
|
180
182
|
"generate.truncated": "The model hit its token cap before writing anything (a reasoning model spends that budget thinking first) — trying again usually works",
|
|
181
183
|
"generate.cancelled": "Draft cancelled",
|
|
182
184
|
"generate.failed": "Draft failed",
|
|
185
|
+
"generate.failedDetail": "Draft failed: {message}",
|
|
186
|
+
"generate.failedRoute": "Draft failed: {message} (route {provider} / {model})",
|
|
183
187
|
"commit.placeholder": "Commit message (multi-line allowed, Ctrl+Enter commits)…",
|
|
184
188
|
"commit.empty": "Enter a commit message",
|
|
185
189
|
"changes.title": "Changes",
|
|
@@ -251,7 +255,12 @@ window.__ModuleLoader__.load({
|
|
|
251
255
|
panel: {
|
|
252
256
|
position: "fixed",
|
|
253
257
|
// Sit above the composer card so the expanded panel never covers
|
|
254
|
-
// the input box
|
|
258
|
+
// the input box. 168px is a measurement of the composer stack
|
|
259
|
+
// (card + tool row + the dock band), not a published contract:
|
|
260
|
+
// ui-conversation keeps that geometry in its own module CSS
|
|
261
|
+
// (`--dsh-composer-stack-gap` etc.). If the composer grows or
|
|
262
|
+
// shrinks, re-measure here and in DIFF_PANEL_HEIGHT below — the
|
|
263
|
+
// panel is anchored by `bottom`, so both move together.
|
|
255
264
|
bottom: 168,
|
|
256
265
|
left: "50%",
|
|
257
266
|
transform: "translateX(-50%)",
|
|
@@ -878,6 +887,33 @@ window.__ModuleLoader__.load({
|
|
|
878
887
|
return text.length > max ? text.slice(0, max - 1) + "…" : text;
|
|
879
888
|
}
|
|
880
889
|
|
|
890
|
+
/**
|
|
891
|
+
* The model route one session's AI draft should use, or null to let the
|
|
892
|
+
* host fall back to its first registered provider route.
|
|
893
|
+
*
|
|
894
|
+
* Two host generations carry the same `modelSelection` projection: the
|
|
895
|
+
* row's own `projectionValues` (present since 0.1.6) and, since 0.1.7,
|
|
896
|
+
* the snapshot's per-session `projectionsBySession[id].values`. Read the
|
|
897
|
+
* row first, then the shared record, so neither generation loses a route
|
|
898
|
+
* the user already chose. `next` (a pending pick) beats `lastUsed`.
|
|
899
|
+
*
|
|
900
|
+
* @param summary - `byId[sessionId]` row, when the snapshot has one.
|
|
901
|
+
* @param sessions - the whole `useSessions` snapshot (may be undefined).
|
|
902
|
+
* @param sessionId - the bound session id, or null/undefined.
|
|
903
|
+
* @returns `{ provider, model }`, or null.
|
|
904
|
+
*/
|
|
905
|
+
function sessionModelRoute(summary, sessions, sessionId) {
|
|
906
|
+
var selection = summary && summary.projectionValues ? summary.projectionValues.modelSelection : undefined;
|
|
907
|
+
if (!selection && sessionId !== null && sessionId !== undefined && sessions && sessions.projectionsBySession) {
|
|
908
|
+
var record = sessions.projectionsBySession[sessionId];
|
|
909
|
+
selection = record && record.values ? record.values.modelSelection : undefined;
|
|
910
|
+
}
|
|
911
|
+
if (!selection) return null;
|
|
912
|
+
var effective = selection.next || selection.lastUsed;
|
|
913
|
+
if (!effective || !effective.provider || !effective.model) return null;
|
|
914
|
+
return { provider: effective.provider, model: effective.model };
|
|
915
|
+
}
|
|
916
|
+
|
|
881
917
|
/**
|
|
882
918
|
* Strip ANSI/VT escape sequences from git's human-readable output.
|
|
883
919
|
*
|
|
@@ -1100,8 +1136,11 @@ window.__ModuleLoader__.load({
|
|
|
1100
1136
|
commit: function (cwd, message) { return rpc(ctx, "commit", { cwd: cwd, message: message }); },
|
|
1101
1137
|
push: function (cwd) { return rpc(ctx, "push", { cwd: cwd }); },
|
|
1102
1138
|
log: function (cwd) { return rpc(ctx, "log", { cwd: cwd, count: 10 }); },
|
|
1103
|
-
generateMessage: function (cwd, mode, provider, model) {
|
|
1104
|
-
|
|
1139
|
+
generateMessage: function (cwd, mode, provider, model, sessionId) {
|
|
1140
|
+
// The session travels with the request: session-labelled
|
|
1141
|
+
// providers reject an unidentified call, and the host forwards
|
|
1142
|
+
// a session id to its adapter only when one is present.
|
|
1143
|
+
return rpc(ctx, "generateMessage", { cwd: cwd, mode: mode, provider: provider, model: model, sessionId: sessionId });
|
|
1105
1144
|
}
|
|
1106
1145
|
};
|
|
1107
1146
|
|
|
@@ -1800,10 +1839,14 @@ window.__ModuleLoader__.load({
|
|
|
1800
1839
|
// with "selector is not a function".
|
|
1801
1840
|
var sessions = useSessions(function (s) { return s; });
|
|
1802
1841
|
|
|
1803
|
-
// NOTE:
|
|
1804
|
-
//
|
|
1805
|
-
//
|
|
1806
|
-
//
|
|
1842
|
+
// NOTE: subscribe through the classic useState + useEffect pattern
|
|
1843
|
+
// instead of React.useSyncExternalStore, so this store's own
|
|
1844
|
+
// subscription is independent of whichever implementation the seed
|
|
1845
|
+
// ships. This is about keeping one dependency out of the path, not a
|
|
1846
|
+
// claim that the seeded build is broken: the framework's own
|
|
1847
|
+
// `useSessions` prop already goes through the renderer's
|
|
1848
|
+
// useSyncExternalStoreWithSelector, so an unusable seed would break
|
|
1849
|
+
// the session list before it broke this panel.
|
|
1807
1850
|
var stateRef = React.useState(function () { return store.getSnapshot(); });
|
|
1808
1851
|
var state = stateRef[0];
|
|
1809
1852
|
var setState = stateRef[1];
|
|
@@ -1824,16 +1867,10 @@ window.__ModuleLoader__.load({
|
|
|
1824
1867
|
var sessionId = state.sessionId;
|
|
1825
1868
|
var currentSummary = sessionId !== null && sessions && sessions.byId ? sessions.byId[sessionId] : undefined;
|
|
1826
1869
|
|
|
1827
|
-
// The session's own model route, when one is recorded
|
|
1828
|
-
//
|
|
1829
|
-
//
|
|
1830
|
-
var sessionRoute = (
|
|
1831
|
-
var selection = currentSummary && currentSummary.projectionValues ? currentSummary.projectionValues.modelSelection : undefined;
|
|
1832
|
-
if (!selection) return null;
|
|
1833
|
-
var effective = selection.next || selection.lastUsed;
|
|
1834
|
-
if (!effective || !effective.provider || !effective.model) return null;
|
|
1835
|
-
return { provider: effective.provider, model: effective.model };
|
|
1836
|
-
})();
|
|
1870
|
+
// The session's own model route, when one is recorded. Both projection
|
|
1871
|
+
// surfaces it can come from are handled by sessionModelRoute; a
|
|
1872
|
+
// missing route leaves generation to the host's first registered one.
|
|
1873
|
+
var sessionRoute = sessionModelRoute(currentSummary, sessions, sessionId);
|
|
1837
1874
|
|
|
1838
1875
|
var messageRef = React.useState("");
|
|
1839
1876
|
var message = messageRef[0];
|
|
@@ -2449,7 +2486,26 @@ window.__ModuleLoader__.load({
|
|
|
2449
2486
|
};
|
|
2450
2487
|
var text = known[details.code];
|
|
2451
2488
|
if (text === undefined) {
|
|
2452
|
-
|
|
2489
|
+
// Unknown code (a transport failure, a provider rejection,
|
|
2490
|
+
// …): lead with the localized sentence, keep the host's
|
|
2491
|
+
// own words as the diagnosis, and name the route that
|
|
2492
|
+
// failed — "which provider is broken" is the actionable
|
|
2493
|
+
// part when a gateway refuses the call.
|
|
2494
|
+
var detail = res && res.error && res.error.message ? String(res.error.message) : "";
|
|
2495
|
+
var failedRoute = details.provider && details.model
|
|
2496
|
+
? { provider: details.provider, model: details.model }
|
|
2497
|
+
: null;
|
|
2498
|
+
if (detail === "") {
|
|
2499
|
+
text = t("generate.failed");
|
|
2500
|
+
} else if (failedRoute !== null) {
|
|
2501
|
+
text = t("generate.failedRoute", {
|
|
2502
|
+
message: shortText(detail, 160),
|
|
2503
|
+
provider: failedRoute.provider,
|
|
2504
|
+
model: failedRoute.model
|
|
2505
|
+
});
|
|
2506
|
+
} else {
|
|
2507
|
+
text = t("generate.failedDetail", { message: shortText(detail, 160) });
|
|
2508
|
+
}
|
|
2453
2509
|
}
|
|
2454
2510
|
return { kind: "error", text: text };
|
|
2455
2511
|
}
|
|
@@ -2462,7 +2518,8 @@ window.__ModuleLoader__.load({
|
|
|
2462
2518
|
state.cwd,
|
|
2463
2519
|
generateMode,
|
|
2464
2520
|
route ? route.provider : undefined,
|
|
2465
|
-
route ? route.model : undefined
|
|
2521
|
+
route ? route.model : undefined,
|
|
2522
|
+
state.sessionId === null ? undefined : state.sessionId
|
|
2466
2523
|
);
|
|
2467
2524
|
}, describeGenerate).then(function (res) {
|
|
2468
2525
|
if (res && res.ok === true && res.value && res.value.message) {
|
|
@@ -2641,7 +2698,8 @@ window.__ModuleLoader__.load({
|
|
|
2641
2698
|
? true
|
|
2642
2699
|
: (summary.retainedBy.mainView || 0) > 0;
|
|
2643
2700
|
|
|
2644
|
-
// Classic subscription pattern (see GitFloatingPanel
|
|
2701
|
+
// Classic subscription pattern (see GitFloatingPanel's note: keep this
|
|
2702
|
+
// store independent of the seed's useSyncExternalStore build).
|
|
2645
2703
|
var stateRef = React.useState(function () { return store.getSnapshot(); });
|
|
2646
2704
|
var state = stateRef[0];
|
|
2647
2705
|
var setState = stateRef[1];
|
|
@@ -2677,17 +2735,22 @@ window.__ModuleLoader__.load({
|
|
|
2677
2735
|
}
|
|
2678
2736
|
|
|
2679
2737
|
// Left-aligned compact pill in the row band ABOVE the composer card
|
|
2680
|
-
// (conversation.input.dock). The
|
|
2681
|
-
// card's
|
|
2682
|
-
//
|
|
2738
|
+
// (conversation.input.dock). The row box is capped at the composer
|
|
2739
|
+
// card's own width and centered — the same way ui-conversation's own
|
|
2740
|
+
// dock occupant (QueueDock) aligns itself — so the pill hugs the
|
|
2741
|
+
// textarea's top-left corner instead of floating at the conversation
|
|
2742
|
+
// column's edge. `--dsh-composer-card-max-width` is ui-conversation's
|
|
2743
|
+
// internal variable (no published contract, and 0.1.7 added a second
|
|
2744
|
+
// value for the embedded body); the 778px fallback keeps this row sane
|
|
2745
|
+
// if it is ever renamed.
|
|
2683
2746
|
return h("div", {
|
|
2684
2747
|
style: {
|
|
2685
2748
|
boxSizing: "border-box",
|
|
2686
2749
|
display: "flex",
|
|
2687
2750
|
justifyContent: "flex-start",
|
|
2688
2751
|
width: "100%",
|
|
2689
|
-
|
|
2690
|
-
margin: "2px
|
|
2752
|
+
maxWidth: "var(--dsh-composer-card-max-width, 778px)",
|
|
2753
|
+
margin: "2px auto"
|
|
2691
2754
|
},
|
|
2692
2755
|
"data-dsh-git": "dock-row"
|
|
2693
2756
|
}, h("button", {
|
|
@@ -2772,7 +2835,8 @@ window.__ModuleLoader__.load({
|
|
|
2772
2835
|
sectionView: sectionView,
|
|
2773
2836
|
changeTagStyle: changeTagStyle,
|
|
2774
2837
|
panelBoxStyle: panelBoxStyle,
|
|
2775
|
-
DIFF_PANEL_HEIGHT: DIFF_PANEL_HEIGHT
|
|
2838
|
+
DIFF_PANEL_HEIGHT: DIFF_PANEL_HEIGHT,
|
|
2839
|
+
sessionModelRoute: sessionModelRoute
|
|
2776
2840
|
};
|
|
2777
2841
|
return module.exports;
|
|
2778
2842
|
}
|
package/lib/index.js
CHANGED
|
@@ -49,7 +49,6 @@
|
|
|
49
49
|
* `/api` transport — loopback or a configured trusted authority, same-origin.
|
|
50
50
|
*/
|
|
51
51
|
import { execFile } from "node:child_process";
|
|
52
|
-
import { randomUUID } from "node:crypto";
|
|
53
52
|
import { isAbsolute } from "node:path";
|
|
54
53
|
|
|
55
54
|
/** Stable Cordis plugin name. */
|
|
@@ -95,6 +94,12 @@ const LLM_STAT_MAX_CHARS = 2000;
|
|
|
95
94
|
const LLM_MAX_TOKENS = 8192;
|
|
96
95
|
/** Accepted `generateMessage` modes. */
|
|
97
96
|
const GENERATE_MODES = ["staged", "unstaged", "all"];
|
|
97
|
+
/**
|
|
98
|
+
* Longest session id this plugin will forward to the LLM service. Real ids are
|
|
99
|
+
* short branded strings; the cap only keeps a hostile caller from stuffing
|
|
100
|
+
* arbitrary text into a request header.
|
|
101
|
+
*/
|
|
102
|
+
const SESSION_ID_MAX_CHARS = 200;
|
|
98
103
|
/** Default mode: the only one whose content is what `commit` will actually record. */
|
|
99
104
|
const GENERATE_MODE_DEFAULT = "staged";
|
|
100
105
|
|
|
@@ -682,8 +687,10 @@ function endpointFromPath(pathname) {
|
|
|
682
687
|
|
|
683
688
|
/**
|
|
684
689
|
* Read a request body with a hard byte cap.
|
|
685
|
-
*
|
|
686
|
-
*
|
|
690
|
+
* Deliberately uses the classic data/end/error events rather than async
|
|
691
|
+
* iteration: the host's own bridge does `for await (const chunk of req)`, and
|
|
692
|
+
* keeping this reader on the event API means one dependency fewer on an
|
|
693
|
+
* IncomingMessage behaviour this plugin cannot pin.
|
|
687
694
|
* @param req - node:http request.
|
|
688
695
|
* @param maxBytes - cap; exceeding it resolves to undefined.
|
|
689
696
|
* @returns the utf8 body, or undefined on overrun/error/abort.
|
|
@@ -1038,17 +1045,21 @@ async function resolveLlmRoute(ctx, rawProvider, rawModel) {
|
|
|
1038
1045
|
* Hand-built rather than imported from `@deepseek-ai/dsh-llm`: this package
|
|
1039
1046
|
* deliberately declares no `@deepseek-ai/*` runtime imports, because a plugin
|
|
1040
1047
|
* installed with pnpm `link:` resolves them from its own real source path,
|
|
1041
|
-
* where no host tree exists.
|
|
1042
|
-
*
|
|
1048
|
+
* where no host tree exists.
|
|
1049
|
+
*
|
|
1050
|
+
* The shape is the host's documented hand-built one-shot input
|
|
1051
|
+
* (`RequestUserInput`): the `user` role plus one text block, and deliberately
|
|
1052
|
+
* **no `id` and no `source`**. dsh >= 0.1.7 retired the catch-all `plugin`
|
|
1053
|
+
* source kind — `MessageSourceMap` has no such member ("each producer declares
|
|
1054
|
+
* its own `kind` in its own module; there is no shared catch-all `plugin`
|
|
1055
|
+
* kind", `dsh-llm/lib/types/message.d.ts`), and Session format v4 refuses
|
|
1056
|
+
* `{kind:"plugin"}` outright (`dsh-session-format-v3-to-v4`). A one-shot
|
|
1057
|
+
* request needs no durable identity: only `provider`/`model`/`messages`/… are
|
|
1058
|
+
* read off {@link GenerateOptions}.
|
|
1043
1059
|
* @param text - model-facing prompt body.
|
|
1044
1060
|
*/
|
|
1045
1061
|
function generationMessage(text) {
|
|
1046
|
-
return
|
|
1047
|
-
id: randomUUID(),
|
|
1048
|
-
role: "user",
|
|
1049
|
-
content: Object.freeze([Object.freeze({ type: "text", text })]),
|
|
1050
|
-
source: Object.freeze({ kind: "plugin", plugin: name })
|
|
1051
|
-
});
|
|
1062
|
+
return { role: "user", content: [{ type: "text", text }] };
|
|
1052
1063
|
}
|
|
1053
1064
|
|
|
1054
1065
|
/**
|
|
@@ -1057,18 +1068,27 @@ function generationMessage(text) {
|
|
|
1057
1068
|
* @param prompt - `{ system, body }` from {@link generationPrompt}.
|
|
1058
1069
|
* @param route - `{ provider, model }` from {@link resolveLlmRoute}.
|
|
1059
1070
|
* @param signal - cancellation (browser abort plus the server ceiling).
|
|
1071
|
+
* @param sessionId - the caller's session, when it has one. Forwarded as
|
|
1072
|
+
* `GenerateOptions.sessionId`; adapters that need session affinity (they
|
|
1073
|
+
* receive it only when it is present — see `dsh-llm-pi-ai`'s options mapping)
|
|
1074
|
+
* reject an unidentified request outright, so an AI draft from the panel must
|
|
1075
|
+
* carry one.
|
|
1060
1076
|
* @returns the trimmed message.
|
|
1061
1077
|
* @throws on a terminal stream failure, an output cap hit before any text
|
|
1062
1078
|
* existed (`llm-truncated`), or genuinely empty output (`llm-empty`).
|
|
1063
1079
|
*/
|
|
1064
|
-
async function requestCommitMessage(ctx, prompt, route, signal) {
|
|
1080
|
+
async function requestCommitMessage(ctx, prompt, route, signal, sessionId = undefined) {
|
|
1065
1081
|
const options = {
|
|
1066
1082
|
provider: route.provider,
|
|
1067
1083
|
model: route.model,
|
|
1068
1084
|
messages: [generationMessage(prompt.body)],
|
|
1069
1085
|
system: prompt.system,
|
|
1070
1086
|
maxTokens: LLM_MAX_TOKENS,
|
|
1071
|
-
signal
|
|
1087
|
+
signal,
|
|
1088
|
+
// Session-labelled routes (an "opencode go"-style gateway is the local
|
|
1089
|
+
// example) answer 400 when the request carries no session identity, and
|
|
1090
|
+
// the host's pi-ai adapter forwards this field only when it is present.
|
|
1091
|
+
...(sessionId === undefined ? {} : { sessionId })
|
|
1072
1092
|
};
|
|
1073
1093
|
// Final text comes from the assembled blocks; the delta map is a fallback for
|
|
1074
1094
|
// an adapter that emits text without a closing `block-end`.
|
|
@@ -1108,13 +1128,20 @@ async function requestCommitMessage(ctx, prompt, route, signal) {
|
|
|
1108
1128
|
* @param rawMode - `staged`, `unstaged`, or `all`.
|
|
1109
1129
|
* @param rawProvider - caller's provider route (optional).
|
|
1110
1130
|
* @param rawModel - caller's model id (optional).
|
|
1131
|
+
* @param rawSessionId - caller's session id (optional; see
|
|
1132
|
+
* {@link requestCommitMessage} for why a session-scoped caller must send it).
|
|
1111
1133
|
* @param signal - transport cancellation.
|
|
1112
1134
|
*/
|
|
1113
|
-
async function gitGenerateMessage(ctx, rawCwd, rawMode, rawProvider, rawModel, signal) {
|
|
1135
|
+
async function gitGenerateMessage(ctx, rawCwd, rawMode, rawProvider, rawModel, rawSessionId, signal) {
|
|
1114
1136
|
if (!validCwd(rawCwd)) return fail("invalid-cwd", "a valid absolute working directory is required");
|
|
1115
1137
|
if (rawMode !== undefined && rawMode !== null && !GENERATE_MODES.includes(rawMode)) {
|
|
1116
1138
|
return fail("invalid-mode", `mode must be one of ${GENERATE_MODES.join(", ")}`);
|
|
1117
1139
|
}
|
|
1140
|
+
if (rawSessionId !== undefined && rawSessionId !== null
|
|
1141
|
+
&& (typeof rawSessionId !== "string" || rawSessionId === "" || rawSessionId.length > SESSION_ID_MAX_CHARS)) {
|
|
1142
|
+
return fail("invalid-session", "sessionId must be a non-empty string when present");
|
|
1143
|
+
}
|
|
1144
|
+
const sessionId = typeof rawSessionId === "string" ? rawSessionId : undefined;
|
|
1118
1145
|
const mode = rawMode ?? GENERATE_MODE_DEFAULT;
|
|
1119
1146
|
const changes = await readChangesForMode(rawCwd, mode, signal);
|
|
1120
1147
|
if (changes.ok !== true) return changes;
|
|
@@ -1138,10 +1165,16 @@ async function gitGenerateMessage(ctx, rawCwd, rawMode, rawProvider, rawModel, s
|
|
|
1138
1165
|
const combined = signal === undefined ? timeout : AbortSignal.any([signal, timeout]);
|
|
1139
1166
|
if (combined.aborted) return fail("cancelled", "generation was cancelled", { mode });
|
|
1140
1167
|
try {
|
|
1141
|
-
const message = await requestCommitMessage(ctx, prompt, route, combined);
|
|
1168
|
+
const message = await requestCommitMessage(ctx, prompt, route, combined, sessionId);
|
|
1142
1169
|
return ok({ message, mode, provider: route.provider, model: route.model });
|
|
1143
1170
|
} catch (error) {
|
|
1144
|
-
|
|
1171
|
+
// The route rides along so the panel can name the provider that failed:
|
|
1172
|
+
// a route-level gateway rejection otherwise reads as a plugin bug.
|
|
1173
|
+
return fail(error.pluginCode ?? "llm-failed", error instanceof Error ? error.message : String(error), {
|
|
1174
|
+
mode,
|
|
1175
|
+
provider: route.provider,
|
|
1176
|
+
model: route.model
|
|
1177
|
+
});
|
|
1145
1178
|
}
|
|
1146
1179
|
}
|
|
1147
1180
|
|
|
@@ -1187,7 +1220,7 @@ async function dispatch(ctx, endpoint, payload, signal) {
|
|
|
1187
1220
|
case "log":
|
|
1188
1221
|
return gitLog(args.cwd, args.count, signal);
|
|
1189
1222
|
case "generateMessage":
|
|
1190
|
-
return gitGenerateMessage(ctx, args.cwd, args.mode, args.provider, args.model, signal);
|
|
1223
|
+
return gitGenerateMessage(ctx, args.cwd, args.mode, args.provider, args.model, args.sessionId, signal);
|
|
1191
1224
|
default:
|
|
1192
1225
|
return fail("unknown-endpoint", `unknown git endpoint ${JSON.stringify(endpoint)}`);
|
|
1193
1226
|
}
|
|
@@ -1203,8 +1236,10 @@ function apply(ctx) {
|
|
|
1203
1236
|
// The channel runs git against caller-supplied absolute paths, so it must
|
|
1204
1237
|
// never mount unfenced: the connection service's Host/Origin + browser-cookie
|
|
1205
1238
|
// check is the only gate. `requestRejection` is the >= 0.1.5-rc.1 form of that
|
|
1206
|
-
// check
|
|
1207
|
-
//
|
|
1239
|
+
// check. An older host without it still gets no channel (this throws), but
|
|
1240
|
+
// note the throw is *stderr-only* for the operator: dsh-app-boot treats
|
|
1241
|
+
// non-required rows as optional, so this row is skipped with a warning rather
|
|
1242
|
+
// than failing the boot. Never downgrade the guard to a pass-through.
|
|
1208
1243
|
const connection = ctx.get("connection");
|
|
1209
1244
|
if (connection === undefined || typeof connection.requestRejection !== "function") {
|
|
1210
1245
|
throw new Error(`${name}: this plugin requires dsh >= 0.1.5-rc.1 (connection.requestRejection is unavailable, so /dsh-git-rpc could not be fenced)`);
|
|
@@ -1213,7 +1248,15 @@ function apply(ctx) {
|
|
|
1213
1248
|
kind: "prefix",
|
|
1214
1249
|
path: RPC_CHANNEL,
|
|
1215
1250
|
handler: async (req, res) => {
|
|
1216
|
-
|
|
1251
|
+
// The host's own router matches on `new URL(req.url, "http://x").pathname`,
|
|
1252
|
+
// so an absolute-form request target (a proxy in front of dsh web) must
|
|
1253
|
+
// resolve here too. Origin-form targets are kept verbatim: `endpointFromPath`
|
|
1254
|
+
// is the stricter reader (it rejects `.`/`..`/empty segments and anything
|
|
1255
|
+
// outside the client's own segment pattern), and routing the raw target
|
|
1256
|
+
// through the URL parser first would silently normalize a traversal
|
|
1257
|
+
// attempt into a different endpoint instead of refusing it.
|
|
1258
|
+
const rawTarget = String(req.url ?? "/");
|
|
1259
|
+
const pathname = rawTarget.startsWith("/") ? rawTarget : new URL(rawTarget, "http://x").pathname;
|
|
1217
1260
|
const endpoint = endpointFromPath(pathname);
|
|
1218
1261
|
if (endpoint === undefined) {
|
|
1219
1262
|
sendEnvelope(res, 404, rpcError("invalid-request", "not-found", "not found", {}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xia-sc/dsh-git",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.1",
|
|
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",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"access": "public"
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
|
-
"test": "node test/smoke.mjs && node test/host-mount.mjs && node test/generate.mjs && node test/render.mjs",
|
|
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
45
|
"prepublishOnly": "npm test"
|
|
@@ -51,9 +51,12 @@
|
|
|
51
51
|
"client": {
|
|
52
52
|
"platform": "web",
|
|
53
53
|
"inject": [
|
|
54
|
+
"@deepseek-ai/dsh-client-connection",
|
|
54
55
|
"@deepseek-ai/dsh-client-locale",
|
|
55
56
|
"@deepseek-ai/dsh-client-ui-conversation",
|
|
56
|
-
"@deepseek-ai/dsh-client-ui-layout"
|
|
57
|
+
"@deepseek-ai/dsh-client-ui-layout",
|
|
58
|
+
"@deepseek-ai/dsh-client-ui-renderer",
|
|
59
|
+
"@deepseek-ai/dsh-client-ui-session"
|
|
57
60
|
]
|
|
58
61
|
}
|
|
59
62
|
},
|