@xbzbing/dsh-git-panel 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/README_EN.md +5 -2
- package/lib/client.js +100 -11
- package/lib/host/core.d.ts +19 -0
- package/lib/host/index.d.ts +6 -2
- package/lib/host/index.js +248 -9
- package/lib/host/llm-face.d.ts +27 -0
- package/lib/host/suggest.d.ts +4 -0
- package/lib/host/types.d.ts +31 -1
- package/package.json +2 -2
package/lib/host/core.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { GitRunner } from './git.ts';
|
|
2
|
+
import type { AgentDefaultModelFace, LlmFace } from './llm-face.ts';
|
|
2
3
|
import type { DiffViewMode, GitChange, GitErrorCode, GitSnapshotResult } from './types.ts';
|
|
3
4
|
export interface GitPanelConfig {
|
|
4
5
|
readonly timeoutMs: number;
|
|
@@ -10,6 +11,15 @@ export interface GitPanelConfig {
|
|
|
10
11
|
readonly showInputPill: boolean;
|
|
11
12
|
/** Default diff layout the views open with (user can switch per-diff). */
|
|
12
13
|
readonly defaultDiffView: DiffViewMode;
|
|
14
|
+
/** Whether the "AI suggest" button shows in the commit box. */
|
|
15
|
+
readonly suggestEnabled: boolean;
|
|
16
|
+
/** Max diff bytes sent to the model; beyond this the diff is truncated. */
|
|
17
|
+
readonly suggestMaxBytes: number;
|
|
18
|
+
readonly suggestMaxOutputTokens: number;
|
|
19
|
+
readonly suggestTimeoutMs: number;
|
|
20
|
+
/** Optional provider/model route override; absent → the agent default. */
|
|
21
|
+
readonly suggestProvider?: string;
|
|
22
|
+
readonly suggestModel?: string;
|
|
13
23
|
}
|
|
14
24
|
export declare const DEFAULT_CONFIG: GitPanelConfig;
|
|
15
25
|
export declare function normalizeConfig(raw: unknown): GitPanelConfig;
|
|
@@ -61,6 +71,15 @@ export interface SnapshotDeps {
|
|
|
61
71
|
* cwd is picked up soon after.
|
|
62
72
|
*/
|
|
63
73
|
readonly rootNegCache?: Map<string, number>;
|
|
74
|
+
/**
|
|
75
|
+
* Optional per-request resolvers for the host `llm` and `agentDefaultModel`
|
|
76
|
+
* services (see llm-face.ts). Resolved on every suggest call — never frozen
|
|
77
|
+
* at construction — so a provider fiber that activates late or restarts is
|
|
78
|
+
* picked up. Absent in deployments without a model backend; the suggest
|
|
79
|
+
* endpoint then reports `llm-unavailable` instead of failing activation.
|
|
80
|
+
*/
|
|
81
|
+
readonly getLlm?: () => LlmFace | undefined;
|
|
82
|
+
readonly getAgentDefaultModel?: () => AgentDefaultModelFace | undefined;
|
|
64
83
|
}
|
|
65
84
|
export type WorkspaceResolution = {
|
|
66
85
|
readonly ok: true;
|
package/lib/host/index.d.ts
CHANGED
|
@@ -9,13 +9,14 @@
|
|
|
9
9
|
import { TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
|
|
10
10
|
import type { Context } from '@deepseek-ai/cordis';
|
|
11
11
|
import Schema from '@deepseek-ai/schemastery';
|
|
12
|
-
import type { GitActionRequest, GitActionResult, GitQueryRequest, GitQueryResponse, GitSnapshotRequest, GitSnapshotResult, GitVersionInfo, GitVersionRequest } from './types.ts';
|
|
13
|
-
export type { GitSnapshot, GitSnapshotResult, GitSnapshotRequest, GitFailure, GitCommit, GraphCommit, GitRef, GitChange, GitChangeStatus, GitAction, GitActionRequest, GitActionResult, GitErrorCode, GitQuery, GitQueryRequest, GitQueryResponse, GitQueryResult, GitBranch, GitFileStat, WorktreeStats, GitVersionRequest, GitVersionInfo, DiffViewMode, DirEntry, } from './types.ts';
|
|
12
|
+
import type { GitActionRequest, GitActionResult, GitQueryRequest, GitQueryResponse, GitSnapshotRequest, GitSnapshotResult, GitSuggestRequest, GitSuggestResult, GitVersionInfo, GitVersionRequest } from './types.ts';
|
|
13
|
+
export type { GitSnapshot, GitSnapshotResult, GitSnapshotRequest, GitFailure, GitCommit, GraphCommit, GitRef, GitChange, GitChangeStatus, GitAction, GitActionRequest, GitActionResult, GitErrorCode, GitQuery, GitQueryRequest, GitQueryResponse, GitQueryResult, GitBranch, GitFileStat, WorktreeStats, GitVersionRequest, GitVersionInfo, DiffViewMode, DirEntry, GitSuggestRequest, GitSuggestResult, } from './types.ts';
|
|
14
14
|
export { normalizeConfig, DEFAULT_CONFIG, snapshotForSession, resolveWorkspace } from './core.ts';
|
|
15
15
|
export { createGitRunner } from './git.ts';
|
|
16
16
|
export { parseStatus, parseGraphLog, parseBranches, parseNameStatus, sumNumstat } from './parser.ts';
|
|
17
17
|
export { isSafePath, planAction, runAction } from './actions.ts';
|
|
18
18
|
export { runQuery } from './queries.ts';
|
|
19
|
+
export { runSuggest } from './suggest.ts';
|
|
19
20
|
export { readVersionInfo, checkLatestVersion, compareVersions, parseRepository } from './version.ts';
|
|
20
21
|
export declare class GitPanelService extends TypertRemoteService {
|
|
21
22
|
static inject: string[];
|
|
@@ -28,9 +29,11 @@ export declare class GitPanelService extends TypertRemoteService {
|
|
|
28
29
|
static Config: Schema<Schemastery.ObjectS<NoInfer<{
|
|
29
30
|
showInputPill: Schema<boolean, boolean, "volatile-defined">;
|
|
30
31
|
defaultDiffView: Schema<"split" | "unified", "split" | "unified", "volatile-defined">;
|
|
32
|
+
suggestEnabled: Schema<boolean, boolean, "volatile-defined">;
|
|
31
33
|
}>>, Schemastery.ObjectT<NoInfer<{
|
|
32
34
|
showInputPill: Schema<boolean, boolean, "volatile-defined">;
|
|
33
35
|
defaultDiffView: Schema<"split" | "unified", "split" | "unified", "volatile-defined">;
|
|
36
|
+
suggestEnabled: Schema<boolean, boolean, "volatile-defined">;
|
|
34
37
|
}>>, "plain">;
|
|
35
38
|
private readonly deps;
|
|
36
39
|
private config;
|
|
@@ -40,6 +43,7 @@ export declare class GitPanelService extends TypertRemoteService {
|
|
|
40
43
|
snapshot(request: GitSnapshotRequest, signal?: AbortSignal): Promise<GitSnapshotResult>;
|
|
41
44
|
run(request: GitActionRequest, signal?: AbortSignal): Promise<GitActionResult>;
|
|
42
45
|
query(request: GitQueryRequest, signal?: AbortSignal): Promise<GitQueryResponse>;
|
|
46
|
+
suggest(request: GitSuggestRequest, signal?: AbortSignal): Promise<GitSuggestResult>;
|
|
43
47
|
version(request: GitVersionRequest): Promise<GitVersionInfo>;
|
|
44
48
|
/** Re-read config so a live-edited volatile field (showInputPill) is current. */
|
|
45
49
|
private liveConfig;
|
package/lib/host/index.js
CHANGED
|
@@ -317,18 +317,30 @@ var DEFAULT_CONFIG = {
|
|
|
317
317
|
maxChanges: 1e3,
|
|
318
318
|
refreshIntervalMs: 3e4,
|
|
319
319
|
showInputPill: true,
|
|
320
|
-
defaultDiffView: "unified"
|
|
320
|
+
defaultDiffView: "unified",
|
|
321
|
+
suggestEnabled: true,
|
|
322
|
+
suggestMaxBytes: 64 * 1024,
|
|
323
|
+
suggestMaxOutputTokens: 200,
|
|
324
|
+
suggestTimeoutMs: 15e3
|
|
321
325
|
};
|
|
322
326
|
function normalizeConfig(raw) {
|
|
323
327
|
const c = raw ?? {};
|
|
324
328
|
const num = (v, d) => typeof v === "number" && Number.isFinite(v) && v >= 1 ? Math.floor(v) : d;
|
|
329
|
+
const hasProvider = typeof c.suggestProvider === "string" && c.suggestProvider !== "";
|
|
330
|
+
const hasModel = typeof c.suggestModel === "string" && c.suggestModel !== "";
|
|
331
|
+
const route = hasProvider && hasModel ? { suggestProvider: c.suggestProvider, suggestModel: c.suggestModel } : {};
|
|
325
332
|
return {
|
|
326
333
|
timeoutMs: num(c.timeoutMs, DEFAULT_CONFIG.timeoutMs),
|
|
327
334
|
maxBytes: num(c.maxBytes, DEFAULT_CONFIG.maxBytes),
|
|
328
335
|
maxChanges: num(c.maxChanges, DEFAULT_CONFIG.maxChanges),
|
|
329
336
|
refreshIntervalMs: num(c.refreshIntervalMs, DEFAULT_CONFIG.refreshIntervalMs),
|
|
330
337
|
showInputPill: readBool(c.showInputPill, DEFAULT_CONFIG.showInputPill),
|
|
331
|
-
defaultDiffView: readDiffView(c.defaultDiffView, DEFAULT_CONFIG.defaultDiffView)
|
|
338
|
+
defaultDiffView: readDiffView(c.defaultDiffView, DEFAULT_CONFIG.defaultDiffView),
|
|
339
|
+
suggestEnabled: readBool(c.suggestEnabled, DEFAULT_CONFIG.suggestEnabled),
|
|
340
|
+
suggestMaxBytes: num(c.suggestMaxBytes, DEFAULT_CONFIG.suggestMaxBytes),
|
|
341
|
+
suggestMaxOutputTokens: num(c.suggestMaxOutputTokens, DEFAULT_CONFIG.suggestMaxOutputTokens),
|
|
342
|
+
suggestTimeoutMs: num(c.suggestTimeoutMs, DEFAULT_CONFIG.suggestTimeoutMs),
|
|
343
|
+
...route
|
|
332
344
|
};
|
|
333
345
|
}
|
|
334
346
|
function unwrapVolatile(value) {
|
|
@@ -526,6 +538,7 @@ async function snapshotForSession(deps, config, sessionId) {
|
|
|
526
538
|
refreshIntervalMs: config.refreshIntervalMs,
|
|
527
539
|
showInputPill: config.showInputPill,
|
|
528
540
|
defaultDiffView: config.defaultDiffView,
|
|
541
|
+
suggestEnabled: config.suggestEnabled,
|
|
529
542
|
checkedAt: Date.now()
|
|
530
543
|
};
|
|
531
544
|
return { ok: true, value: snapshot };
|
|
@@ -693,6 +706,7 @@ function imageMimeFor(path) {
|
|
|
693
706
|
|
|
694
707
|
// src/host/queries.ts
|
|
695
708
|
var GRAPH_FORMAT = "--format=%H%x1f%h%x1f%P%x1f%an%x1f%aI%x1f%D%x1f%s%x1e";
|
|
709
|
+
var VISIBLE_REFS = ["--branches", "--remotes", "--tags", "HEAD"];
|
|
696
710
|
function isHexLike(text) {
|
|
697
711
|
return /^[0-9a-fA-F]{7,40}$/.test(text.trim());
|
|
698
712
|
}
|
|
@@ -741,7 +755,7 @@ async function runQuery(deps, config, request) {
|
|
|
741
755
|
async function queryHistory(deps, root, q) {
|
|
742
756
|
const limit = Number.isFinite(q.limit) ? Math.min(500, Math.max(1, Math.trunc(q.limit))) : 100;
|
|
743
757
|
const skip = Number.isFinite(q.skip) ? Math.max(0, Math.trunc(q.skip)) : 0;
|
|
744
|
-
const args = ["git", "log", GRAPH_FORMAT, `--max-count=${limit}`, `--skip=${skip}`];
|
|
758
|
+
const args = ["git", "log", "--topo-order", GRAPH_FORMAT, `--max-count=${limit}`, `--skip=${skip}`];
|
|
745
759
|
const search = q.search?.trim() ?? "";
|
|
746
760
|
const hexJump = search !== "" && isHexLike(search);
|
|
747
761
|
const countArgs = ["git", "rev-list", "--count"];
|
|
@@ -754,7 +768,7 @@ async function queryHistory(deps, root, q) {
|
|
|
754
768
|
if (!isSafeRev(q.ref)) return { ok: false, error: { code: "invalid-name", message: `unsafe ref: ${q.ref}` } };
|
|
755
769
|
filters.push("--end-of-options", q.ref);
|
|
756
770
|
} else {
|
|
757
|
-
filters.push(
|
|
771
|
+
filters.push(...VISIBLE_REFS);
|
|
758
772
|
}
|
|
759
773
|
args.push(...filters);
|
|
760
774
|
countArgs.push(...filters);
|
|
@@ -1083,7 +1097,7 @@ async function queryTags(deps, root) {
|
|
|
1083
1097
|
return { ok: true, value: { kind: "tags", tags } };
|
|
1084
1098
|
}
|
|
1085
1099
|
async function queryAuthors(deps, root) {
|
|
1086
|
-
const res = await runCommand(deps.run, ["git", "log",
|
|
1100
|
+
const res = await runCommand(deps.run, ["git", "log", ...VISIBLE_REFS, "--format=%an", "--max-count=2000"], root, "authors", deps.signal);
|
|
1087
1101
|
const authors = "run" in res && res.run.exitCode === 0 ? [...new Set(res.run.stdout.split("\n").map((s) => s.trim()).filter((s) => s !== ""))].sort((a, b) => a.localeCompare(b)) : [];
|
|
1088
1102
|
return { ok: true, value: { kind: "authors", authors } };
|
|
1089
1103
|
}
|
|
@@ -1098,6 +1112,219 @@ async function queryWorktreeStats(deps, config, sessionId) {
|
|
|
1098
1112
|
return { ok: true, value: { kind: "worktree-stats", stats: snapshot.value.stats } };
|
|
1099
1113
|
}
|
|
1100
1114
|
|
|
1115
|
+
// src/host/suggest.ts
|
|
1116
|
+
var SYSTEM_PROMPT = [
|
|
1117
|
+
"You are an expert at writing concise, accurate git commit messages.",
|
|
1118
|
+
"Given a repository\u2019s uncommitted changes, write ONE commit message for them.",
|
|
1119
|
+
"The message must start with a short imperative subject line (72 chars max), optionally followed by a blank line and a brief body explaining what changed and why.",
|
|
1120
|
+
"Return only the message text: no quotes, code fences, prefixes, bullet lists, or explanations.",
|
|
1121
|
+
"Match the language and style of the recent commit message when one is provided; otherwise write in English.",
|
|
1122
|
+
"Focus on the most important change; do not mechanically list every file."
|
|
1123
|
+
].join("\n");
|
|
1124
|
+
var UNTRACKED_FILE_CAP = 16;
|
|
1125
|
+
function resolveRoute(config, agentDefaultModel) {
|
|
1126
|
+
if (config.suggestProvider !== void 0 && config.suggestModel !== void 0) {
|
|
1127
|
+
return { provider: config.suggestProvider, model: config.suggestModel };
|
|
1128
|
+
}
|
|
1129
|
+
return agentDefaultModel?.currentSelection();
|
|
1130
|
+
}
|
|
1131
|
+
function withDeadline(timeoutMs, parent) {
|
|
1132
|
+
const controller = new AbortController();
|
|
1133
|
+
let timedOut = false;
|
|
1134
|
+
const timer = setTimeout(() => {
|
|
1135
|
+
timedOut = true;
|
|
1136
|
+
controller.abort();
|
|
1137
|
+
}, timeoutMs);
|
|
1138
|
+
const onAbort = () => {
|
|
1139
|
+
controller.abort();
|
|
1140
|
+
};
|
|
1141
|
+
if (parent !== void 0) {
|
|
1142
|
+
if (parent.aborted) {
|
|
1143
|
+
controller.abort();
|
|
1144
|
+
} else parent.addEventListener("abort", onAbort, { once: true });
|
|
1145
|
+
}
|
|
1146
|
+
return {
|
|
1147
|
+
signal: controller.signal,
|
|
1148
|
+
expired: () => timedOut,
|
|
1149
|
+
dispose: () => {
|
|
1150
|
+
clearTimeout(timer);
|
|
1151
|
+
parent?.removeEventListener("abort", onAbort);
|
|
1152
|
+
}
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
function truncateUtf8(text, maxBytes) {
|
|
1156
|
+
if (Buffer.byteLength(text, "utf8") <= maxBytes) return text;
|
|
1157
|
+
let lo = 0;
|
|
1158
|
+
let hi = text.length;
|
|
1159
|
+
while (lo < hi) {
|
|
1160
|
+
const mid = lo + hi + 1 >> 1;
|
|
1161
|
+
if (Buffer.byteLength(text.slice(0, mid), "utf8") <= maxBytes) lo = mid;
|
|
1162
|
+
else hi = mid - 1;
|
|
1163
|
+
}
|
|
1164
|
+
return text.slice(0, lo);
|
|
1165
|
+
}
|
|
1166
|
+
function frameChanges(files, diff, recentCommitMessage) {
|
|
1167
|
+
return JSON.stringify({
|
|
1168
|
+
files,
|
|
1169
|
+
diff,
|
|
1170
|
+
recentCommitMessage: recentCommitMessage === "" ? null : recentCommitMessage
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
function normalizeMessage(raw) {
|
|
1174
|
+
let m = raw.trim();
|
|
1175
|
+
if (m.startsWith("```")) {
|
|
1176
|
+
const firstNl = m.indexOf("\n");
|
|
1177
|
+
if (firstNl !== -1) {
|
|
1178
|
+
m = m.slice(firstNl + 1);
|
|
1179
|
+
m = m.endsWith("```") ? m.slice(0, -3) : m;
|
|
1180
|
+
m = m.trim();
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
return m.replace(/\n---+\s*$/, "").trim();
|
|
1184
|
+
}
|
|
1185
|
+
function finishError(reason) {
|
|
1186
|
+
if (reason === void 0 || reason.kind === "stop") return void 0;
|
|
1187
|
+
if (reason.kind === "max-tokens") return { ok: false, error: { code: "llm-output", message: "model output hit the token limit" } };
|
|
1188
|
+
if (reason.kind === "tool-calls") return { ok: false, error: { code: "llm-output", message: "model requested a tool instead of writing a message" } };
|
|
1189
|
+
const failure = "failure" in reason ? reason.failure : void 0;
|
|
1190
|
+
return { ok: false, error: { code: "llm-error", message: failure?.message ?? `model stream finished abnormally (${reason.kind})` } };
|
|
1191
|
+
}
|
|
1192
|
+
async function collectDiff(deps, root, hasHead, pathArgs) {
|
|
1193
|
+
const cmds = hasHead ? [["git", "diff", "HEAD", ...pathArgs]] : [["git", "diff", "--cached", ...pathArgs], ["git", "diff", ...pathArgs]];
|
|
1194
|
+
const runs = await Promise.all(cmds.map((argv) => runCommand(deps.run, argv, root, "suggest-diff", deps.signal)));
|
|
1195
|
+
let diff = "";
|
|
1196
|
+
for (const res of runs) {
|
|
1197
|
+
if ("failure" in res) return { ok: false, error: { code: "git-unavailable", message: "diff failed" } };
|
|
1198
|
+
if (res.run.cancelled) return { ok: false, error: { code: "cancelled" } };
|
|
1199
|
+
if (res.run.timedOut) return { ok: false, error: { code: "timeout" } };
|
|
1200
|
+
if (res.run.exitCode !== 0) {
|
|
1201
|
+
return { ok: false, error: { code: "git-error", message: res.run.stderr.trim() || "git diff failed" } };
|
|
1202
|
+
}
|
|
1203
|
+
diff += res.run.stdout;
|
|
1204
|
+
}
|
|
1205
|
+
return diff;
|
|
1206
|
+
}
|
|
1207
|
+
async function appendUntracked(deps, root, files, diff, budget) {
|
|
1208
|
+
let text = diff;
|
|
1209
|
+
let truncated = false;
|
|
1210
|
+
const candidates = files.filter((c) => c.status === "untracked" && !c.isDirectory && isSafePath(c.path));
|
|
1211
|
+
for (const c of candidates.slice(0, UNTRACKED_FILE_CAP)) {
|
|
1212
|
+
const remaining = budget - Buffer.byteLength(text, "utf8");
|
|
1213
|
+
if (remaining <= 0) {
|
|
1214
|
+
truncated = true;
|
|
1215
|
+
break;
|
|
1216
|
+
}
|
|
1217
|
+
const res = await runCommand(deps.run, ["git", "diff", "--no-index", "--", "/dev/null", c.path], root, "suggest-untracked", deps.signal);
|
|
1218
|
+
if ("failure" in res || res.run.cancelled || res.run.timedOut) continue;
|
|
1219
|
+
const chunk = res.run.stdout;
|
|
1220
|
+
if (chunk === "") continue;
|
|
1221
|
+
if (Buffer.byteLength(chunk, "utf8") > remaining) {
|
|
1222
|
+
text += truncateUtf8(chunk, remaining) + "\n... [diff truncated]";
|
|
1223
|
+
truncated = true;
|
|
1224
|
+
break;
|
|
1225
|
+
}
|
|
1226
|
+
text += chunk;
|
|
1227
|
+
}
|
|
1228
|
+
return { diff: text, truncated };
|
|
1229
|
+
}
|
|
1230
|
+
async function runSuggest(deps, config, request) {
|
|
1231
|
+
if (!config.suggestEnabled) {
|
|
1232
|
+
return { ok: false, error: { code: "suggest-disabled", message: "AI suggest is turned off in the plugin config" } };
|
|
1233
|
+
}
|
|
1234
|
+
const workspace = await resolveWorkspace(deps, request.sessionId);
|
|
1235
|
+
if (!workspace.ok) return { ok: false, error: mapWorkspaceFailure(workspace.failure) };
|
|
1236
|
+
const root = workspace.root;
|
|
1237
|
+
const paths = request.paths ?? [];
|
|
1238
|
+
for (const path of paths) {
|
|
1239
|
+
if (!isSafePath(path)) return { ok: false, error: { code: "invalid-path", message: path } };
|
|
1240
|
+
}
|
|
1241
|
+
const pathArgs = paths.length > 0 ? ["--", ...paths] : [];
|
|
1242
|
+
const pathSet = new Set(paths);
|
|
1243
|
+
const [statusRes, headRes, lastRes] = await Promise.all([
|
|
1244
|
+
runCommand(deps.run, ["git", "status", "--porcelain=v1", "-z"], root, "suggest-status", deps.signal),
|
|
1245
|
+
runCommand(deps.run, ["git", "rev-parse", "--verify", "HEAD"], root, "suggest-head", deps.signal),
|
|
1246
|
+
runCommand(deps.run, ["git", "log", "-1", "--format=%B"], root, "suggest-last", deps.signal)
|
|
1247
|
+
]);
|
|
1248
|
+
if ("failure" in statusRes) return { ok: false, error: { code: "git-unavailable", message: "status failed" } };
|
|
1249
|
+
if (statusRes.run.cancelled) return { ok: false, error: { code: "cancelled" } };
|
|
1250
|
+
if (statusRes.run.timedOut) return { ok: false, error: { code: "timeout" } };
|
|
1251
|
+
if (statusRes.run.exitCode !== 0) {
|
|
1252
|
+
return { ok: false, error: { code: "git-error", message: statusRes.run.stderr.trim() || "git status failed" } };
|
|
1253
|
+
}
|
|
1254
|
+
const hasHead = "run" in headRes && headRes.run.exitCode === 0;
|
|
1255
|
+
const recentCommitMessage = "run" in lastRes && lastRes.run.exitCode === 0 ? lastRes.run.stdout.replace(/\n+$/, "") : "";
|
|
1256
|
+
const all = statusRes.run.stdoutLossy ? [] : parseStatus(statusRes.run.stdout);
|
|
1257
|
+
const files = pathSet.size > 0 ? all.filter((c) => pathSet.has(c.path)) : all;
|
|
1258
|
+
const collected = await collectDiff(deps, root, hasHead, pathArgs);
|
|
1259
|
+
if (typeof collected !== "string") return collected;
|
|
1260
|
+
if (files.length === 0 && collected.trim() === "") {
|
|
1261
|
+
return { ok: false, error: { code: "empty-diff", message: "no uncommitted changes to summarize" } };
|
|
1262
|
+
}
|
|
1263
|
+
let diff = collected;
|
|
1264
|
+
let truncated = false;
|
|
1265
|
+
if (Buffer.byteLength(diff, "utf8") > config.suggestMaxBytes) {
|
|
1266
|
+
diff = truncateUtf8(diff, config.suggestMaxBytes) + "\n... [diff truncated]";
|
|
1267
|
+
truncated = true;
|
|
1268
|
+
}
|
|
1269
|
+
const untracked = await appendUntracked(deps, root, files, diff, config.suggestMaxBytes);
|
|
1270
|
+
diff = untracked.diff;
|
|
1271
|
+
truncated = truncated || untracked.truncated;
|
|
1272
|
+
const llm = deps.getLlm?.();
|
|
1273
|
+
const route = resolveRoute(config, deps.getAgentDefaultModel?.());
|
|
1274
|
+
if (llm === void 0 || route === void 0) {
|
|
1275
|
+
return { ok: false, error: { code: "llm-unavailable", message: "no model service or route is configured" } };
|
|
1276
|
+
}
|
|
1277
|
+
const frame = frameChanges(
|
|
1278
|
+
files.map((c) => ({ path: c.path, status: c.status })),
|
|
1279
|
+
diff,
|
|
1280
|
+
recentCommitMessage
|
|
1281
|
+
);
|
|
1282
|
+
const deadline = withDeadline(config.suggestTimeoutMs, deps.signal);
|
|
1283
|
+
const options = {
|
|
1284
|
+
provider: route.provider,
|
|
1285
|
+
model: route.model,
|
|
1286
|
+
system: SYSTEM_PROMPT,
|
|
1287
|
+
messages: [{
|
|
1288
|
+
id: "git-panel-suggest",
|
|
1289
|
+
role: "user",
|
|
1290
|
+
content: [{ type: "text", text: frame }],
|
|
1291
|
+
source: { kind: "user" }
|
|
1292
|
+
}],
|
|
1293
|
+
maxTokens: config.suggestMaxOutputTokens,
|
|
1294
|
+
temperature: 0.3,
|
|
1295
|
+
signal: deadline.signal
|
|
1296
|
+
};
|
|
1297
|
+
let output = "";
|
|
1298
|
+
let finish;
|
|
1299
|
+
try {
|
|
1300
|
+
for await (const chunk of llm.stream(options)) {
|
|
1301
|
+
if (chunk.type === "text-delta") output += chunk.text;
|
|
1302
|
+
else if (chunk.type === "finish") finish = chunk.reason;
|
|
1303
|
+
}
|
|
1304
|
+
} catch (error) {
|
|
1305
|
+
if (deps.signal?.aborted === true) return { ok: false, error: { code: "cancelled" } };
|
|
1306
|
+
if (deadline.expired()) return { ok: false, error: { code: "llm-error", message: `model call timed out after ${config.suggestTimeoutMs}ms` } };
|
|
1307
|
+
return { ok: false, error: { code: "llm-error", message: error instanceof Error ? error.message : "model stream failed" } };
|
|
1308
|
+
} finally {
|
|
1309
|
+
deadline.dispose();
|
|
1310
|
+
}
|
|
1311
|
+
if (deps.signal?.aborted === true) return { ok: false, error: { code: "cancelled" } };
|
|
1312
|
+
if (deadline.expired()) return { ok: false, error: { code: "llm-error", message: `model call timed out after ${config.suggestTimeoutMs}ms` } };
|
|
1313
|
+
const terminal = finishError(finish);
|
|
1314
|
+
if (terminal !== void 0) return terminal;
|
|
1315
|
+
const message = normalizeMessage(output);
|
|
1316
|
+
if (message === "") return { ok: false, error: { code: "llm-output", message: "model produced no message text" } };
|
|
1317
|
+
return {
|
|
1318
|
+
ok: true,
|
|
1319
|
+
value: {
|
|
1320
|
+
message,
|
|
1321
|
+
provider: route.provider,
|
|
1322
|
+
model: route.model,
|
|
1323
|
+
...truncated ? { truncated: true } : {}
|
|
1324
|
+
}
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1101
1328
|
// src/host/version.ts
|
|
1102
1329
|
import { readFile as readFile2 } from "node:fs/promises";
|
|
1103
1330
|
import { dirname as dirname2, join as join4 } from "node:path";
|
|
@@ -1225,8 +1452,8 @@ async function checkLatestVersion(options = {}) {
|
|
|
1225
1452
|
}
|
|
1226
1453
|
|
|
1227
1454
|
// src/host/index.ts
|
|
1228
|
-
var _version_dec, _query_dec, _run_dec, _snapshot_dec, _a, _init;
|
|
1229
|
-
var GitPanelService = class extends (_a = TypertRemoteService, _snapshot_dec = [Remote("snapshot")], _run_dec = [Remote("run")], _query_dec = [Remote("query")], _version_dec = [Remote("version")], _a) {
|
|
1455
|
+
var _version_dec, _suggest_dec, _query_dec, _run_dec, _snapshot_dec, _a, _init;
|
|
1456
|
+
var GitPanelService = class extends (_a = TypertRemoteService, _snapshot_dec = [Remote("snapshot")], _run_dec = [Remote("run")], _query_dec = [Remote("query")], _suggest_dec = [Remote("suggest")], _version_dec = [Remote("version")], _a) {
|
|
1230
1457
|
constructor(ctx, config) {
|
|
1231
1458
|
super(ctx, "gitPanel");
|
|
1232
1459
|
__runInitializers(_init, 5, this);
|
|
@@ -1272,7 +1499,13 @@ var GitPanelService = class extends (_a = TypertRemoteService, _snapshot_dec = [
|
|
|
1272
1499
|
}
|
|
1273
1500
|
},
|
|
1274
1501
|
rootCache,
|
|
1275
|
-
rootNegCache
|
|
1502
|
+
rootNegCache,
|
|
1503
|
+
// Optional model services, resolved per request so a provider fiber that
|
|
1504
|
+
// activates late or restarts is picked up — absent in deployments without
|
|
1505
|
+
// a model backend, where the suggest endpoint reports `llm-unavailable`
|
|
1506
|
+
// rather than failing activation (kept out of `static inject` on purpose).
|
|
1507
|
+
getLlm: () => get("llm"),
|
|
1508
|
+
getAgentDefaultModel: () => get("agentDefaultModel")
|
|
1276
1509
|
};
|
|
1277
1510
|
}
|
|
1278
1511
|
async snapshot(request, signal) {
|
|
@@ -1284,6 +1517,9 @@ var GitPanelService = class extends (_a = TypertRemoteService, _snapshot_dec = [
|
|
|
1284
1517
|
async query(request, signal) {
|
|
1285
1518
|
return runQuery(this.withSignal(signal), this.liveConfig(), request);
|
|
1286
1519
|
}
|
|
1520
|
+
async suggest(request, signal) {
|
|
1521
|
+
return runSuggest(this.withSignal(signal), this.liveConfig(), request);
|
|
1522
|
+
}
|
|
1287
1523
|
async version(request) {
|
|
1288
1524
|
try {
|
|
1289
1525
|
return request.check === true ? await checkLatestVersion() : await readVersionInfo();
|
|
@@ -1305,6 +1541,7 @@ _init = __decoratorStart(_a);
|
|
|
1305
1541
|
__decorateElement(_init, 1, "snapshot", _snapshot_dec, GitPanelService);
|
|
1306
1542
|
__decorateElement(_init, 1, "run", _run_dec, GitPanelService);
|
|
1307
1543
|
__decorateElement(_init, 1, "query", _query_dec, GitPanelService);
|
|
1544
|
+
__decorateElement(_init, 1, "suggest", _suggest_dec, GitPanelService);
|
|
1308
1545
|
__decorateElement(_init, 1, "version", _version_dec, GitPanelService);
|
|
1309
1546
|
__decoratorMetadata(_init, GitPanelService);
|
|
1310
1547
|
__publicField(GitPanelService, "inject", ["subprocess", "sessions", "sessionPersistence"]);
|
|
@@ -1319,7 +1556,8 @@ __publicField(GitPanelService, "Config", Schema.object({
|
|
|
1319
1556
|
defaultDiffView: Schema.union([
|
|
1320
1557
|
Schema.const("unified").description("\u7EDF\u4E00\u89C6\u56FE\uFF08\u5355\u680F\u884C\u5185\u5BF9\u6BD4\uFF09"),
|
|
1321
1558
|
Schema.const("split").description("\u5E76\u6392\u89C6\u56FE\uFF08\u5DE6\u53F3\u5206\u680F\u5BF9\u6BD4\uFF09")
|
|
1322
|
-
]).default("unified").volatile().description("\u5DEE\u5F02\u5BF9\u6BD4\u9ED8\u8BA4\u89C6\u56FE")
|
|
1559
|
+
]).default("unified").volatile().description("\u5DEE\u5F02\u5BF9\u6BD4\u9ED8\u8BA4\u89C6\u56FE"),
|
|
1560
|
+
suggestEnabled: Schema.boolean().default(true).volatile().description("\u63D0\u4EA4\u6846\u663E\u793A AI \u751F\u6210\u6309\u94AE")
|
|
1323
1561
|
}));
|
|
1324
1562
|
var index_default = GitPanelService;
|
|
1325
1563
|
export {
|
|
@@ -1341,6 +1579,7 @@ export {
|
|
|
1341
1579
|
resolveWorkspace,
|
|
1342
1580
|
runAction,
|
|
1343
1581
|
runQuery,
|
|
1582
|
+
runSuggest,
|
|
1344
1583
|
snapshotForSession,
|
|
1345
1584
|
sumNumstat
|
|
1346
1585
|
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural faces of the host `llm` and `agentDefaultModel` services.
|
|
3
|
+
*
|
|
4
|
+
* The `llm` face is typed against the real `@deepseek-ai/dsh-llm` contract
|
|
5
|
+
* via type-only imports — the build externalizes the whole `@deepseek-ai/*`
|
|
6
|
+
* scope, so this adds no runtime dependency, and any wire-shape drift (e.g.
|
|
7
|
+
* the finish chunk's `reason` envelope) breaks typecheck instead of silently
|
|
8
|
+
* passing. Both services are shipped by `@deepseek-ai/dsh-base`; they are
|
|
9
|
+
* resolved per request (never frozen at construction) and absence degrades
|
|
10
|
+
* the suggest endpoint to a typed `llm-unavailable` failure instead of
|
|
11
|
+
* breaking plugin activation (kept out of `static inject` on purpose).
|
|
12
|
+
*/
|
|
13
|
+
import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm';
|
|
14
|
+
/** Minimal face of the host `llm` service (`ctx.llm`). */
|
|
15
|
+
export interface LlmFace {
|
|
16
|
+
stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
|
|
17
|
+
listProviders?(): readonly {
|
|
18
|
+
readonly id: string;
|
|
19
|
+
}[];
|
|
20
|
+
}
|
|
21
|
+
/** Minimal face of the host `agentDefaultModel` service. */
|
|
22
|
+
export interface AgentDefaultModelFace {
|
|
23
|
+
currentSelection(): {
|
|
24
|
+
readonly provider: string;
|
|
25
|
+
readonly model: string;
|
|
26
|
+
} | undefined;
|
|
27
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { GitPanelConfig, SnapshotDeps } from './core.ts';
|
|
2
|
+
import type { GitSuggestRequest, GitSuggestResult } from './types.ts';
|
|
3
|
+
/** Generate a commit message for the session's uncommitted changes. */
|
|
4
|
+
export declare function runSuggest(deps: SnapshotDeps, config: GitPanelConfig, request: GitSuggestRequest): Promise<GitSuggestResult>;
|
package/lib/host/types.d.ts
CHANGED
|
@@ -62,6 +62,8 @@ export interface GitSnapshot {
|
|
|
62
62
|
readonly showInputPill: boolean;
|
|
63
63
|
/** Default diff layout the views open with (user preference). */
|
|
64
64
|
readonly defaultDiffView: DiffViewMode;
|
|
65
|
+
/** Whether the commit box shows the AI-suggest button (user preference). */
|
|
66
|
+
readonly suggestEnabled: boolean;
|
|
65
67
|
/** Epoch millis of the snapshot. */
|
|
66
68
|
readonly checkedAt: number;
|
|
67
69
|
}
|
|
@@ -121,7 +123,7 @@ export type GitAction = {
|
|
|
121
123
|
} | {
|
|
122
124
|
readonly kind: 'fetch';
|
|
123
125
|
};
|
|
124
|
-
export type GitErrorCode = 'cwd-unavailable' | 'not-a-git-repo' | 'git-unavailable' | 'invalid-path' | 'invalid-name' | 'git-error' | 'timeout' | 'cancelled' | 'empty-message' | 'local-changes-block';
|
|
126
|
+
export type GitErrorCode = 'cwd-unavailable' | 'not-a-git-repo' | 'git-unavailable' | 'invalid-path' | 'invalid-name' | 'git-error' | 'timeout' | 'cancelled' | 'empty-message' | 'local-changes-block' | 'empty-diff' | 'llm-unavailable' | 'llm-error' | 'llm-output' | 'suggest-disabled';
|
|
125
127
|
export type GitActionResult = {
|
|
126
128
|
readonly ok: true;
|
|
127
129
|
readonly snapshot: GitSnapshot;
|
|
@@ -325,6 +327,34 @@ export interface GitQueryRequest {
|
|
|
325
327
|
readonly sessionId: string;
|
|
326
328
|
readonly query: GitQuery;
|
|
327
329
|
}
|
|
330
|
+
/**
|
|
331
|
+
* Wire request for the suggest endpoint: ask the configured model to write a
|
|
332
|
+
* commit message from the repository's uncommitted changes. `paths` restricts
|
|
333
|
+
* the diff to the user's selection (bare repo-relative paths); absent means
|
|
334
|
+
* all uncommitted changes.
|
|
335
|
+
*/
|
|
336
|
+
export interface GitSuggestRequest {
|
|
337
|
+
readonly sessionId: string;
|
|
338
|
+
readonly paths?: readonly string[];
|
|
339
|
+
}
|
|
340
|
+
export type GitSuggestResult = {
|
|
341
|
+
readonly ok: true;
|
|
342
|
+
readonly value: {
|
|
343
|
+
/** The generated message, ready for the user to edit and commit. */
|
|
344
|
+
readonly message: string;
|
|
345
|
+
/** Actual provider/model route used (display only). */
|
|
346
|
+
readonly provider?: string;
|
|
347
|
+
readonly model?: string;
|
|
348
|
+
/** The diff exceeded the model input cap and was truncated. */
|
|
349
|
+
readonly truncated?: boolean;
|
|
350
|
+
};
|
|
351
|
+
} | {
|
|
352
|
+
readonly ok: false;
|
|
353
|
+
readonly error: {
|
|
354
|
+
readonly code: GitErrorCode;
|
|
355
|
+
readonly message?: string;
|
|
356
|
+
};
|
|
357
|
+
};
|
|
328
358
|
/**
|
|
329
359
|
* Extensions the image-diff query serves, mapped to MIME types. Part of the
|
|
330
360
|
* query's contract, so both halves gate on this one list and cannot drift.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xbzbing/dsh-git-panel",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "DeepSeek Harness (dsh) plugin: an IDE-style Git panel in the Web GUI — branch/history overview, uncommitted changes with commit & amend, diff stats, and a zsh-style branch marker in the input bar.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"build": "node build.mjs",
|
|
43
43
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
44
44
|
"test:unit": "node build.mjs && node --test --test-force-exit test/unit/*.test.mjs",
|
|
45
|
-
"test:e2e": "node build.mjs && node test/e2e/setup.mjs && node test/e2e/run.mjs && node test/e2e/run2.mjs",
|
|
45
|
+
"test:e2e": "node build.mjs && node test/e2e/setup.mjs && node test/e2e/run.mjs && node test/e2e/run2.mjs && node test/e2e/run3.mjs",
|
|
46
46
|
"test": "npm run test:unit && npm run test:e2e",
|
|
47
47
|
"check": "npm run typecheck && npm run test && git diff --exit-code -- lib/"
|
|
48
48
|
},
|