@xbzbing/dsh-git-panel 1.5.4 → 1.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 +5 -3
- package/README.md +5 -3
- package/lib/client.js +149 -19
- package/lib/host/actions.d.ts +3 -1
- package/lib/host/actions.js +28 -8
- package/lib/host/core.d.ts +9 -1
- package/lib/host/core.js +11 -6
- package/lib/host/index.d.ts +10 -4
- package/lib/host/index.js +21 -9
- package/lib/host/queries.js +67 -28
- package/lib/host/suggest.js +9 -6
- package/lib/host/types.d.ts +77 -3
- package/lib/host/types.js +14 -0
- package/package.json +2 -2
package/lib/host/actions.d.ts
CHANGED
|
@@ -22,7 +22,9 @@ export declare function scrubCredentials(text: string): string;
|
|
|
22
22
|
* rule below (git keeps the stash entry on conflict → recoverable).
|
|
23
23
|
* - index.lock busy (another process holds the lock).
|
|
24
24
|
* - nothing-to-commit / no changes added (a real commit no-op).
|
|
25
|
-
* - not-found (missing ref / tag / stash
|
|
25
|
+
* - not-found (missing ref / tag / stash, or a checkout target that resolves
|
|
26
|
+
* to neither a ref nor a path).
|
|
27
|
+
* - branch-in-use (the target branch is checked out in another worktree).
|
|
26
28
|
* - local-changes-block (a dirty-worktree refusal; neutral wording covers both
|
|
27
29
|
* `git checkout` and `git stash apply`).
|
|
28
30
|
* - revert-merge (reverting a merge commit without a mainline parent — git
|
package/lib/host/actions.js
CHANGED
|
@@ -34,9 +34,16 @@ function planAction(action, unborn) {
|
|
|
34
34
|
const commitCmd = ["git", "commit", ...amendFlag, ...msgArgs, "--", ...action.paths];
|
|
35
35
|
return { argv: [...staged.argv, commitCmd] };
|
|
36
36
|
}
|
|
37
|
-
case "branch-checkout":
|
|
37
|
+
case "branch-checkout": {
|
|
38
38
|
if (!isSafeBranchName(action.name)) return { error: "invalid-name", message: `unsafe branch name: ${action.name}` };
|
|
39
|
-
|
|
39
|
+
let checkout = ["git", "checkout", "--end-of-options", action.name];
|
|
40
|
+
if (action.createFrom !== void 0) {
|
|
41
|
+
if (!isSafeBranchName(action.createFrom)) return { error: "invalid-name", message: `unsafe tracking ref: ${action.createFrom}` };
|
|
42
|
+
checkout = ["git", "checkout", "-b", action.name, "--track", "--end-of-options", action.createFrom];
|
|
43
|
+
}
|
|
44
|
+
const msg = action.stashMessage?.trim() ?? "";
|
|
45
|
+
return { argv: msg === "" ? [checkout] : [["git", "stash", "push", "-u", "-m", msg], checkout] };
|
|
46
|
+
}
|
|
40
47
|
case "fetch":
|
|
41
48
|
return { argv: [["git", "fetch", "--all", "--prune"]] };
|
|
42
49
|
case "pull-ff":
|
|
@@ -101,9 +108,12 @@ function classifyActionFailure(stdout, stderr, exitCode) {
|
|
|
101
108
|
if (/nothing to commit|no changes added/i.test(combined)) {
|
|
102
109
|
return { code: "git-error", message: err || "nothing to commit" };
|
|
103
110
|
}
|
|
104
|
-
if (/No such ref|not a valid reference|is not a stash|no tag|tag .* not found|unknown revision|bad revision|Could not parse object|ambiguous argument/i.test(combined)) {
|
|
111
|
+
if (/No such ref|not a valid reference|is not a stash|no tag|tag .* not found|unknown revision|bad revision|Could not parse object|ambiguous argument|did not match any file\(s\) known to git/i.test(combined)) {
|
|
105
112
|
return { code: "not-found", message: err || "not found" };
|
|
106
113
|
}
|
|
114
|
+
if (/is already used by worktree|already checked out at/i.test(combined)) {
|
|
115
|
+
return { code: "branch-in-use", message: err || "branch is checked out in another worktree" };
|
|
116
|
+
}
|
|
107
117
|
if (/Authentication failed|could not read (Username|Password)|terminal prompts disabled|Permission denied \(publickey\)|Could not read from remote repository/i.test(combined)) {
|
|
108
118
|
return { code: "auth-failed", message: err || "authentication failed" };
|
|
109
119
|
}
|
|
@@ -152,6 +162,7 @@ async function runAction(deps, config, request) {
|
|
|
152
162
|
}
|
|
153
163
|
const plan = planAction(request.action, unborn);
|
|
154
164
|
if ("error" in plan) return { ok: false, error: { code: plan.error, ...plan.message ? { message: plan.message } : {} } };
|
|
165
|
+
const stashing = request.action.kind === "branch-checkout" && (request.action.stashMessage?.trim() ?? "") !== "";
|
|
155
166
|
let lastOutput = "";
|
|
156
167
|
for (let step = 0; step < plan.argv.length; step += 1) {
|
|
157
168
|
const argv = plan.argv[step];
|
|
@@ -161,15 +172,23 @@ async function runAction(deps, config, request) {
|
|
|
161
172
|
outcome = await runCommand(deps.run, argv, root, "action-retry", deps.signal);
|
|
162
173
|
}
|
|
163
174
|
const where = plan.argv.length > 1 ? ` (step ${step + 1}/${plan.argv.length}: ${argv.join(" ")})` : "";
|
|
175
|
+
const stashChangedWorkTree = stashing && step > 0;
|
|
164
176
|
if ("failure" in outcome) {
|
|
165
177
|
const message = outcome.failure instanceof Error ? outcome.failure.message : String(outcome.failure);
|
|
166
|
-
return { ok: false, error: { code: "git-unavailable", message: message + where } };
|
|
178
|
+
return { ok: false, error: { code: stashChangedWorkTree ? "stash-switch-failed" : "git-unavailable", message: message + where } };
|
|
179
|
+
}
|
|
180
|
+
if (outcome.run.cancelled) {
|
|
181
|
+
return { ok: false, error: stashChangedWorkTree ? { code: "stash-switch-failed", message: "cancelled" + where } : { code: "cancelled" } };
|
|
182
|
+
}
|
|
183
|
+
if (outcome.run.timedOut) {
|
|
184
|
+
return { ok: false, error: stashChangedWorkTree ? { code: "stash-switch-failed", message: "timed out" + where } : { code: "timeout" } };
|
|
167
185
|
}
|
|
168
|
-
if (outcome.run.cancelled) return { ok: false, error: { code: "cancelled" } };
|
|
169
|
-
if (outcome.run.timedOut) return { ok: false, error: { code: "timeout" } };
|
|
170
186
|
lastOutput = outcome.run.stdout || outcome.run.stderr;
|
|
171
187
|
if (outcome.run.exitCode !== 0) {
|
|
172
188
|
const failure = classifyActionFailure(outcome.run.stdout, outcome.run.stderr, outcome.run.exitCode ?? -1);
|
|
189
|
+
if (stashChangedWorkTree) {
|
|
190
|
+
return { ok: false, error: { code: "stash-switch-failed", message: failure.message + where } };
|
|
191
|
+
}
|
|
173
192
|
if (request.action.kind === "revert" && failure.code === "conflict") {
|
|
174
193
|
let abort = await runCommand(deps.run, ["git", "revert", "--abort"], root, "revert-abort", deps.signal);
|
|
175
194
|
if (isIndexBusy(abort)) {
|
|
@@ -181,8 +200,9 @@ async function runAction(deps, config, request) {
|
|
|
181
200
|
}
|
|
182
201
|
return { ok: false, error: { code: failure.code, message: failure.message + where } };
|
|
183
202
|
}
|
|
184
|
-
if (request.action.kind === "stash-push" && /No local changes to save/i.test(outcome.run.stdout + outcome.run.stderr)) {
|
|
185
|
-
|
|
203
|
+
if ((request.action.kind === "stash-push" || stashing) && /No local changes to save/i.test(outcome.run.stdout + outcome.run.stderr)) {
|
|
204
|
+
const message = (outcome.run.stdout.trim() || "No local changes to save") + where;
|
|
205
|
+
return { ok: false, error: { code: stashing ? "local-changes-block" : "git-error", message } };
|
|
186
206
|
}
|
|
187
207
|
}
|
|
188
208
|
const snapshot = await snapshotForSession(deps, config, request.sessionId);
|
package/lib/host/core.d.ts
CHANGED
|
@@ -15,9 +15,11 @@ export interface GitPanelConfig {
|
|
|
15
15
|
readonly graphStyle: GraphStyle;
|
|
16
16
|
/** Whether the "AI suggest" button shows in the commit box. */
|
|
17
17
|
readonly suggestEnabled: boolean;
|
|
18
|
-
/** Max diff bytes sent to the model; beyond this the diff is truncated. */
|
|
18
|
+
/** Max diff bytes sent to the model; beyond this the diff is truncated. Range 16–512 KiB. */
|
|
19
19
|
readonly suggestMaxBytes: number;
|
|
20
|
+
/** Model output token budget (reasoning tokens count against it on thinking models). Range 256–16384. */
|
|
20
21
|
readonly suggestMaxOutputTokens: number;
|
|
22
|
+
/** Timeout for one suggest call. Range 5–120 s. */
|
|
21
23
|
readonly suggestTimeoutMs: number;
|
|
22
24
|
/** Optional provider/model route override; absent → the agent default. */
|
|
23
25
|
readonly suggestProvider?: string;
|
|
@@ -46,6 +48,12 @@ export interface SnapshotDeps {
|
|
|
46
48
|
}>;
|
|
47
49
|
/** Raw bytes (no encoding) — image sides for the image-diff query. */
|
|
48
50
|
readFile(path: string): Promise<Buffer>;
|
|
51
|
+
/**
|
|
52
|
+
* A symlink's target text, else null. git stores the LINK as the worktree
|
|
53
|
+
* blob (mode 120000), so the diff's new side is the target string, not the
|
|
54
|
+
* file it points at — following the link would describe a different file.
|
|
55
|
+
*/
|
|
56
|
+
symlinkTarget(path: string): Promise<string | null>;
|
|
49
57
|
/** One directory's entries (name + is-directory) — the file browser. */
|
|
50
58
|
readdir(path: string): Promise<ReadonlyArray<{
|
|
51
59
|
name: string;
|
package/lib/host/core.js
CHANGED
|
@@ -2,6 +2,7 @@ import { dirname, join } from "node:path";
|
|
|
2
2
|
import { commitFromFields, parseStatus, sumNumstat } from "./parser.js";
|
|
3
3
|
import { parseRemote } from "./remote.js";
|
|
4
4
|
import { isSafePath } from "./validate.js";
|
|
5
|
+
import { SUGGEST_LIMITS } from "./types.js";
|
|
5
6
|
const DEFAULT_CONFIG = {
|
|
6
7
|
timeoutMs: 8e3,
|
|
7
8
|
maxBytes: 4 * 1024 * 1024,
|
|
@@ -11,13 +12,17 @@ const DEFAULT_CONFIG = {
|
|
|
11
12
|
defaultDiffView: "unified",
|
|
12
13
|
graphStyle: "parallel",
|
|
13
14
|
suggestEnabled: true,
|
|
14
|
-
suggestMaxBytes:
|
|
15
|
-
suggestMaxOutputTokens:
|
|
16
|
-
suggestTimeoutMs:
|
|
15
|
+
suggestMaxBytes: SUGGEST_LIMITS.maxBytes.default,
|
|
16
|
+
suggestMaxOutputTokens: SUGGEST_LIMITS.maxOutputTokens.default,
|
|
17
|
+
suggestTimeoutMs: SUGGEST_LIMITS.timeoutMs.default
|
|
17
18
|
};
|
|
18
19
|
function normalizeConfig(raw) {
|
|
19
20
|
const c = raw ?? {};
|
|
20
21
|
const num = (v, d) => typeof v === "number" && Number.isFinite(v) && v >= 1 ? Math.floor(v) : d;
|
|
22
|
+
const clampLimit = (v, limit) => {
|
|
23
|
+
const raw2 = unwrapVolatile(v);
|
|
24
|
+
return typeof raw2 === "number" && Number.isFinite(raw2) ? Math.min(limit.max, Math.max(limit.min, Math.floor(raw2))) : limit.default;
|
|
25
|
+
};
|
|
21
26
|
const hasProvider = typeof c.suggestProvider === "string" && c.suggestProvider !== "";
|
|
22
27
|
const hasModel = typeof c.suggestModel === "string" && c.suggestModel !== "";
|
|
23
28
|
const route = hasProvider && hasModel ? { suggestProvider: c.suggestProvider, suggestModel: c.suggestModel } : {};
|
|
@@ -30,9 +35,9 @@ function normalizeConfig(raw) {
|
|
|
30
35
|
defaultDiffView: readDiffView(c.defaultDiffView, DEFAULT_CONFIG.defaultDiffView),
|
|
31
36
|
graphStyle: readGraphStyle(c.graphStyle, DEFAULT_CONFIG.graphStyle),
|
|
32
37
|
suggestEnabled: readBool(c.suggestEnabled, DEFAULT_CONFIG.suggestEnabled),
|
|
33
|
-
suggestMaxBytes:
|
|
34
|
-
suggestMaxOutputTokens:
|
|
35
|
-
suggestTimeoutMs:
|
|
38
|
+
suggestMaxBytes: clampLimit(c.suggestMaxBytes, SUGGEST_LIMITS.maxBytes),
|
|
39
|
+
suggestMaxOutputTokens: clampLimit(c.suggestMaxOutputTokens, SUGGEST_LIMITS.maxOutputTokens),
|
|
40
|
+
suggestTimeoutMs: clampLimit(c.suggestTimeoutMs, SUGGEST_LIMITS.timeoutMs),
|
|
36
41
|
...route
|
|
37
42
|
};
|
|
38
43
|
}
|
package/lib/host/index.d.ts
CHANGED
|
@@ -22,21 +22,27 @@ export { readVersionInfo, checkLatestVersion, compareVersions, parseRepository }
|
|
|
22
22
|
export declare class GitPanelService extends TypertRemoteService {
|
|
23
23
|
static inject: string[];
|
|
24
24
|
/**
|
|
25
|
-
* Config schema surfaced on the plugin detail page. `showInputPill
|
|
26
|
-
* `defaultDiffView` are `.volatile()`,
|
|
27
|
-
*
|
|
28
|
-
* of the UI form.
|
|
25
|
+
* Config schema surfaced on the plugin detail page. `showInputPill`,
|
|
26
|
+
* `defaultDiffView`, `graphStyle` and the suggest fields are `.volatile()`,
|
|
27
|
+
* so the settings host renders them as live-editable controls; the
|
|
28
|
+
* operational limits stay profile-only and out of the UI form.
|
|
29
29
|
*/
|
|
30
30
|
static Config: Schema<Schemastery.ObjectS<NoInfer<{
|
|
31
31
|
showInputPill: Schema<boolean, boolean, "volatile-defined">;
|
|
32
32
|
defaultDiffView: Schema<"split" | "unified", "split" | "unified", "volatile-defined">;
|
|
33
33
|
graphStyle: Schema<"compact" | "parallel", "compact" | "parallel", "volatile-defined">;
|
|
34
34
|
suggestEnabled: Schema<boolean, boolean, "volatile-defined">;
|
|
35
|
+
suggestMaxBytes: Schema<number, number, "volatile-defined">;
|
|
36
|
+
suggestMaxOutputTokens: Schema<number, number, "volatile-defined">;
|
|
37
|
+
suggestTimeoutMs: Schema<number, number, "volatile-defined">;
|
|
35
38
|
}>>, Schemastery.ObjectT<NoInfer<{
|
|
36
39
|
showInputPill: Schema<boolean, boolean, "volatile-defined">;
|
|
37
40
|
defaultDiffView: Schema<"split" | "unified", "split" | "unified", "volatile-defined">;
|
|
38
41
|
graphStyle: Schema<"compact" | "parallel", "compact" | "parallel", "volatile-defined">;
|
|
39
42
|
suggestEnabled: Schema<boolean, boolean, "volatile-defined">;
|
|
43
|
+
suggestMaxBytes: Schema<number, number, "volatile-defined">;
|
|
44
|
+
suggestMaxOutputTokens: Schema<number, number, "volatile-defined">;
|
|
45
|
+
suggestTimeoutMs: Schema<number, number, "volatile-defined">;
|
|
40
46
|
}>>, "plain">;
|
|
41
47
|
private readonly deps;
|
|
42
48
|
private config;
|
package/lib/host/index.js
CHANGED
|
@@ -49,14 +49,14 @@ var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "acce
|
|
|
49
49
|
var _version_dec, _suggest_dec, _query_dec, _run_dec, _snapshot_dec, _a, _init;
|
|
50
50
|
import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
51
51
|
import Schema from "@deepseek-ai/schemastery";
|
|
52
|
-
import { readFile, readdir, realpath, rm, stat } from "node:fs/promises";
|
|
52
|
+
import { lstat, readFile, readdir, readlink, realpath, rm, stat } from "node:fs/promises";
|
|
53
53
|
import { createGitRunner } from "./git.js";
|
|
54
|
-
import { normalizeConfig, snapshotForSession } from "./core.js";
|
|
54
|
+
import { normalizeConfig, snapshotForSession, DEFAULT_CONFIG } from "./core.js";
|
|
55
55
|
import { runAction } from "./actions.js";
|
|
56
56
|
import { runQuery } from "./queries.js";
|
|
57
57
|
import { runSuggest } from "./suggest.js";
|
|
58
58
|
import { checkLatestVersion, readVersionInfo } from "./version.js";
|
|
59
|
-
import { normalizeConfig as normalizeConfig2, DEFAULT_CONFIG, snapshotForSession as snapshotForSession2, resolveWorkspace, parsePrimaryRemote } from "./core.js";
|
|
59
|
+
import { normalizeConfig as normalizeConfig2, DEFAULT_CONFIG as DEFAULT_CONFIG2, snapshotForSession as snapshotForSession2, resolveWorkspace, parsePrimaryRemote } from "./core.js";
|
|
60
60
|
import { createGitRunner as createGitRunner2 } from "./git.js";
|
|
61
61
|
import { parseStatus, parseGraphLog, parseBranches, parseNameStatus, parseStashList, parseTags, sumNumstat } from "./parser.js";
|
|
62
62
|
import { parseRemote, classifyHost } from "./remote.js";
|
|
@@ -83,6 +83,13 @@ class GitPanelService extends (_a = TypertRemoteService, _snapshot_dec = [Remote
|
|
|
83
83
|
realpath,
|
|
84
84
|
stat: async (p) => stat(p),
|
|
85
85
|
readFile,
|
|
86
|
+
symlinkTarget: async (p) => {
|
|
87
|
+
try {
|
|
88
|
+
return (await lstat(p)).isSymbolicLink() ? await readlink(p) : null;
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
},
|
|
86
93
|
readdir: async (p) => (await readdir(p, { withFileTypes: true })).map((e) => ({ name: e.name, isDirectory: e.isDirectory() })),
|
|
87
94
|
remove: async (p) => {
|
|
88
95
|
await rm(p, { force: true });
|
|
@@ -157,10 +164,10 @@ __decorateElement(_init, 1, "version", _version_dec, GitPanelService);
|
|
|
157
164
|
__decoratorMetadata(_init, GitPanelService);
|
|
158
165
|
__publicField(GitPanelService, "inject", ["subprocess", "sessions", "sessionPersistence"]);
|
|
159
166
|
/**
|
|
160
|
-
* Config schema surfaced on the plugin detail page. `showInputPill
|
|
161
|
-
* `defaultDiffView` are `.volatile()`,
|
|
162
|
-
*
|
|
163
|
-
* of the UI form.
|
|
167
|
+
* Config schema surfaced on the plugin detail page. `showInputPill`,
|
|
168
|
+
* `defaultDiffView`, `graphStyle` and the suggest fields are `.volatile()`,
|
|
169
|
+
* so the settings host renders them as live-editable controls; the
|
|
170
|
+
* operational limits stay profile-only and out of the UI form.
|
|
164
171
|
*/
|
|
165
172
|
__publicField(GitPanelService, "Config", Schema.object({
|
|
166
173
|
showInputPill: Schema.boolean().default(true).volatile().description("\u663E\u793A\u8F93\u5165\u6846\u7684 Git \u5206\u652F\u6807\u8BB0"),
|
|
@@ -172,11 +179,16 @@ __publicField(GitPanelService, "Config", Schema.object({
|
|
|
172
179
|
Schema.const("parallel").description("\u5E73\u884C\u7EBF\uFF08VSCode \u98CE\u683C\uFF0C\u5206\u652F\u5404\u5360\u4E00\u5217\uFF09"),
|
|
173
180
|
Schema.const("compact").description("\u7D27\u51D1\uFF08git log \u98CE\u683C\uFF0C\u5171\u4EAB\u7956\u5148\u5408\u5E76\u4E3A\u4E00\u5217\uFF09")
|
|
174
181
|
]).default("parallel").volatile().description("\u63D0\u4EA4\u5386\u53F2\u56FE\u7EBF\u6761\u6837\u5F0F"),
|
|
175
|
-
suggestEnabled: Schema.boolean().default(true).volatile().description("\u63D0\u4EA4\u6846\u663E\u793A AI \u751F\u6210\u6309\u94AE")
|
|
182
|
+
suggestEnabled: Schema.boolean().default(true).volatile().description("\u63D0\u4EA4\u6846\u663E\u793A AI \u751F\u6210\u6309\u94AE"),
|
|
183
|
+
// Defaults come from DEFAULT_CONFIG (which itself reads SUGGEST_LIMITS) —
|
|
184
|
+
// a literal here could silently drift from what normalizeConfig enforces.
|
|
185
|
+
suggestMaxBytes: Schema.number().default(DEFAULT_CONFIG.suggestMaxBytes).volatile().description("AI \u751F\u6210\u8F93\u5165\u9884\u7B97\uFF08diff \u5B57\u8282\u4E0A\u9650\uFF0C16\u2013512 KiB\uFF09"),
|
|
186
|
+
suggestMaxOutputTokens: Schema.number().default(DEFAULT_CONFIG.suggestMaxOutputTokens).volatile().description("AI \u751F\u6210\u8F93\u51FA\u4E0A\u9650\uFF08tokens\uFF0C256\u201316384\uFF09"),
|
|
187
|
+
suggestTimeoutMs: Schema.number().default(DEFAULT_CONFIG.suggestTimeoutMs).volatile().description("AI \u751F\u6210\u8D85\u65F6\uFF08\u6BEB\u79D2\uFF0C5\u2013120 \u79D2\uFF09")
|
|
176
188
|
}));
|
|
177
189
|
var index_default = GitPanelService;
|
|
178
190
|
export {
|
|
179
|
-
DEFAULT_CONFIG,
|
|
191
|
+
DEFAULT_CONFIG2 as DEFAULT_CONFIG,
|
|
180
192
|
GitPanelService,
|
|
181
193
|
checkLatestVersion2 as checkLatestVersion,
|
|
182
194
|
classifyActionFailure,
|
package/lib/host/queries.js
CHANGED
|
@@ -2,7 +2,7 @@ import { join, sep } from "node:path";
|
|
|
2
2
|
import { mapWorkspaceFailure, resolveBrowseRoot, resolveWorkspace, runCommand, snapshotForSession } from "./core.js";
|
|
3
3
|
import { isSafePath, isSafeRev } from "./validate.js";
|
|
4
4
|
import { commitFromFields, parseBranches, parseGraphLog, parseNameStatus, parseStashList, parseTags, sumNumstat } from "./parser.js";
|
|
5
|
-
import { imageMimeFor } from "./types.js";
|
|
5
|
+
import { imageMimeFor, DIFF_CONTEXT_DEFAULT, DIFF_CONTEXT_MAX, DIFF_COUNT_READ_MAX } from "./types.js";
|
|
6
6
|
const GRAPH_FORMAT = "--format=%H%x1f%h%x1f%P%x1f%an%x1f%aI%x1f%D%x1f%s%x1e";
|
|
7
7
|
const VISIBLE_REFS = ["--branches", "--remotes", "--tags", "HEAD"];
|
|
8
8
|
function isHexLike(text) {
|
|
@@ -103,7 +103,7 @@ async function queryHistory(deps, root, q) {
|
|
|
103
103
|
}
|
|
104
104
|
async function queryDiff(deps, root, q) {
|
|
105
105
|
if (!isSafePath(q.path)) return { ok: false, error: { code: "invalid-path", message: q.path } };
|
|
106
|
-
const ctx = q.context !== void 0 && Number.isFinite(q.context) ? Math.max(0, Math.min(
|
|
106
|
+
const ctx = q.context !== void 0 && Number.isFinite(q.context) ? Math.max(0, Math.min(DIFF_CONTEXT_MAX, Math.floor(q.context))) : DIFF_CONTEXT_DEFAULT;
|
|
107
107
|
const unified = `-U${ctx}`;
|
|
108
108
|
let args;
|
|
109
109
|
if (q.base === "staged") {
|
|
@@ -118,41 +118,80 @@ async function queryDiff(deps, root, q) {
|
|
|
118
118
|
if (!("run" in res)) return { ok: false, error: { code: "git-unavailable" } };
|
|
119
119
|
if (res.run.cancelled) return { ok: false, error: { code: "cancelled" } };
|
|
120
120
|
if (res.run.timedOut) return { ok: false, error: { code: "timeout" } };
|
|
121
|
+
let truncated = res.run.stdoutLossy;
|
|
121
122
|
let text = res.run.stdout;
|
|
122
123
|
if (q.base === "worktree" && text.trim() === "") {
|
|
123
124
|
const tracked = await runCommand(deps.run, ["git", "ls-files", "--error-unmatch", "--", q.path], root, "diff-tracked-probe", deps.signal);
|
|
124
125
|
const isUntracked = !("run" in tracked) || tracked.run.exitCode !== 0;
|
|
125
126
|
if (isUntracked) {
|
|
126
127
|
const noIndex = await runCommand(deps.run, ["git", "diff", unified, "--no-index", "--", "/dev/null", q.path], root, "diff-untracked", deps.signal);
|
|
127
|
-
if ("run" in noIndex)
|
|
128
|
+
if ("run" in noIndex) {
|
|
129
|
+
truncated = truncated || noIndex.run.stdoutLossy;
|
|
130
|
+
text = noIndex.run.stdout;
|
|
131
|
+
}
|
|
128
132
|
}
|
|
129
133
|
}
|
|
130
|
-
return {
|
|
134
|
+
return {
|
|
135
|
+
ok: true,
|
|
136
|
+
value: {
|
|
137
|
+
kind: "diff",
|
|
138
|
+
path: q.path,
|
|
139
|
+
text,
|
|
140
|
+
newLines: await countNewSideLines(deps, root, q, text),
|
|
141
|
+
...truncated ? { truncated: true } : {}
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
async function countNewSideLines(deps, root, q, diffText) {
|
|
146
|
+
if (!diffText.includes("@@")) return 0;
|
|
147
|
+
const side = await readNewSide(deps, root, q, { worktreeMaxBytes: DIFF_COUNT_READ_MAX });
|
|
148
|
+
if (!side.ok || side.text === null) return 0;
|
|
149
|
+
return countLines(side.text);
|
|
150
|
+
}
|
|
151
|
+
function countLines(content) {
|
|
152
|
+
if (content === "") return 0;
|
|
153
|
+
let lines = 1;
|
|
154
|
+
for (let at = content.indexOf("\n"); at !== -1; at = content.indexOf("\n", at + 1)) lines++;
|
|
155
|
+
return content.endsWith("\n") ? lines - 1 : lines;
|
|
156
|
+
}
|
|
157
|
+
function splitLines(content) {
|
|
158
|
+
const all = content.split("\n");
|
|
159
|
+
if (all[all.length - 1] === "") all.pop();
|
|
160
|
+
return all;
|
|
131
161
|
}
|
|
132
162
|
async function queryFileLines(deps, root, q) {
|
|
133
163
|
if (!isSafePath(q.path)) return { ok: false, error: { code: "invalid-path", message: q.path } };
|
|
134
164
|
const start = Number.isFinite(q.start) ? Math.max(1, Math.floor(q.start)) : 1;
|
|
135
165
|
const end = Number.isFinite(q.end) ? Math.max(start, Math.floor(q.end)) : start;
|
|
136
|
-
|
|
137
|
-
if (
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
if (res
|
|
147
|
-
if (res.run.
|
|
166
|
+
const side = await readNewSide(deps, root, q);
|
|
167
|
+
if (!side.ok) return { ok: false, error: { code: side.code, message: side.message } };
|
|
168
|
+
return sliceLines(q.path, side.text ?? "", start, end);
|
|
169
|
+
}
|
|
170
|
+
const ABSENT_SIDE_RE = /does not exist in|exists on disk, but not in the index|no such path/i;
|
|
171
|
+
async function readNewSide(deps, root, q, opts) {
|
|
172
|
+
if (q.base === "worktree") return { ok: true, text: await readWorktreeSide(deps, root, q.path, opts?.worktreeMaxBytes) };
|
|
173
|
+
if (q.base === "commit" && !isSafeRev(q.commit)) return { ok: false, code: "invalid-name", message: `unsafe commit: ${q.commit}` };
|
|
174
|
+
const spec = q.base === "commit" ? `${q.commit}:${q.path}` : `:${q.path}`;
|
|
175
|
+
const res = await runCommand(deps.run, ["git", "show", "--end-of-options", spec], root, "new-side", deps.signal);
|
|
176
|
+
if (!("run" in res)) return { ok: false, code: "git-unavailable", message: "git unavailable" };
|
|
177
|
+
if (res.run.cancelled) return { ok: false, code: "cancelled", message: "cancelled" };
|
|
178
|
+
if (res.run.timedOut) return { ok: false, code: "timeout", message: "timed out" };
|
|
148
179
|
if (res.run.exitCode !== 0) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
180
|
+
return ABSENT_SIDE_RE.test(res.run.stderr) ? { ok: true, text: null } : { ok: false, code: "git-error", message: res.run.stderr.trim() || "git show failed" };
|
|
181
|
+
}
|
|
182
|
+
if (res.run.stdoutLossy) return { ok: false, code: "git-error", message: "git show output exceeded the output cap" };
|
|
183
|
+
return { ok: true, text: res.run.stdout };
|
|
184
|
+
}
|
|
185
|
+
async function readWorktreeSide(deps, root, path, maxBytes) {
|
|
186
|
+
const file = join(root, path);
|
|
187
|
+
const link = await deps.fs.symlinkTarget(file);
|
|
188
|
+
if (link !== null) return link;
|
|
189
|
+
if (maxBytes !== void 0) {
|
|
190
|
+
const size = await deps.fs.stat(file).catch(() => null);
|
|
191
|
+
if (size === null || size.size > maxBytes) return null;
|
|
154
192
|
}
|
|
155
|
-
|
|
193
|
+
const text = await readWorktreeText(deps, root, path);
|
|
194
|
+
return text === null ? null : text.replace(/\r\n/g, "\n");
|
|
156
195
|
}
|
|
157
196
|
async function readWorktreeText(deps, root, path) {
|
|
158
197
|
try {
|
|
@@ -167,8 +206,7 @@ async function readWorktreeText(deps, root, path) {
|
|
|
167
206
|
}
|
|
168
207
|
}
|
|
169
208
|
function sliceLines(path, content, start, end) {
|
|
170
|
-
const all = content
|
|
171
|
-
if (all[all.length - 1] === "") all.pop();
|
|
209
|
+
const all = splitLines(content);
|
|
172
210
|
const from = Math.min(start, all.length + 1);
|
|
173
211
|
const to = Math.min(end, all.length);
|
|
174
212
|
const lines = from <= to ? all.slice(from - 1, to) : [];
|
|
@@ -323,7 +361,8 @@ async function queryFileContent(deps, config, root, q) {
|
|
|
323
361
|
return { ok: false, error: { code: "git-error", message: error instanceof Error ? error.message : "stat failed" } };
|
|
324
362
|
}
|
|
325
363
|
const mime = imageMimeFor(q.path);
|
|
326
|
-
const
|
|
364
|
+
const statFields = { size: info.size, mtimeMs: Math.round(info.mtimeMs) };
|
|
365
|
+
const tooLarge = { ok: true, value: { kind: "file-content", path: q.path, variant: mime !== null ? "image" : "text", tooLarge: true, ...statFields } };
|
|
327
366
|
if (info.size > cap) return tooLarge;
|
|
328
367
|
let buf;
|
|
329
368
|
try {
|
|
@@ -333,12 +372,12 @@ async function queryFileContent(deps, config, root, q) {
|
|
|
333
372
|
}
|
|
334
373
|
if (buf.length > cap) return tooLarge;
|
|
335
374
|
if (mime !== null) {
|
|
336
|
-
return { ok: true, value: { kind: "file-content", path: q.path, variant: "image", dataUrl: `data:${mime};base64,${buf.toString("base64")}
|
|
375
|
+
return { ok: true, value: { kind: "file-content", path: q.path, variant: "image", dataUrl: `data:${mime};base64,${buf.toString("base64")}`, ...statFields } };
|
|
337
376
|
}
|
|
338
|
-
if (isBinaryBuffer(buf)) return { ok: true, value: { kind: "file-content", path: q.path, variant: "binary" } };
|
|
377
|
+
if (isBinaryBuffer(buf)) return { ok: true, value: { kind: "file-content", path: q.path, variant: "binary", ...statFields } };
|
|
339
378
|
const content = buf.toString("utf8");
|
|
340
379
|
const lines = content === "" ? 0 : content.split("\n").length - (content.endsWith("\n") ? 1 : 0);
|
|
341
|
-
return { ok: true, value: { kind: "file-content", path: q.path, variant: "text", content, lines } };
|
|
380
|
+
return { ok: true, value: { kind: "file-content", path: q.path, variant: "text", content, lines, ...statFields } };
|
|
342
381
|
}
|
|
343
382
|
function hasGitSegment(path) {
|
|
344
383
|
return path.split(/[\\/]/).includes(".git");
|
package/lib/host/suggest.js
CHANGED
|
@@ -81,6 +81,7 @@ async function collectDiff(deps, root, hasHead, pathArgs) {
|
|
|
81
81
|
const cmds = hasHead ? [["git", "diff", "HEAD", ...pathArgs]] : [["git", "diff", "--cached", ...pathArgs], ["git", "diff", ...pathArgs]];
|
|
82
82
|
const runs = await Promise.all(cmds.map((argv) => runCommand(deps.run, argv, root, "suggest-diff", deps.signal)));
|
|
83
83
|
let diff = "";
|
|
84
|
+
let lossy = false;
|
|
84
85
|
for (const res of runs) {
|
|
85
86
|
if ("failure" in res) return { ok: false, error: { code: "git-unavailable", message: "diff failed" } };
|
|
86
87
|
if (res.run.cancelled) return { ok: false, error: { code: "cancelled" } };
|
|
@@ -88,9 +89,10 @@ async function collectDiff(deps, root, hasHead, pathArgs) {
|
|
|
88
89
|
if (res.run.exitCode !== 0) {
|
|
89
90
|
return { ok: false, error: { code: "git-error", message: res.run.stderr.trim() || "git diff failed" } };
|
|
90
91
|
}
|
|
92
|
+
if (res.run.stdoutLossy) lossy = true;
|
|
91
93
|
diff += res.run.stdout;
|
|
92
94
|
}
|
|
93
|
-
return diff;
|
|
95
|
+
return { diff, lossy };
|
|
94
96
|
}
|
|
95
97
|
async function appendUntracked(deps, root, files, diff, budget) {
|
|
96
98
|
let text = diff;
|
|
@@ -106,7 +108,8 @@ async function appendUntracked(deps, root, files, diff, budget) {
|
|
|
106
108
|
if ("failure" in res || res.run.cancelled || res.run.timedOut) continue;
|
|
107
109
|
const chunk = res.run.stdout;
|
|
108
110
|
if (chunk === "") continue;
|
|
109
|
-
|
|
111
|
+
const lossy = res.run.stdoutLossy;
|
|
112
|
+
if (lossy || Buffer.byteLength(chunk, "utf8") > remaining) {
|
|
110
113
|
text += truncateUtf8(chunk, remaining) + "\n... [diff truncated]";
|
|
111
114
|
truncated = true;
|
|
112
115
|
break;
|
|
@@ -144,12 +147,12 @@ async function runSuggest(deps, config, request) {
|
|
|
144
147
|
const all = statusRes.run.stdoutLossy ? [] : parseStatus(statusRes.run.stdout);
|
|
145
148
|
const files = pathSet.size > 0 ? all.filter((c) => pathSet.has(c.path)) : all;
|
|
146
149
|
const collected = await collectDiff(deps, root, hasHead, pathArgs);
|
|
147
|
-
if (
|
|
148
|
-
if (files.length === 0 && collected.trim() === "") {
|
|
150
|
+
if (!("diff" in collected)) return collected;
|
|
151
|
+
if (files.length === 0 && collected.diff.trim() === "") {
|
|
149
152
|
return { ok: false, error: { code: "empty-diff", message: "no uncommitted changes to summarize" } };
|
|
150
153
|
}
|
|
151
|
-
let diff = collected;
|
|
152
|
-
let truncated =
|
|
154
|
+
let diff = collected.diff;
|
|
155
|
+
let truncated = collected.lossy;
|
|
153
156
|
if (Buffer.byteLength(diff, "utf8") > config.suggestMaxBytes) {
|
|
154
157
|
diff = truncateUtf8(diff, config.suggestMaxBytes) + "\n... [diff truncated]";
|
|
155
158
|
truncated = true;
|
package/lib/host/types.d.ts
CHANGED
|
@@ -9,6 +9,26 @@ export type DiffViewMode = 'unified' | 'split';
|
|
|
9
9
|
* lane (git log --graph); `parallel` = keep each merge's first parent in its
|
|
10
10
|
* own lane, converging only at the ancestor (VSCode / GUI tools). */
|
|
11
11
|
export type GraphStyle = 'compact' | 'parallel';
|
|
12
|
+
/**
|
|
13
|
+
* Diff context (-U) values shared by both halves. `DEFAULT` matches git's own
|
|
14
|
+
* default and is what the host applies when a request omits `context`.
|
|
15
|
+
* `MAX` is the host's clamp ceiling. `EXPAND` is what the "expand all" button
|
|
16
|
+
* asks for: deliberately bounded, because a -U window wide enough to cover a
|
|
17
|
+
* huge file makes the host emit that whole file as one hunk — megabytes of
|
|
18
|
+
* text, and past the runner's stdout cap a silently truncated diff. Whatever
|
|
19
|
+
* the window does not reach stays a collapsed gap the view reveals on demand.
|
|
20
|
+
*/
|
|
21
|
+
export declare const DIFF_CONTEXT_DEFAULT = 3;
|
|
22
|
+
export declare const DIFF_CONTEXT_MAX = 100000;
|
|
23
|
+
export declare const DIFF_EXPAND_CONTEXT = 2000;
|
|
24
|
+
/**
|
|
25
|
+
* Ceiling for the working-file read taken only to count the diff's new-side
|
|
26
|
+
* lines. That count decides whether hidden context follows the last hunk, so it
|
|
27
|
+
* is read on every diff query — including the changes tab's poll while a diff is
|
|
28
|
+
* open — while `file-lines` (the on-demand expansion) stays uncapped. Above this
|
|
29
|
+
* size the count reports 0 (unknown) and no trailing control is offered.
|
|
30
|
+
*/
|
|
31
|
+
export declare const DIFF_COUNT_READ_MAX: number;
|
|
12
32
|
/** Wire request: the browser sends only session identity, never a path. */
|
|
13
33
|
export interface GitSnapshotRequest {
|
|
14
34
|
readonly sessionId: string;
|
|
@@ -145,6 +165,8 @@ export type GitAction = {
|
|
|
145
165
|
} | {
|
|
146
166
|
readonly kind: 'branch-checkout';
|
|
147
167
|
readonly name: string;
|
|
168
|
+
readonly createFrom?: string;
|
|
169
|
+
readonly stashMessage?: string;
|
|
148
170
|
} | {
|
|
149
171
|
readonly kind: 'fetch';
|
|
150
172
|
} | {
|
|
@@ -188,7 +210,7 @@ export type GitAction = {
|
|
|
188
210
|
/** `git reset` modes, from least to most destructive. Single source so the
|
|
189
211
|
* client dialog and the command builder cannot drift. */
|
|
190
212
|
export type ResetMode = 'soft' | 'mixed' | 'hard';
|
|
191
|
-
export type GitErrorCode = 'cwd-unavailable' | 'not-a-git-repo' | 'git-unavailable' | 'invalid-path' | 'invalid-name' | 'invalid-index' | 'git-error' | 'timeout' | 'cancelled' | 'empty-message' | 'local-changes-block' | 'not-ff' | 'push-rejected' | 'auth-failed' | 'conflict' | 'revert-conflict' | 'revert-stuck' | 'revert-merge' | 'not-found' | 'index-busy' | 'empty-diff' | 'llm-unavailable' | 'llm-error' | 'llm-output' | 'suggest-disabled';
|
|
213
|
+
export type GitErrorCode = 'cwd-unavailable' | 'not-a-git-repo' | 'git-unavailable' | 'invalid-path' | 'invalid-name' | 'invalid-index' | 'git-error' | 'timeout' | 'cancelled' | 'empty-message' | 'local-changes-block' | 'not-ff' | 'push-rejected' | 'auth-failed' | 'conflict' | 'revert-conflict' | 'revert-stuck' | 'revert-merge' | 'not-found' | 'branch-in-use' | 'stash-switch-failed' | 'index-busy' | 'empty-diff' | 'llm-unavailable' | 'llm-error' | 'llm-output' | 'suggest-disabled';
|
|
192
214
|
export type GitActionResult = {
|
|
193
215
|
readonly ok: true;
|
|
194
216
|
readonly snapshot: GitSnapshot;
|
|
@@ -335,10 +357,32 @@ export type GitQueryResult = {
|
|
|
335
357
|
readonly kind: 'history';
|
|
336
358
|
readonly commits: readonly GraphCommit[];
|
|
337
359
|
readonly total: number;
|
|
338
|
-
}
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Unified diff text plus the new-side line count. The count is the fact the
|
|
363
|
+
* view needs to decide whether hidden context can follow the LAST hunk: git
|
|
364
|
+
* caps trailing context at -U, so a full trailing run is ambiguous from the
|
|
365
|
+
* text alone (it may be mid-file, or the file may end exactly there), while
|
|
366
|
+
* `newLines` makes it provable.
|
|
367
|
+
*
|
|
368
|
+
* 0 means "no trailing control is possible, or the count is not trustworthy":
|
|
369
|
+
* the side does not exist (a deleted file), the read failed or was truncated,
|
|
370
|
+
* the diff has no hunk at all, or the working file is above
|
|
371
|
+
* DIFF_COUNT_READ_MAX. The view treats all of those the same way — no
|
|
372
|
+
* trailing gap rather than a speculative one.
|
|
373
|
+
*
|
|
374
|
+
* `truncated` marks the one case where the text itself is incomplete: the
|
|
375
|
+
* runner dropped output at its collect/spill ceiling, so `text` is a prefix
|
|
376
|
+
* that still parses like a real diff. It is flagged rather than refused —
|
|
377
|
+
* the partial hunks stay readable — but the view must say so, because line
|
|
378
|
+
* numbers and +/- counts past the cut are simply wrong.
|
|
379
|
+
*/
|
|
380
|
+
| {
|
|
339
381
|
readonly kind: 'diff';
|
|
340
382
|
readonly path: string;
|
|
341
383
|
readonly text: string;
|
|
384
|
+
readonly newLines: number;
|
|
385
|
+
readonly truncated?: true;
|
|
342
386
|
} | {
|
|
343
387
|
readonly kind: 'file-lines';
|
|
344
388
|
readonly path: string;
|
|
@@ -429,6 +473,10 @@ export type GitQueryResult = {
|
|
|
429
473
|
readonly dataUrl?: string;
|
|
430
474
|
/** The file exceeded the byte cap, so no content is returned. */
|
|
431
475
|
readonly tooLarge?: true;
|
|
476
|
+
/** Byte size from the stat the read already performs (all variants). */
|
|
477
|
+
readonly size?: number;
|
|
478
|
+
/** Modification time (epoch ms) from the same stat (all variants). */
|
|
479
|
+
readonly mtimeMs?: number;
|
|
432
480
|
};
|
|
433
481
|
export type GitQueryResponse = {
|
|
434
482
|
readonly ok: true;
|
|
@@ -444,6 +492,31 @@ export interface GitQueryRequest {
|
|
|
444
492
|
readonly sessionId: string;
|
|
445
493
|
readonly query: GitQuery;
|
|
446
494
|
}
|
|
495
|
+
/**
|
|
496
|
+
* Suggest-limit ranges and defaults, in host storage units (bytes / tokens /
|
|
497
|
+
* ms) — the single source shared by both plugin halves (the client bundle
|
|
498
|
+
* imports this value, like imageMimeFor below): the host's DEFAULT_CONFIG and
|
|
499
|
+
* normalizeConfig clamp every read into [min, max], and the detail-page form
|
|
500
|
+
* derives its display units from it (the /1024 and /1000 divisions are exact
|
|
501
|
+
* and guarded by test).
|
|
502
|
+
*/
|
|
503
|
+
export declare const SUGGEST_LIMITS: {
|
|
504
|
+
readonly maxBytes: {
|
|
505
|
+
readonly min: number;
|
|
506
|
+
readonly max: number;
|
|
507
|
+
readonly default: number;
|
|
508
|
+
};
|
|
509
|
+
readonly maxOutputTokens: {
|
|
510
|
+
readonly min: 256;
|
|
511
|
+
readonly max: 16384;
|
|
512
|
+
readonly default: 4096;
|
|
513
|
+
};
|
|
514
|
+
readonly timeoutMs: {
|
|
515
|
+
readonly min: 5000;
|
|
516
|
+
readonly max: 120000;
|
|
517
|
+
readonly default: 15000;
|
|
518
|
+
};
|
|
519
|
+
};
|
|
447
520
|
/**
|
|
448
521
|
* Wire request for the suggest endpoint: ask the configured model to write a
|
|
449
522
|
* commit message from the repository's uncommitted changes. `paths` restricts
|
|
@@ -462,7 +535,8 @@ export type GitSuggestResult = {
|
|
|
462
535
|
/** Actual provider/model route used (display only). */
|
|
463
536
|
readonly provider?: string;
|
|
464
537
|
readonly model?: string;
|
|
465
|
-
/** The diff
|
|
538
|
+
/** The diff reached the model truncated: over the input cap, or cut
|
|
539
|
+
* by the per-command runner stdout cap before it. */
|
|
466
540
|
readonly truncated?: boolean;
|
|
467
541
|
};
|
|
468
542
|
} | {
|
package/lib/host/types.js
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
const DIFF_CONTEXT_DEFAULT = 3;
|
|
2
|
+
const DIFF_CONTEXT_MAX = 1e5;
|
|
3
|
+
const DIFF_EXPAND_CONTEXT = 2e3;
|
|
4
|
+
const DIFF_COUNT_READ_MAX = 8 * 1024 * 1024;
|
|
5
|
+
const SUGGEST_LIMITS = {
|
|
6
|
+
maxBytes: { min: 16 * 1024, max: 512 * 1024, default: 128 * 1024 },
|
|
7
|
+
maxOutputTokens: { min: 256, max: 16384, default: 4096 },
|
|
8
|
+
timeoutMs: { min: 5e3, max: 12e4, default: 15e3 }
|
|
9
|
+
};
|
|
1
10
|
const IMAGE_MIME = {
|
|
2
11
|
png: "image/png",
|
|
3
12
|
jpg: "image/jpeg",
|
|
@@ -17,6 +26,11 @@ function imageMimeFor(path) {
|
|
|
17
26
|
return IMAGE_MIME[path.slice(dot + 1).toLowerCase()] ?? null;
|
|
18
27
|
}
|
|
19
28
|
export {
|
|
29
|
+
DIFF_CONTEXT_DEFAULT,
|
|
30
|
+
DIFF_CONTEXT_MAX,
|
|
31
|
+
DIFF_COUNT_READ_MAX,
|
|
32
|
+
DIFF_EXPAND_CONTEXT,
|
|
20
33
|
IMAGE_MIME,
|
|
34
|
+
SUGGEST_LIMITS,
|
|
21
35
|
imageMimeFor
|
|
22
36
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xbzbing/dsh-git-panel",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.1",
|
|
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",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"build": "node build.mjs",
|
|
44
44
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
45
45
|
"test:unit": "node build.mjs && node --test --test-force-exit test/unit/*.test.mjs",
|
|
46
|
-
"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
|
+
"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 && node test/e2e/run4.mjs && node test/e2e/run5.mjs",
|
|
47
47
|
"test": "npm run test:unit && npm run test:e2e",
|
|
48
48
|
"check": "npm run typecheck && npm run test && git diff --exit-code -- lib/"
|
|
49
49
|
},
|