@xbzbing/dsh-git-panel 1.2.0 → 1.4.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 +9 -2
- package/README_EN.md +9 -2
- package/lib/client.js +164 -15
- package/lib/host/actions.d.ts +19 -0
- package/lib/host/core.d.ts +24 -1
- package/lib/host/index.d.ts +10 -4
- package/lib/host/index.js +393 -27
- package/lib/host/llm-face.d.ts +27 -0
- package/lib/host/parser.d.ts +9 -1
- package/lib/host/suggest.d.ts +4 -0
- package/lib/host/types.d.ts +95 -1
- package/package.json +2 -2
package/lib/host/index.js
CHANGED
|
@@ -250,6 +250,25 @@ function parseTags(stdout) {
|
|
|
250
250
|
}
|
|
251
251
|
return out;
|
|
252
252
|
}
|
|
253
|
+
function parseStashList(stdout) {
|
|
254
|
+
const out = [];
|
|
255
|
+
for (const record of stdout.split("\0")) {
|
|
256
|
+
if (record.trim() === "") continue;
|
|
257
|
+
const [selector = "", sha = "", subject = "", relTime = ""] = record.split("");
|
|
258
|
+
const idxMatch = /stash@\{(\d+)\}/.exec(selector);
|
|
259
|
+
if (idxMatch === null) continue;
|
|
260
|
+
const index = Number(idxMatch[1]);
|
|
261
|
+
let branch = null;
|
|
262
|
+
let message = subject;
|
|
263
|
+
const m = /^(?:WIP on|On) (.+?): (.*)$/s.exec(subject);
|
|
264
|
+
if (m !== null) {
|
|
265
|
+
branch = m[1] === "(no branch)" ? null : m[1] ?? null;
|
|
266
|
+
message = m[2] ?? "";
|
|
267
|
+
}
|
|
268
|
+
out.push({ index, sha: sha.trim(), message: message.trim(), branch, relTime: relTime.trim() });
|
|
269
|
+
}
|
|
270
|
+
return out;
|
|
271
|
+
}
|
|
253
272
|
function parseNameStatus(stdout) {
|
|
254
273
|
const out = [];
|
|
255
274
|
const tokens = stdout.split("\0");
|
|
@@ -317,18 +336,32 @@ var DEFAULT_CONFIG = {
|
|
|
317
336
|
maxChanges: 1e3,
|
|
318
337
|
refreshIntervalMs: 3e4,
|
|
319
338
|
showInputPill: true,
|
|
320
|
-
defaultDiffView: "unified"
|
|
339
|
+
defaultDiffView: "unified",
|
|
340
|
+
graphStyle: "parallel",
|
|
341
|
+
suggestEnabled: true,
|
|
342
|
+
suggestMaxBytes: 64 * 1024,
|
|
343
|
+
suggestMaxOutputTokens: 200,
|
|
344
|
+
suggestTimeoutMs: 15e3
|
|
321
345
|
};
|
|
322
346
|
function normalizeConfig(raw) {
|
|
323
347
|
const c = raw ?? {};
|
|
324
348
|
const num = (v, d) => typeof v === "number" && Number.isFinite(v) && v >= 1 ? Math.floor(v) : d;
|
|
349
|
+
const hasProvider = typeof c.suggestProvider === "string" && c.suggestProvider !== "";
|
|
350
|
+
const hasModel = typeof c.suggestModel === "string" && c.suggestModel !== "";
|
|
351
|
+
const route = hasProvider && hasModel ? { suggestProvider: c.suggestProvider, suggestModel: c.suggestModel } : {};
|
|
325
352
|
return {
|
|
326
353
|
timeoutMs: num(c.timeoutMs, DEFAULT_CONFIG.timeoutMs),
|
|
327
354
|
maxBytes: num(c.maxBytes, DEFAULT_CONFIG.maxBytes),
|
|
328
355
|
maxChanges: num(c.maxChanges, DEFAULT_CONFIG.maxChanges),
|
|
329
356
|
refreshIntervalMs: num(c.refreshIntervalMs, DEFAULT_CONFIG.refreshIntervalMs),
|
|
330
357
|
showInputPill: readBool(c.showInputPill, DEFAULT_CONFIG.showInputPill),
|
|
331
|
-
defaultDiffView: readDiffView(c.defaultDiffView, DEFAULT_CONFIG.defaultDiffView)
|
|
358
|
+
defaultDiffView: readDiffView(c.defaultDiffView, DEFAULT_CONFIG.defaultDiffView),
|
|
359
|
+
graphStyle: readGraphStyle(c.graphStyle, DEFAULT_CONFIG.graphStyle),
|
|
360
|
+
suggestEnabled: readBool(c.suggestEnabled, DEFAULT_CONFIG.suggestEnabled),
|
|
361
|
+
suggestMaxBytes: num(c.suggestMaxBytes, DEFAULT_CONFIG.suggestMaxBytes),
|
|
362
|
+
suggestMaxOutputTokens: num(c.suggestMaxOutputTokens, DEFAULT_CONFIG.suggestMaxOutputTokens),
|
|
363
|
+
suggestTimeoutMs: num(c.suggestTimeoutMs, DEFAULT_CONFIG.suggestTimeoutMs),
|
|
364
|
+
...route
|
|
332
365
|
};
|
|
333
366
|
}
|
|
334
367
|
function unwrapVolatile(value) {
|
|
@@ -338,11 +371,16 @@ function readDiffView(value, fallback) {
|
|
|
338
371
|
const raw = unwrapVolatile(value);
|
|
339
372
|
return raw === "unified" || raw === "split" ? raw : fallback;
|
|
340
373
|
}
|
|
374
|
+
function readGraphStyle(value, fallback) {
|
|
375
|
+
const raw = unwrapVolatile(value);
|
|
376
|
+
return raw === "parallel" || raw === "compact" ? raw : fallback;
|
|
377
|
+
}
|
|
341
378
|
function readBool(value, fallback) {
|
|
342
379
|
const raw = unwrapVolatile(value);
|
|
343
380
|
return typeof raw === "boolean" ? raw : fallback;
|
|
344
381
|
}
|
|
345
382
|
var NEG_CACHE_MS = 15e3;
|
|
383
|
+
var STATS_FANOUT_LIMIT = 200;
|
|
346
384
|
async function resolveBrowseRoot(deps, sessionId) {
|
|
347
385
|
const ws = await resolveWorkspace(deps, sessionId);
|
|
348
386
|
if (ws.ok) return { ok: true, root: ws.root, isGitRepo: true };
|
|
@@ -487,16 +525,19 @@ async function snapshotForSession(deps, config, sessionId) {
|
|
|
487
525
|
}
|
|
488
526
|
const wt = "run" in worktreeNumRes && worktreeNumRes.run.exitCode === 0 ? sumNumstat(worktreeNumRes.run.stdout) : { insertions: 0, deletions: 0 };
|
|
489
527
|
const stg = "run" in stagedNumRes && stagedNumRes.run.exitCode === 0 ? sumNumstat(stagedNumRes.run.stdout) : { insertions: 0, deletions: 0 };
|
|
528
|
+
const statsDetailed = allChanges.length <= STATS_FANOUT_LIMIT;
|
|
490
529
|
let untrackedInsertions = 0;
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
530
|
+
if (statsDetailed) {
|
|
531
|
+
const untrackedPaths = allChanges.filter((c) => c.status === "untracked" && !c.isDirectory).map((c) => c.path);
|
|
532
|
+
const safeUntracked = untrackedPaths.filter(isSafePath);
|
|
533
|
+
if (safeUntracked.length > 0) {
|
|
534
|
+
const perFile = await Promise.all(
|
|
535
|
+
safeUntracked.map((p) => runCommand(deps.run, ["git", "diff", "--numstat", "--no-index", "--", "/dev/null", p], root, "numstat-untracked", deps.signal))
|
|
536
|
+
);
|
|
537
|
+
for (const r of perFile) if ("run" in r) untrackedInsertions += sumNumstat(r.run.stdout).insertions;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
const lastChangeAt = statsDetailed ? await maxChangeMtime(deps, root, allChanges) : null;
|
|
500
541
|
const distinct = new Set(allChanges.map((c) => c.path));
|
|
501
542
|
const stats = {
|
|
502
543
|
fileCount: distinct.size,
|
|
@@ -506,7 +547,8 @@ async function snapshotForSession(deps, config, sessionId) {
|
|
|
506
547
|
insertions: wt.insertions + stg.insertions + untrackedInsertions,
|
|
507
548
|
deletions: wt.deletions + stg.deletions,
|
|
508
549
|
lastChangeAt,
|
|
509
|
-
headCommittedAt: lastCommit?.dateIso ?? null
|
|
550
|
+
headCommittedAt: lastCommit?.dateIso ?? null,
|
|
551
|
+
partial: !statsDetailed
|
|
510
552
|
};
|
|
511
553
|
const snapshot = {
|
|
512
554
|
root,
|
|
@@ -526,6 +568,8 @@ async function snapshotForSession(deps, config, sessionId) {
|
|
|
526
568
|
refreshIntervalMs: config.refreshIntervalMs,
|
|
527
569
|
showInputPill: config.showInputPill,
|
|
528
570
|
defaultDiffView: config.defaultDiffView,
|
|
571
|
+
graphStyle: config.graphStyle,
|
|
572
|
+
suggestEnabled: config.suggestEnabled,
|
|
529
573
|
checkedAt: Date.now()
|
|
530
574
|
};
|
|
531
575
|
return { ok: true, value: snapshot };
|
|
@@ -586,14 +630,86 @@ function planAction(action, unborn) {
|
|
|
586
630
|
return { argv: [["git", "checkout", "--end-of-options", action.name]] };
|
|
587
631
|
case "fetch":
|
|
588
632
|
return { argv: [["git", "fetch", "--all", "--prune"]] };
|
|
633
|
+
case "tag-create": {
|
|
634
|
+
if (!isSafeBranchName(action.name)) return { error: "invalid-name", message: `unsafe tag name: ${action.name}` };
|
|
635
|
+
if (!isSafeRev(action.commit)) return { error: "invalid-name", message: `unsafe commit: ${action.commit}` };
|
|
636
|
+
const msg = action.message?.trim() ?? "";
|
|
637
|
+
const flags = msg === "" ? [] : ["-a", "-m", msg];
|
|
638
|
+
return { argv: [["git", "tag", ...flags, "--end-of-options", action.name, action.commit]] };
|
|
639
|
+
}
|
|
640
|
+
case "tag-delete":
|
|
641
|
+
if (!isSafeBranchName(action.name)) return { error: "invalid-name", message: `unsafe tag name: ${action.name}` };
|
|
642
|
+
return { argv: [["git", "tag", "-d", "--end-of-options", action.name]] };
|
|
643
|
+
case "stash-push": {
|
|
644
|
+
const msg = action.message?.trim() ?? "";
|
|
645
|
+
return { argv: [msg === "" ? ["git", "stash", "push"] : ["git", "stash", "push", "-m", msg]] };
|
|
646
|
+
}
|
|
647
|
+
case "stash-apply":
|
|
648
|
+
case "stash-pop":
|
|
649
|
+
case "stash-drop": {
|
|
650
|
+
if (!Number.isInteger(action.index) || action.index < 0) {
|
|
651
|
+
return { error: "invalid-index", message: `invalid stash index: ${action.index}` };
|
|
652
|
+
}
|
|
653
|
+
const verb = action.kind === "stash-apply" ? "apply" : action.kind === "stash-pop" ? "pop" : "drop";
|
|
654
|
+
return { argv: [["git", "stash", verb, "--end-of-options", `stash@{${action.index}}`]] };
|
|
655
|
+
}
|
|
656
|
+
case "revert": {
|
|
657
|
+
if (!isSafeRev(action.commit)) return { error: "invalid-name", message: `unsafe commit: ${action.commit}` };
|
|
658
|
+
return { argv: [["git", "revert", "--no-edit", "--end-of-options", action.commit]] };
|
|
659
|
+
}
|
|
660
|
+
case "reset": {
|
|
661
|
+
if (!isSafeRev(action.commit)) return { error: "invalid-name", message: `unsafe commit: ${action.commit}` };
|
|
662
|
+
if (action.mode !== "soft" && action.mode !== "mixed" && action.mode !== "hard") {
|
|
663
|
+
return { error: "invalid-name", message: `invalid reset mode: ${String(action.mode)}` };
|
|
664
|
+
}
|
|
665
|
+
return { argv: [["git", "reset", `--${action.mode}`, "--end-of-options", action.commit]] };
|
|
666
|
+
}
|
|
589
667
|
}
|
|
590
668
|
}
|
|
669
|
+
function isIndexBusy(outcome) {
|
|
670
|
+
if (!("run" in outcome) || outcome.run.exitCode === 0) return false;
|
|
671
|
+
return /index\.lock|Unable to create.*index|another git process/i.test(outcome.run.stderr + outcome.run.stdout);
|
|
672
|
+
}
|
|
673
|
+
function classifyActionFailure(stdout, stderr, exitCode) {
|
|
674
|
+
const combined = stderr + stdout;
|
|
675
|
+
const err = stderr.trim();
|
|
676
|
+
if (/CONFLICT|Merge conflict|needs merge|could not restore untracked/i.test(combined)) {
|
|
677
|
+
return { code: "conflict", message: err || "merge conflict" };
|
|
678
|
+
}
|
|
679
|
+
if (/index\.lock|Unable to create.*index|another git process/i.test(combined)) {
|
|
680
|
+
return { code: "index-busy", message: err };
|
|
681
|
+
}
|
|
682
|
+
if (/nothing to commit|no changes added/i.test(combined)) {
|
|
683
|
+
return { code: "git-error", message: err || "nothing to commit" };
|
|
684
|
+
}
|
|
685
|
+
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)) {
|
|
686
|
+
return { code: "not-found", message: err || "not found" };
|
|
687
|
+
}
|
|
688
|
+
if (/would be overwritten by (checkout|merge)|local changes|overwritten by merge|Your local changes/i.test(stderr)) {
|
|
689
|
+
return { code: "local-changes-block", message: err };
|
|
690
|
+
}
|
|
691
|
+
if (/is a merge but no -m|mainline parent/i.test(combined)) {
|
|
692
|
+
return { code: "revert-merge", message: err || "cannot revert a merge commit from the panel" };
|
|
693
|
+
}
|
|
694
|
+
return { code: "git-error", message: err || `git exited ${exitCode}` };
|
|
695
|
+
}
|
|
591
696
|
async function runAction(deps, config, request) {
|
|
592
697
|
const workspace = await resolveWorkspace(deps, request.sessionId);
|
|
593
698
|
if (!workspace.ok) return { ok: false, error: mapWorkspaceFailure(workspace.failure) };
|
|
594
699
|
const root = workspace.root;
|
|
595
700
|
const headProbe = await runCommand(deps.run, ["git", "rev-parse", "--verify", "HEAD"], root, "head-probe", deps.signal);
|
|
596
701
|
const unborn = !("run" in headProbe) || headProbe.run.exitCode !== 0;
|
|
702
|
+
const act = request.action;
|
|
703
|
+
if (act.kind === "stash-apply" || act.kind === "stash-pop" || act.kind === "stash-drop") {
|
|
704
|
+
if (!Number.isInteger(act.index) || act.index < 0) {
|
|
705
|
+
return { ok: false, error: { code: "invalid-index", message: `invalid stash index: ${act.index}` } };
|
|
706
|
+
}
|
|
707
|
+
const probe = await runCommand(deps.run, ["git", "rev-parse", "--verify", "--quiet", "--end-of-options", `stash@{${act.index}}`], root, "stash-verify", deps.signal);
|
|
708
|
+
const sha = "run" in probe && probe.run.exitCode === 0 ? probe.run.stdout.trim() : "";
|
|
709
|
+
if (sha === "" || sha !== act.sha) {
|
|
710
|
+
return { ok: false, error: { code: "not-found", message: "stash entry changed; refresh and retry" } };
|
|
711
|
+
}
|
|
712
|
+
}
|
|
597
713
|
if (request.action.kind === "discard") {
|
|
598
714
|
const removed = await discardUntracked(deps, config, root, request.sessionId, request.action.paths);
|
|
599
715
|
if (removed !== null) {
|
|
@@ -611,7 +727,11 @@ async function runAction(deps, config, request) {
|
|
|
611
727
|
let lastOutput = "";
|
|
612
728
|
for (let step = 0; step < plan.argv.length; step += 1) {
|
|
613
729
|
const argv = plan.argv[step];
|
|
614
|
-
|
|
730
|
+
let outcome = await runCommand(deps.run, argv, root, "action", deps.signal);
|
|
731
|
+
if (isIndexBusy(outcome)) {
|
|
732
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
733
|
+
outcome = await runCommand(deps.run, argv, root, "action-retry", deps.signal);
|
|
734
|
+
}
|
|
615
735
|
const where = plan.argv.length > 1 ? ` (step ${step + 1}/${plan.argv.length}: ${argv.join(" ")})` : "";
|
|
616
736
|
if ("failure" in outcome) {
|
|
617
737
|
const message = outcome.failure instanceof Error ? outcome.failure.message : String(outcome.failure);
|
|
@@ -621,14 +741,20 @@ async function runAction(deps, config, request) {
|
|
|
621
741
|
if (outcome.run.timedOut) return { ok: false, error: { code: "timeout" } };
|
|
622
742
|
lastOutput = outcome.run.stdout || outcome.run.stderr;
|
|
623
743
|
if (outcome.run.exitCode !== 0) {
|
|
624
|
-
const
|
|
625
|
-
if (
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
744
|
+
const failure = classifyActionFailure(outcome.run.stdout, outcome.run.stderr, outcome.run.exitCode ?? -1);
|
|
745
|
+
if (request.action.kind === "revert" && failure.code === "conflict") {
|
|
746
|
+
let abort = await runCommand(deps.run, ["git", "revert", "--abort"], root, "revert-abort", deps.signal);
|
|
747
|
+
if (isIndexBusy(abort)) {
|
|
748
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
749
|
+
abort = await runCommand(deps.run, ["git", "revert", "--abort"], root, "revert-abort-retry", deps.signal);
|
|
750
|
+
}
|
|
751
|
+
const aborted = "run" in abort && abort.run.exitCode === 0;
|
|
752
|
+
return aborted ? { ok: false, error: { code: "revert-conflict", message: failure.message + where } } : { ok: false, error: { code: "revert-stuck", message: failure.message + where } };
|
|
630
753
|
}
|
|
631
|
-
return { ok: false, error: { code:
|
|
754
|
+
return { ok: false, error: { code: failure.code, message: failure.message + where } };
|
|
755
|
+
}
|
|
756
|
+
if (request.action.kind === "stash-push" && /No local changes to save/i.test(outcome.run.stdout + outcome.run.stderr)) {
|
|
757
|
+
return { ok: false, error: { code: "git-error", message: (outcome.run.stdout.trim() || "No local changes to save") + where } };
|
|
632
758
|
}
|
|
633
759
|
}
|
|
634
760
|
const snapshot = await snapshotForSession(deps, config, request.sessionId);
|
|
@@ -693,6 +819,7 @@ function imageMimeFor(path) {
|
|
|
693
819
|
|
|
694
820
|
// src/host/queries.ts
|
|
695
821
|
var GRAPH_FORMAT = "--format=%H%x1f%h%x1f%P%x1f%an%x1f%aI%x1f%D%x1f%s%x1e";
|
|
822
|
+
var VISIBLE_REFS = ["--branches", "--remotes", "--tags", "HEAD"];
|
|
696
823
|
function isHexLike(text) {
|
|
697
824
|
return /^[0-9a-fA-F]{7,40}$/.test(text.trim());
|
|
698
825
|
}
|
|
@@ -726,6 +853,8 @@ async function runQuery(deps, config, request) {
|
|
|
726
853
|
return await queryBranches(deps, root);
|
|
727
854
|
case "tags":
|
|
728
855
|
return await queryTags(deps, root);
|
|
856
|
+
case "stash-list":
|
|
857
|
+
return await queryStashList(deps, root);
|
|
729
858
|
case "authors":
|
|
730
859
|
return await queryAuthors(deps, root);
|
|
731
860
|
case "last-commit-message":
|
|
@@ -741,7 +870,7 @@ async function runQuery(deps, config, request) {
|
|
|
741
870
|
async function queryHistory(deps, root, q) {
|
|
742
871
|
const limit = Number.isFinite(q.limit) ? Math.min(500, Math.max(1, Math.trunc(q.limit))) : 100;
|
|
743
872
|
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}`];
|
|
873
|
+
const args = ["git", "log", "--topo-order", GRAPH_FORMAT, `--max-count=${limit}`, `--skip=${skip}`];
|
|
745
874
|
const search = q.search?.trim() ?? "";
|
|
746
875
|
const hexJump = search !== "" && isHexLike(search);
|
|
747
876
|
const countArgs = ["git", "rev-list", "--count"];
|
|
@@ -754,7 +883,7 @@ async function queryHistory(deps, root, q) {
|
|
|
754
883
|
if (!isSafeRev(q.ref)) return { ok: false, error: { code: "invalid-name", message: `unsafe ref: ${q.ref}` } };
|
|
755
884
|
filters.push("--end-of-options", q.ref);
|
|
756
885
|
} else {
|
|
757
|
-
filters.push(
|
|
886
|
+
filters.push(...VISIBLE_REFS);
|
|
758
887
|
}
|
|
759
888
|
args.push(...filters);
|
|
760
889
|
countArgs.push(...filters);
|
|
@@ -1082,8 +1211,13 @@ async function queryTags(deps, root) {
|
|
|
1082
1211
|
const tags = "run" in res && res.run.exitCode === 0 ? parseTags(res.run.stdout) : [];
|
|
1083
1212
|
return { ok: true, value: { kind: "tags", tags } };
|
|
1084
1213
|
}
|
|
1214
|
+
async function queryStashList(deps, root) {
|
|
1215
|
+
const res = await runCommand(deps.run, ["git", "stash", "list", "-z", "--format=%gd%x1f%H%x1f%gs%x1f%cr"], root, "stash-list", deps.signal);
|
|
1216
|
+
const entries = "run" in res && res.run.exitCode === 0 ? parseStashList(res.run.stdout) : [];
|
|
1217
|
+
return { ok: true, value: { kind: "stash-list", entries } };
|
|
1218
|
+
}
|
|
1085
1219
|
async function queryAuthors(deps, root) {
|
|
1086
|
-
const res = await runCommand(deps.run, ["git", "log",
|
|
1220
|
+
const res = await runCommand(deps.run, ["git", "log", ...VISIBLE_REFS, "--format=%an", "--max-count=2000"], root, "authors", deps.signal);
|
|
1087
1221
|
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
1222
|
return { ok: true, value: { kind: "authors", authors } };
|
|
1089
1223
|
}
|
|
@@ -1098,6 +1232,219 @@ async function queryWorktreeStats(deps, config, sessionId) {
|
|
|
1098
1232
|
return { ok: true, value: { kind: "worktree-stats", stats: snapshot.value.stats } };
|
|
1099
1233
|
}
|
|
1100
1234
|
|
|
1235
|
+
// src/host/suggest.ts
|
|
1236
|
+
var SYSTEM_PROMPT = [
|
|
1237
|
+
"You are an expert at writing concise, accurate git commit messages.",
|
|
1238
|
+
"Given a repository\u2019s uncommitted changes, write ONE commit message for them.",
|
|
1239
|
+
"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.",
|
|
1240
|
+
"Return only the message text: no quotes, code fences, prefixes, bullet lists, or explanations.",
|
|
1241
|
+
"Match the language and style of the recent commit message when one is provided; otherwise write in English.",
|
|
1242
|
+
"Focus on the most important change; do not mechanically list every file."
|
|
1243
|
+
].join("\n");
|
|
1244
|
+
var UNTRACKED_FILE_CAP = 16;
|
|
1245
|
+
function resolveRoute(config, agentDefaultModel) {
|
|
1246
|
+
if (config.suggestProvider !== void 0 && config.suggestModel !== void 0) {
|
|
1247
|
+
return { provider: config.suggestProvider, model: config.suggestModel };
|
|
1248
|
+
}
|
|
1249
|
+
return agentDefaultModel?.currentSelection();
|
|
1250
|
+
}
|
|
1251
|
+
function withDeadline(timeoutMs, parent) {
|
|
1252
|
+
const controller = new AbortController();
|
|
1253
|
+
let timedOut = false;
|
|
1254
|
+
const timer = setTimeout(() => {
|
|
1255
|
+
timedOut = true;
|
|
1256
|
+
controller.abort();
|
|
1257
|
+
}, timeoutMs);
|
|
1258
|
+
const onAbort = () => {
|
|
1259
|
+
controller.abort();
|
|
1260
|
+
};
|
|
1261
|
+
if (parent !== void 0) {
|
|
1262
|
+
if (parent.aborted) {
|
|
1263
|
+
controller.abort();
|
|
1264
|
+
} else parent.addEventListener("abort", onAbort, { once: true });
|
|
1265
|
+
}
|
|
1266
|
+
return {
|
|
1267
|
+
signal: controller.signal,
|
|
1268
|
+
expired: () => timedOut,
|
|
1269
|
+
dispose: () => {
|
|
1270
|
+
clearTimeout(timer);
|
|
1271
|
+
parent?.removeEventListener("abort", onAbort);
|
|
1272
|
+
}
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
function truncateUtf8(text, maxBytes) {
|
|
1276
|
+
if (Buffer.byteLength(text, "utf8") <= maxBytes) return text;
|
|
1277
|
+
let lo = 0;
|
|
1278
|
+
let hi = text.length;
|
|
1279
|
+
while (lo < hi) {
|
|
1280
|
+
const mid = lo + hi + 1 >> 1;
|
|
1281
|
+
if (Buffer.byteLength(text.slice(0, mid), "utf8") <= maxBytes) lo = mid;
|
|
1282
|
+
else hi = mid - 1;
|
|
1283
|
+
}
|
|
1284
|
+
return text.slice(0, lo);
|
|
1285
|
+
}
|
|
1286
|
+
function frameChanges(files, diff, recentCommitMessage) {
|
|
1287
|
+
return JSON.stringify({
|
|
1288
|
+
files,
|
|
1289
|
+
diff,
|
|
1290
|
+
recentCommitMessage: recentCommitMessage === "" ? null : recentCommitMessage
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
function normalizeMessage(raw) {
|
|
1294
|
+
let m = raw.trim();
|
|
1295
|
+
if (m.startsWith("```")) {
|
|
1296
|
+
const firstNl = m.indexOf("\n");
|
|
1297
|
+
if (firstNl !== -1) {
|
|
1298
|
+
m = m.slice(firstNl + 1);
|
|
1299
|
+
m = m.endsWith("```") ? m.slice(0, -3) : m;
|
|
1300
|
+
m = m.trim();
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
return m.replace(/\n---+\s*$/, "").trim();
|
|
1304
|
+
}
|
|
1305
|
+
function finishError(reason) {
|
|
1306
|
+
if (reason === void 0 || reason.kind === "stop") return void 0;
|
|
1307
|
+
if (reason.kind === "max-tokens") return { ok: false, error: { code: "llm-output", message: "model output hit the token limit" } };
|
|
1308
|
+
if (reason.kind === "tool-calls") return { ok: false, error: { code: "llm-output", message: "model requested a tool instead of writing a message" } };
|
|
1309
|
+
const failure = "failure" in reason ? reason.failure : void 0;
|
|
1310
|
+
return { ok: false, error: { code: "llm-error", message: failure?.message ?? `model stream finished abnormally (${reason.kind})` } };
|
|
1311
|
+
}
|
|
1312
|
+
async function collectDiff(deps, root, hasHead, pathArgs) {
|
|
1313
|
+
const cmds = hasHead ? [["git", "diff", "HEAD", ...pathArgs]] : [["git", "diff", "--cached", ...pathArgs], ["git", "diff", ...pathArgs]];
|
|
1314
|
+
const runs = await Promise.all(cmds.map((argv) => runCommand(deps.run, argv, root, "suggest-diff", deps.signal)));
|
|
1315
|
+
let diff = "";
|
|
1316
|
+
for (const res of runs) {
|
|
1317
|
+
if ("failure" in res) return { ok: false, error: { code: "git-unavailable", message: "diff failed" } };
|
|
1318
|
+
if (res.run.cancelled) return { ok: false, error: { code: "cancelled" } };
|
|
1319
|
+
if (res.run.timedOut) return { ok: false, error: { code: "timeout" } };
|
|
1320
|
+
if (res.run.exitCode !== 0) {
|
|
1321
|
+
return { ok: false, error: { code: "git-error", message: res.run.stderr.trim() || "git diff failed" } };
|
|
1322
|
+
}
|
|
1323
|
+
diff += res.run.stdout;
|
|
1324
|
+
}
|
|
1325
|
+
return diff;
|
|
1326
|
+
}
|
|
1327
|
+
async function appendUntracked(deps, root, files, diff, budget) {
|
|
1328
|
+
let text = diff;
|
|
1329
|
+
let truncated = false;
|
|
1330
|
+
const candidates = files.filter((c) => c.status === "untracked" && !c.isDirectory && isSafePath(c.path));
|
|
1331
|
+
for (const c of candidates.slice(0, UNTRACKED_FILE_CAP)) {
|
|
1332
|
+
const remaining = budget - Buffer.byteLength(text, "utf8");
|
|
1333
|
+
if (remaining <= 0) {
|
|
1334
|
+
truncated = true;
|
|
1335
|
+
break;
|
|
1336
|
+
}
|
|
1337
|
+
const res = await runCommand(deps.run, ["git", "diff", "--no-index", "--", "/dev/null", c.path], root, "suggest-untracked", deps.signal);
|
|
1338
|
+
if ("failure" in res || res.run.cancelled || res.run.timedOut) continue;
|
|
1339
|
+
const chunk = res.run.stdout;
|
|
1340
|
+
if (chunk === "") continue;
|
|
1341
|
+
if (Buffer.byteLength(chunk, "utf8") > remaining) {
|
|
1342
|
+
text += truncateUtf8(chunk, remaining) + "\n... [diff truncated]";
|
|
1343
|
+
truncated = true;
|
|
1344
|
+
break;
|
|
1345
|
+
}
|
|
1346
|
+
text += chunk;
|
|
1347
|
+
}
|
|
1348
|
+
return { diff: text, truncated };
|
|
1349
|
+
}
|
|
1350
|
+
async function runSuggest(deps, config, request) {
|
|
1351
|
+
if (!config.suggestEnabled) {
|
|
1352
|
+
return { ok: false, error: { code: "suggest-disabled", message: "AI suggest is turned off in the plugin config" } };
|
|
1353
|
+
}
|
|
1354
|
+
const workspace = await resolveWorkspace(deps, request.sessionId);
|
|
1355
|
+
if (!workspace.ok) return { ok: false, error: mapWorkspaceFailure(workspace.failure) };
|
|
1356
|
+
const root = workspace.root;
|
|
1357
|
+
const paths = request.paths ?? [];
|
|
1358
|
+
for (const path of paths) {
|
|
1359
|
+
if (!isSafePath(path)) return { ok: false, error: { code: "invalid-path", message: path } };
|
|
1360
|
+
}
|
|
1361
|
+
const pathArgs = paths.length > 0 ? ["--", ...paths] : [];
|
|
1362
|
+
const pathSet = new Set(paths);
|
|
1363
|
+
const [statusRes, headRes, lastRes] = await Promise.all([
|
|
1364
|
+
runCommand(deps.run, ["git", "status", "--porcelain=v1", "-z"], root, "suggest-status", deps.signal),
|
|
1365
|
+
runCommand(deps.run, ["git", "rev-parse", "--verify", "HEAD"], root, "suggest-head", deps.signal),
|
|
1366
|
+
runCommand(deps.run, ["git", "log", "-1", "--format=%B"], root, "suggest-last", deps.signal)
|
|
1367
|
+
]);
|
|
1368
|
+
if ("failure" in statusRes) return { ok: false, error: { code: "git-unavailable", message: "status failed" } };
|
|
1369
|
+
if (statusRes.run.cancelled) return { ok: false, error: { code: "cancelled" } };
|
|
1370
|
+
if (statusRes.run.timedOut) return { ok: false, error: { code: "timeout" } };
|
|
1371
|
+
if (statusRes.run.exitCode !== 0) {
|
|
1372
|
+
return { ok: false, error: { code: "git-error", message: statusRes.run.stderr.trim() || "git status failed" } };
|
|
1373
|
+
}
|
|
1374
|
+
const hasHead = "run" in headRes && headRes.run.exitCode === 0;
|
|
1375
|
+
const recentCommitMessage = "run" in lastRes && lastRes.run.exitCode === 0 ? lastRes.run.stdout.replace(/\n+$/, "") : "";
|
|
1376
|
+
const all = statusRes.run.stdoutLossy ? [] : parseStatus(statusRes.run.stdout);
|
|
1377
|
+
const files = pathSet.size > 0 ? all.filter((c) => pathSet.has(c.path)) : all;
|
|
1378
|
+
const collected = await collectDiff(deps, root, hasHead, pathArgs);
|
|
1379
|
+
if (typeof collected !== "string") return collected;
|
|
1380
|
+
if (files.length === 0 && collected.trim() === "") {
|
|
1381
|
+
return { ok: false, error: { code: "empty-diff", message: "no uncommitted changes to summarize" } };
|
|
1382
|
+
}
|
|
1383
|
+
let diff = collected;
|
|
1384
|
+
let truncated = false;
|
|
1385
|
+
if (Buffer.byteLength(diff, "utf8") > config.suggestMaxBytes) {
|
|
1386
|
+
diff = truncateUtf8(diff, config.suggestMaxBytes) + "\n... [diff truncated]";
|
|
1387
|
+
truncated = true;
|
|
1388
|
+
}
|
|
1389
|
+
const untracked = await appendUntracked(deps, root, files, diff, config.suggestMaxBytes);
|
|
1390
|
+
diff = untracked.diff;
|
|
1391
|
+
truncated = truncated || untracked.truncated;
|
|
1392
|
+
const llm = deps.getLlm?.();
|
|
1393
|
+
const route = resolveRoute(config, deps.getAgentDefaultModel?.());
|
|
1394
|
+
if (llm === void 0 || route === void 0) {
|
|
1395
|
+
return { ok: false, error: { code: "llm-unavailable", message: "no model service or route is configured" } };
|
|
1396
|
+
}
|
|
1397
|
+
const frame = frameChanges(
|
|
1398
|
+
files.map((c) => ({ path: c.path, status: c.status })),
|
|
1399
|
+
diff,
|
|
1400
|
+
recentCommitMessage
|
|
1401
|
+
);
|
|
1402
|
+
const deadline = withDeadline(config.suggestTimeoutMs, deps.signal);
|
|
1403
|
+
const options = {
|
|
1404
|
+
provider: route.provider,
|
|
1405
|
+
model: route.model,
|
|
1406
|
+
system: SYSTEM_PROMPT,
|
|
1407
|
+
messages: [{
|
|
1408
|
+
id: "git-panel-suggest",
|
|
1409
|
+
role: "user",
|
|
1410
|
+
content: [{ type: "text", text: frame }],
|
|
1411
|
+
source: { kind: "user" }
|
|
1412
|
+
}],
|
|
1413
|
+
maxTokens: config.suggestMaxOutputTokens,
|
|
1414
|
+
temperature: 0.3,
|
|
1415
|
+
signal: deadline.signal
|
|
1416
|
+
};
|
|
1417
|
+
let output = "";
|
|
1418
|
+
let finish;
|
|
1419
|
+
try {
|
|
1420
|
+
for await (const chunk of llm.stream(options)) {
|
|
1421
|
+
if (chunk.type === "text-delta") output += chunk.text;
|
|
1422
|
+
else if (chunk.type === "finish") finish = chunk.reason;
|
|
1423
|
+
}
|
|
1424
|
+
} catch (error) {
|
|
1425
|
+
if (deps.signal?.aborted === true) return { ok: false, error: { code: "cancelled" } };
|
|
1426
|
+
if (deadline.expired()) return { ok: false, error: { code: "llm-error", message: `model call timed out after ${config.suggestTimeoutMs}ms` } };
|
|
1427
|
+
return { ok: false, error: { code: "llm-error", message: error instanceof Error ? error.message : "model stream failed" } };
|
|
1428
|
+
} finally {
|
|
1429
|
+
deadline.dispose();
|
|
1430
|
+
}
|
|
1431
|
+
if (deps.signal?.aborted === true) return { ok: false, error: { code: "cancelled" } };
|
|
1432
|
+
if (deadline.expired()) return { ok: false, error: { code: "llm-error", message: `model call timed out after ${config.suggestTimeoutMs}ms` } };
|
|
1433
|
+
const terminal = finishError(finish);
|
|
1434
|
+
if (terminal !== void 0) return terminal;
|
|
1435
|
+
const message = normalizeMessage(output);
|
|
1436
|
+
if (message === "") return { ok: false, error: { code: "llm-output", message: "model produced no message text" } };
|
|
1437
|
+
return {
|
|
1438
|
+
ok: true,
|
|
1439
|
+
value: {
|
|
1440
|
+
message,
|
|
1441
|
+
provider: route.provider,
|
|
1442
|
+
model: route.model,
|
|
1443
|
+
...truncated ? { truncated: true } : {}
|
|
1444
|
+
}
|
|
1445
|
+
};
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1101
1448
|
// src/host/version.ts
|
|
1102
1449
|
import { readFile as readFile2 } from "node:fs/promises";
|
|
1103
1450
|
import { dirname as dirname2, join as join4 } from "node:path";
|
|
@@ -1225,8 +1572,8 @@ async function checkLatestVersion(options = {}) {
|
|
|
1225
1572
|
}
|
|
1226
1573
|
|
|
1227
1574
|
// 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) {
|
|
1575
|
+
var _version_dec, _suggest_dec, _query_dec, _run_dec, _snapshot_dec, _a, _init;
|
|
1576
|
+
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
1577
|
constructor(ctx, config) {
|
|
1231
1578
|
super(ctx, "gitPanel");
|
|
1232
1579
|
__runInitializers(_init, 5, this);
|
|
@@ -1272,7 +1619,13 @@ var GitPanelService = class extends (_a = TypertRemoteService, _snapshot_dec = [
|
|
|
1272
1619
|
}
|
|
1273
1620
|
},
|
|
1274
1621
|
rootCache,
|
|
1275
|
-
rootNegCache
|
|
1622
|
+
rootNegCache,
|
|
1623
|
+
// Optional model services, resolved per request so a provider fiber that
|
|
1624
|
+
// activates late or restarts is picked up — absent in deployments without
|
|
1625
|
+
// a model backend, where the suggest endpoint reports `llm-unavailable`
|
|
1626
|
+
// rather than failing activation (kept out of `static inject` on purpose).
|
|
1627
|
+
getLlm: () => get("llm"),
|
|
1628
|
+
getAgentDefaultModel: () => get("agentDefaultModel")
|
|
1276
1629
|
};
|
|
1277
1630
|
}
|
|
1278
1631
|
async snapshot(request, signal) {
|
|
@@ -1284,6 +1637,9 @@ var GitPanelService = class extends (_a = TypertRemoteService, _snapshot_dec = [
|
|
|
1284
1637
|
async query(request, signal) {
|
|
1285
1638
|
return runQuery(this.withSignal(signal), this.liveConfig(), request);
|
|
1286
1639
|
}
|
|
1640
|
+
async suggest(request, signal) {
|
|
1641
|
+
return runSuggest(this.withSignal(signal), this.liveConfig(), request);
|
|
1642
|
+
}
|
|
1287
1643
|
async version(request) {
|
|
1288
1644
|
try {
|
|
1289
1645
|
return request.check === true ? await checkLatestVersion() : await readVersionInfo();
|
|
@@ -1305,6 +1661,7 @@ _init = __decoratorStart(_a);
|
|
|
1305
1661
|
__decorateElement(_init, 1, "snapshot", _snapshot_dec, GitPanelService);
|
|
1306
1662
|
__decorateElement(_init, 1, "run", _run_dec, GitPanelService);
|
|
1307
1663
|
__decorateElement(_init, 1, "query", _query_dec, GitPanelService);
|
|
1664
|
+
__decorateElement(_init, 1, "suggest", _suggest_dec, GitPanelService);
|
|
1308
1665
|
__decorateElement(_init, 1, "version", _version_dec, GitPanelService);
|
|
1309
1666
|
__decoratorMetadata(_init, GitPanelService);
|
|
1310
1667
|
__publicField(GitPanelService, "inject", ["subprocess", "sessions", "sessionPersistence"]);
|
|
@@ -1319,13 +1676,19 @@ __publicField(GitPanelService, "Config", Schema.object({
|
|
|
1319
1676
|
defaultDiffView: Schema.union([
|
|
1320
1677
|
Schema.const("unified").description("\u7EDF\u4E00\u89C6\u56FE\uFF08\u5355\u680F\u884C\u5185\u5BF9\u6BD4\uFF09"),
|
|
1321
1678
|
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")
|
|
1679
|
+
]).default("unified").volatile().description("\u5DEE\u5F02\u5BF9\u6BD4\u9ED8\u8BA4\u89C6\u56FE"),
|
|
1680
|
+
graphStyle: Schema.union([
|
|
1681
|
+
Schema.const("parallel").description("\u5E73\u884C\u7EBF\uFF08VSCode \u98CE\u683C\uFF0C\u5206\u652F\u5404\u5360\u4E00\u5217\uFF09"),
|
|
1682
|
+
Schema.const("compact").description("\u7D27\u51D1\uFF08git log \u98CE\u683C\uFF0C\u5171\u4EAB\u7956\u5148\u5408\u5E76\u4E3A\u4E00\u5217\uFF09")
|
|
1683
|
+
]).default("parallel").volatile().description("\u63D0\u4EA4\u5386\u53F2\u56FE\u7EBF\u6761\u6837\u5F0F"),
|
|
1684
|
+
suggestEnabled: Schema.boolean().default(true).volatile().description("\u63D0\u4EA4\u6846\u663E\u793A AI \u751F\u6210\u6309\u94AE")
|
|
1323
1685
|
}));
|
|
1324
1686
|
var index_default = GitPanelService;
|
|
1325
1687
|
export {
|
|
1326
1688
|
DEFAULT_CONFIG,
|
|
1327
1689
|
GitPanelService,
|
|
1328
1690
|
checkLatestVersion,
|
|
1691
|
+
classifyActionFailure,
|
|
1329
1692
|
compareVersions,
|
|
1330
1693
|
createGitRunner,
|
|
1331
1694
|
index_default as default,
|
|
@@ -1335,12 +1698,15 @@ export {
|
|
|
1335
1698
|
parseGraphLog,
|
|
1336
1699
|
parseNameStatus,
|
|
1337
1700
|
parseRepository,
|
|
1701
|
+
parseStashList,
|
|
1338
1702
|
parseStatus,
|
|
1703
|
+
parseTags,
|
|
1339
1704
|
planAction,
|
|
1340
1705
|
readVersionInfo,
|
|
1341
1706
|
resolveWorkspace,
|
|
1342
1707
|
runAction,
|
|
1343
1708
|
runQuery,
|
|
1709
|
+
runSuggest,
|
|
1344
1710
|
snapshotForSession,
|
|
1345
1711
|
sumNumstat
|
|
1346
1712
|
};
|
|
@@ -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
|
+
}
|
package/lib/host/parser.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* git output parsers: porcelain status, log, branch, numstat, name-status.
|
|
3
3
|
* Pure functions over raw stdout, no I/O.
|
|
4
4
|
*/
|
|
5
|
-
import type { GitBranch, GitChange, GitCommit, GitFileStat, GraphCommit, GitRef } from './types.ts';
|
|
5
|
+
import type { GitBranch, GitChange, GitCommit, GitFileStat, GraphCommit, GitRef, StashEntry } from './types.ts';
|
|
6
6
|
/**
|
|
7
7
|
* Build a GitCommit from a `%H\x1f%h\x1f%s\x1f%an\x1f%aI`-ordered field array
|
|
8
8
|
* (the body, when present, is the caller's `parts.slice(5)`). Null when the
|
|
@@ -27,6 +27,14 @@ export declare function parseRefs(decoration: string): GitRef[];
|
|
|
27
27
|
export declare function parseBranches(stdout: string): GitBranch[];
|
|
28
28
|
/** Parse `git for-each-ref` tag lines: `name\0shortHash` per line. */
|
|
29
29
|
export declare function parseTags(stdout: string): GitBranch[];
|
|
30
|
+
/**
|
|
31
|
+
* Parse `git stash list -z --format=%gd%x1f%H%x1f%gs%x1f%cr` into stash entries.
|
|
32
|
+
* Each NUL-separated record is: selector (`stash@{N}`), the stash commit SHA, the
|
|
33
|
+
* reflog subject, and a relative time. The subject is either auto ("WIP on
|
|
34
|
+
* <branch>: <sha> <subj>") or custom ("On <branch>: <message>"); the branch is
|
|
35
|
+
* the text between "on " and the first ": ", and the message is after it.
|
|
36
|
+
*/
|
|
37
|
+
export declare function parseStashList(stdout: string): StashEntry[];
|
|
30
38
|
/**
|
|
31
39
|
* Parse `git show --name-status -z` into stats with an explicit state machine:
|
|
32
40
|
* read a status token, then consume exactly the paths it owns (2 for R/C, 1
|
|
@@ -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>;
|