@xbzbing/dsh-git-panel 1.6.1 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +3 -3
- package/README.md +4 -4
- package/lib/client.js +175 -52
- package/lib/host/actions.js +49 -5
- package/lib/host/core.d.ts +12 -2
- package/lib/host/core.js +33 -6
- package/lib/host/fs-deps.d.ts +2 -0
- package/lib/host/fs-deps.js +25 -0
- package/lib/host/history-topology.d.ts +25 -0
- package/lib/host/history-topology.js +55 -0
- package/lib/host/index.d.ts +5 -3
- package/lib/host/index.js +15 -19
- package/lib/host/queries.js +20 -14
- package/lib/host/snapshot-fingerprint.d.ts +3 -0
- package/lib/host/snapshot-fingerprint.js +12 -0
- package/lib/host/suggest.d.ts +33 -1
- package/lib/host/suggest.js +107 -21
- package/lib/host/types.d.ts +64 -8
- package/lib/host/types.js +17 -2
- package/package.json +2 -2
package/lib/host/suggest.d.ts
CHANGED
|
@@ -1,4 +1,36 @@
|
|
|
1
1
|
import type { GitPanelConfig, SnapshotDeps } from './core.js';
|
|
2
|
-
import type { GitSuggestRequest, GitSuggestResult } from './types.js';
|
|
2
|
+
import type { GitSuggestRequest, GitSuggestResult, ResolvedLanguage, SuggestLanguage } from './types.js';
|
|
3
|
+
export declare function isCjkText(text: string): boolean;
|
|
4
|
+
/**
|
|
5
|
+
* Parse `git log -z --format=%ae%x1f%s` into the repository's own commit
|
|
6
|
+
* subjects, newest first: mechanical commits dropped, and the current
|
|
7
|
+
* author's commits preferred once they are numerous enough to represent the
|
|
8
|
+
* repository (a shared checkout may mix identities). Subjects are trimmed to
|
|
9
|
+
* `STYLE_SAMPLE_MAX_CHARS` code points so one runaway subject cannot consume
|
|
10
|
+
* the generation budget; the closest commit's language still dominates.
|
|
11
|
+
*/
|
|
12
|
+
export declare function parseStyleHistory(stdout: string, ownEmail: string | undefined): string[];
|
|
13
|
+
/**
|
|
14
|
+
* Resolve the language a suggestion is written in. `zh` / `en` are honored as
|
|
15
|
+
* configured; `auto` votes over the repository's recent non-mechanical commit
|
|
16
|
+
* subjects with a recency decay, so a repository that switched languages is
|
|
17
|
+
* judged by how it writes *now* rather than by its whole history. With no
|
|
18
|
+
* history to read (an unborn repository) the vote is empty and English is the
|
|
19
|
+
* neutral fallback.
|
|
20
|
+
*/
|
|
21
|
+
export declare function resolveSuggestLanguage(setting: SuggestLanguage, samples: readonly string[]): ResolvedLanguage;
|
|
22
|
+
/** A diff line longer than this is replaced by a one-line marker before the
|
|
23
|
+
* model-input budget is applied. Build output is minified into single lines (a
|
|
24
|
+
* bundled `lib/client.js` line can be hundreds of KB), so shipping it verbatim
|
|
25
|
+
* spends the whole `suggestMaxBytes` budget on one line and cuts off the hunks
|
|
26
|
+
* that actually describe the change. Line count and the diff prefix are kept,
|
|
27
|
+
* so `@@` headers still describe the same regions. */
|
|
28
|
+
export declare const LONG_LINE_MAX_CHARS = 400;
|
|
29
|
+
/** Fold over-long lines into `<prefix>… [long line: N chars omitted]`.
|
|
30
|
+
* `N` counts UTF-16 code units (what `String.length` reports), not bytes. */
|
|
31
|
+
export declare function compactLongLines(text: string, maxChars?: number): {
|
|
32
|
+
text: string;
|
|
33
|
+
folded: number;
|
|
34
|
+
};
|
|
3
35
|
/** Generate a commit message for the session's uncommitted changes. */
|
|
4
36
|
export declare function runSuggest(deps: SnapshotDeps, config: GitPanelConfig, request: GitSuggestRequest): Promise<GitSuggestResult>;
|
package/lib/host/suggest.js
CHANGED
|
@@ -1,15 +1,65 @@
|
|
|
1
1
|
import { mapWorkspaceFailure, resolveWorkspace, runCommand } from "./core.js";
|
|
2
2
|
import { isSafePath } from "./validate.js";
|
|
3
3
|
import { parseStatus } from "./parser.js";
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
4
|
+
const HISTORY_SCAN = 60;
|
|
5
|
+
const STYLE_SAMPLE_CAP = 12;
|
|
6
|
+
const STYLE_SAMPLE_MAX_CHARS = 200;
|
|
7
|
+
const OWN_AUTHOR_MIN = 3;
|
|
8
|
+
const LANGUAGE_DECAY = 0.9;
|
|
9
|
+
const MECHANICAL_SUBJECT = /^(chore: release|merged?\b|revert\b|fixup!|squash!)/i;
|
|
10
|
+
const CJK = /[\u4e00-\u9fff]/;
|
|
11
|
+
function isCjkText(text) {
|
|
12
|
+
return CJK.test(text);
|
|
13
|
+
}
|
|
14
|
+
function parseStyleHistory(stdout, ownEmail) {
|
|
15
|
+
const records = [];
|
|
16
|
+
for (const record of stdout.split("\0")) {
|
|
17
|
+
const sep = record.indexOf("");
|
|
18
|
+
if (sep === -1) continue;
|
|
19
|
+
const subject = record.slice(sep + 1).trim();
|
|
20
|
+
if (subject === "" || MECHANICAL_SUBJECT.test(subject)) continue;
|
|
21
|
+
records.push({ email: record.slice(0, sep), subject });
|
|
22
|
+
}
|
|
23
|
+
const own = ownEmail === void 0 || ownEmail === "" ? [] : records.filter((r) => r.email === ownEmail);
|
|
24
|
+
const pool = own.length >= OWN_AUTHOR_MIN ? own : records;
|
|
25
|
+
return pool.slice(0, STYLE_SAMPLE_CAP).map((r) => clampSubject(r.subject));
|
|
26
|
+
}
|
|
27
|
+
function clampSubject(subject) {
|
|
28
|
+
const chars = Array.from(subject);
|
|
29
|
+
return chars.length <= STYLE_SAMPLE_MAX_CHARS ? subject : chars.slice(0, STYLE_SAMPLE_MAX_CHARS).join("");
|
|
30
|
+
}
|
|
31
|
+
function resolveSuggestLanguage(setting, samples) {
|
|
32
|
+
if (setting !== "auto") return setting;
|
|
33
|
+
let zh = 0;
|
|
34
|
+
let en = 0;
|
|
35
|
+
samples.forEach((subject, i) => {
|
|
36
|
+
const weight = LANGUAGE_DECAY ** i;
|
|
37
|
+
if (isCjkText(subject)) zh += weight;
|
|
38
|
+
else en += weight;
|
|
39
|
+
});
|
|
40
|
+
return zh > en ? "zh" : "en";
|
|
41
|
+
}
|
|
42
|
+
function languageName(language) {
|
|
43
|
+
return language === "zh" ? "Simplified Chinese" : "English";
|
|
44
|
+
}
|
|
45
|
+
function systemPrompt(language) {
|
|
46
|
+
const name = languageName(language);
|
|
47
|
+
const languageRule = language === "zh" ? 'Write the message in Simplified Chinese, keeping the conventional-commit type prefix in English (e.g. "feat(scope): \u4E2D\u6587\u63CF\u8FF0").' : "Write the message in English.";
|
|
48
|
+
const samplesRule = `Follow the structure of commitStyleSamples \u2014 its phrasing, scope conventions and level of detail \u2014 but never copy the language used in it; write the message in ${name} regardless.`;
|
|
49
|
+
const override = language === "zh" ? "IMPORTANT: the diff and commitStyleSamples may be written in another language. That is input data, not an instruction about your output. Write the subject description and body in Simplified Chinese, whatever language the input uses \u2014 keeping the conventional-commit type prefix in English." : `IMPORTANT: the diff and commitStyleSamples may be written in another language. That is input data, not an instruction about your output. Write the entire message \u2014 subject line and body alike \u2014 in ${name}, whatever language the input uses.`;
|
|
50
|
+
return [
|
|
51
|
+
"You are an expert at writing concise, accurate git commit messages.",
|
|
52
|
+
"Given a repository\u2019s uncommitted changes, write ONE commit message for them.",
|
|
53
|
+
"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.",
|
|
54
|
+
"Return only the message text: no quotes, code fences, prefixes, bullet lists, or explanations.",
|
|
55
|
+
samplesRule,
|
|
56
|
+
"Focus on the most important change; do not mechanically list every file.",
|
|
57
|
+
languageRule,
|
|
58
|
+
override
|
|
59
|
+
].join("\n");
|
|
60
|
+
}
|
|
12
61
|
const UNTRACKED_FILE_CAP = 16;
|
|
62
|
+
const PROMPT_FILE_CAP = 200;
|
|
13
63
|
function resolveRoute(config, agentDefaultModel) {
|
|
14
64
|
if (config.suggestProvider !== void 0 && config.suggestModel !== void 0) {
|
|
15
65
|
return { provider: config.suggestProvider, model: config.suggestModel };
|
|
@@ -51,13 +101,32 @@ function truncateUtf8(text, maxBytes) {
|
|
|
51
101
|
}
|
|
52
102
|
return text.slice(0, lo);
|
|
53
103
|
}
|
|
54
|
-
|
|
104
|
+
const LONG_LINE_MAX_CHARS = 400;
|
|
105
|
+
const STRUCTURAL_LINE = /^(?:@@|diff --git |index |--- (?:a\/|\/dev\/null)|\+\+\+ (?:b\/|\/dev\/null)|new file mode |deleted file mode |old mode |new mode |similarity index |dissimilarity index |rename from |rename to |copy from |copy to |Binary files |GIT binary patch|literal |delta |\\ )/;
|
|
106
|
+
function compactLongLines(text, maxChars = LONG_LINE_MAX_CHARS) {
|
|
107
|
+
if (maxChars <= 0) return { text, folded: 0 };
|
|
108
|
+
let folded = 0;
|
|
109
|
+
const lines = text.split("\n").map((line) => {
|
|
110
|
+
if (line.length <= maxChars || STRUCTURAL_LINE.test(line)) return line;
|
|
111
|
+
folded += 1;
|
|
112
|
+
const prefix = line[0] === "+" || line[0] === "-" || line[0] === " " || line[0] === "\\" ? line[0] : "";
|
|
113
|
+
return `${prefix}\u2026 [long line: ${line.length} chars omitted]`;
|
|
114
|
+
});
|
|
115
|
+
return { text: lines.join("\n"), folded };
|
|
116
|
+
}
|
|
117
|
+
function frameChanges(files, filesOmitted, diff, styleSamples) {
|
|
55
118
|
return JSON.stringify({
|
|
56
119
|
files,
|
|
120
|
+
...filesOmitted > 0 ? { filesOmitted } : {},
|
|
57
121
|
diff,
|
|
58
|
-
|
|
122
|
+
commitStyleSamples: styleSamples.length === 0 ? null : styleSamples
|
|
59
123
|
});
|
|
60
124
|
}
|
|
125
|
+
function languageReminder(language) {
|
|
126
|
+
const name = languageName(language);
|
|
127
|
+
const prefix = language === "zh" ? " Keep the conventional-commit type prefix in English." : "";
|
|
128
|
+
return `Write the commit message in ${name}. The subject line and body must be ${name}.${prefix}`;
|
|
129
|
+
}
|
|
61
130
|
function normalizeMessage(raw) {
|
|
62
131
|
let m = raw.trim();
|
|
63
132
|
if (m.startsWith("```")) {
|
|
@@ -106,7 +175,7 @@ async function appendUntracked(deps, root, files, diff, budget) {
|
|
|
106
175
|
}
|
|
107
176
|
const res = await runCommand(deps.run, ["git", "diff", "--no-index", "--", "/dev/null", c.path], root, "suggest-untracked", deps.signal);
|
|
108
177
|
if ("failure" in res || res.run.cancelled || res.run.timedOut) continue;
|
|
109
|
-
const chunk = res.run.stdout;
|
|
178
|
+
const chunk = compactLongLines(res.run.stdout).text;
|
|
110
179
|
if (chunk === "") continue;
|
|
111
180
|
const lossy = res.run.stdoutLossy;
|
|
112
181
|
if (lossy || Buffer.byteLength(chunk, "utf8") > remaining) {
|
|
@@ -131,10 +200,11 @@ async function runSuggest(deps, config, request) {
|
|
|
131
200
|
}
|
|
132
201
|
const pathArgs = paths.length > 0 ? ["--", ...paths] : [];
|
|
133
202
|
const pathSet = new Set(paths);
|
|
134
|
-
const [statusRes, headRes,
|
|
135
|
-
runCommand(deps.run, ["git", "status", "--porcelain=v1", "-z"], root, "suggest-status", deps.signal),
|
|
203
|
+
const [statusRes, headRes, logRes, emailRes] = await Promise.all([
|
|
204
|
+
runCommand(deps.run, ["git", "status", "--porcelain=v1", "-z", "--untracked-files=all"], root, "suggest-status", deps.signal),
|
|
136
205
|
runCommand(deps.run, ["git", "rev-parse", "--verify", "HEAD"], root, "suggest-head", deps.signal),
|
|
137
|
-
runCommand(deps.run, ["git", "log", "-
|
|
206
|
+
runCommand(deps.run, ["git", "log", `-n${HISTORY_SCAN}`, "-z", "--format=%ae%s"], root, "suggest-log", deps.signal),
|
|
207
|
+
runCommand(deps.run, ["git", "config", "user.email"], root, "suggest-email", deps.signal)
|
|
138
208
|
]);
|
|
139
209
|
if ("failure" in statusRes) return { ok: false, error: { code: "git-unavailable", message: "status failed" } };
|
|
140
210
|
if (statusRes.run.cancelled) return { ok: false, error: { code: "cancelled" } };
|
|
@@ -143,15 +213,22 @@ async function runSuggest(deps, config, request) {
|
|
|
143
213
|
return { ok: false, error: { code: "git-error", message: statusRes.run.stderr.trim() || "git status failed" } };
|
|
144
214
|
}
|
|
145
215
|
const hasHead = "run" in headRes && headRes.run.exitCode === 0;
|
|
146
|
-
const
|
|
216
|
+
const styleSamples = "run" in logRes && logRes.run.exitCode === 0 ? parseStyleHistory(logRes.run.stdout, "run" in emailRes && emailRes.run.exitCode === 0 ? emailRes.run.stdout.trim() : void 0) : [];
|
|
217
|
+
const language = resolveSuggestLanguage(config.suggestLanguage, styleSamples);
|
|
147
218
|
const all = statusRes.run.stdoutLossy ? [] : parseStatus(statusRes.run.stdout);
|
|
148
|
-
const files = pathSet.size > 0 ? all.filter((c) =>
|
|
219
|
+
const files = pathSet.size > 0 ? all.filter((c) => {
|
|
220
|
+
if (pathSet.has(c.path)) return true;
|
|
221
|
+
for (let slash = c.path.indexOf("/"); slash !== -1; slash = c.path.indexOf("/", slash + 1)) {
|
|
222
|
+
if (pathSet.has(c.path.slice(0, slash))) return true;
|
|
223
|
+
}
|
|
224
|
+
return false;
|
|
225
|
+
}) : all;
|
|
149
226
|
const collected = await collectDiff(deps, root, hasHead, pathArgs);
|
|
150
227
|
if (!("diff" in collected)) return collected;
|
|
151
228
|
if (files.length === 0 && collected.diff.trim() === "") {
|
|
152
229
|
return { ok: false, error: { code: "empty-diff", message: "no uncommitted changes to summarize" } };
|
|
153
230
|
}
|
|
154
|
-
let diff = collected.diff;
|
|
231
|
+
let diff = compactLongLines(collected.diff).text;
|
|
155
232
|
let truncated = collected.lossy;
|
|
156
233
|
if (Buffer.byteLength(diff, "utf8") > config.suggestMaxBytes) {
|
|
157
234
|
diff = truncateUtf8(diff, config.suggestMaxBytes) + "\n... [diff truncated]";
|
|
@@ -166,19 +243,23 @@ async function runSuggest(deps, config, request) {
|
|
|
166
243
|
return { ok: false, error: { code: "llm-unavailable", message: "no model service or route is configured" } };
|
|
167
244
|
}
|
|
168
245
|
const frame = frameChanges(
|
|
169
|
-
files.map((c) => ({ path: c.path, status: c.status })),
|
|
246
|
+
files.slice(0, PROMPT_FILE_CAP).map((c) => ({ path: c.path, status: c.status })),
|
|
247
|
+
Math.max(0, files.length - PROMPT_FILE_CAP),
|
|
170
248
|
diff,
|
|
171
|
-
|
|
249
|
+
styleSamples
|
|
172
250
|
);
|
|
173
251
|
const deadline = withDeadline(config.suggestTimeoutMs, deps.signal);
|
|
174
252
|
const options = {
|
|
175
253
|
provider: route.provider,
|
|
176
254
|
model: route.model,
|
|
177
|
-
system:
|
|
255
|
+
system: systemPrompt(language),
|
|
178
256
|
messages: [{
|
|
179
257
|
id: "git-panel-suggest",
|
|
180
258
|
role: "user",
|
|
181
|
-
content: [
|
|
259
|
+
content: [
|
|
260
|
+
{ type: "text", text: frame },
|
|
261
|
+
{ type: "text", text: languageReminder(language) }
|
|
262
|
+
],
|
|
182
263
|
source: { kind: "user" }
|
|
183
264
|
}],
|
|
184
265
|
maxTokens: config.suggestMaxOutputTokens,
|
|
@@ -216,5 +297,10 @@ async function runSuggest(deps, config, request) {
|
|
|
216
297
|
};
|
|
217
298
|
}
|
|
218
299
|
export {
|
|
300
|
+
LONG_LINE_MAX_CHARS,
|
|
301
|
+
compactLongLines,
|
|
302
|
+
isCjkText,
|
|
303
|
+
parseStyleHistory,
|
|
304
|
+
resolveSuggestLanguage,
|
|
219
305
|
runSuggest
|
|
220
306
|
};
|
package/lib/host/types.d.ts
CHANGED
|
@@ -72,6 +72,9 @@ export interface GitRemote {
|
|
|
72
72
|
/** Known-host classification, for the status-bar icon. */
|
|
73
73
|
readonly hostKind: RemoteHostKind;
|
|
74
74
|
}
|
|
75
|
+
/** Why the change list is incomplete: capped at `maxChanges`, or the status
|
|
76
|
+
* stream itself was clipped (a lossy read drops the head). */
|
|
77
|
+
export type TruncationReason = 'cap' | 'lossy';
|
|
75
78
|
/** Immutable snapshot of one repository's status at `checkedAt`. */
|
|
76
79
|
export interface GitSnapshot {
|
|
77
80
|
/** Realpath of the repository root (work tree top). */
|
|
@@ -97,8 +100,14 @@ export interface GitSnapshot {
|
|
|
97
100
|
readonly changes: readonly GitChange[];
|
|
98
101
|
/** Working-tree statistics for the changes-page header (single source). */
|
|
99
102
|
readonly stats: WorktreeStats;
|
|
100
|
-
/** True when the change list
|
|
103
|
+
/** True when the change list is incomplete (capped or clipped). */
|
|
101
104
|
readonly truncated: boolean;
|
|
105
|
+
/** Which limit produced `truncated`; absent on older hosts. */
|
|
106
|
+
readonly truncatedReason?: TruncationReason;
|
|
107
|
+
/** True when untracked files fell back to git's collapsed `?? dir/` entries
|
|
108
|
+
* because expanding them would overflow `maxChanges` (or the status stream).
|
|
109
|
+
* Older hosts never set it. */
|
|
110
|
+
readonly untrackedCollapsed?: boolean;
|
|
102
111
|
/** Polling interval the client should use after this snapshot (0 = off). */
|
|
103
112
|
readonly refreshIntervalMs: number;
|
|
104
113
|
/** Whether the input-bar git marker pill should render (user preference). */
|
|
@@ -109,6 +118,9 @@ export interface GitSnapshot {
|
|
|
109
118
|
readonly graphStyle: GraphStyle;
|
|
110
119
|
/** Whether the commit box shows the AI-suggest button (user preference). */
|
|
111
120
|
readonly suggestEnabled: boolean;
|
|
121
|
+
/** SHA-256 of the final business snapshot, excluding checkedAt and itself.
|
|
122
|
+
* Optional for compatibility with hosts that do not provide fingerprints. */
|
|
123
|
+
readonly stateFingerprint?: string;
|
|
112
124
|
/** Epoch millis of the snapshot. */
|
|
113
125
|
readonly checkedAt: number;
|
|
114
126
|
}
|
|
@@ -119,9 +131,15 @@ export interface GitCommit {
|
|
|
119
131
|
readonly author: string;
|
|
120
132
|
readonly dateIso: string;
|
|
121
133
|
}
|
|
122
|
-
/**
|
|
123
|
-
|
|
134
|
+
/** One node of the commit graph's raw ancestry: a full hash and its raw parent
|
|
135
|
+
* hashes. `parents` is empty for a true root — and equally for a shallow
|
|
136
|
+
* boundary, which git cannot tell apart from a root. */
|
|
137
|
+
export interface GitGraphNode {
|
|
138
|
+
readonly hash: string;
|
|
124
139
|
readonly parents: readonly string[];
|
|
140
|
+
}
|
|
141
|
+
/** A commit with parent links and ref decorations (graph rendering). */
|
|
142
|
+
export interface GraphCommit extends GitCommit, GitGraphNode {
|
|
125
143
|
readonly refs: readonly GitRef[];
|
|
126
144
|
}
|
|
127
145
|
export interface GitRef {
|
|
@@ -210,7 +228,7 @@ export type GitAction = {
|
|
|
210
228
|
/** `git reset` modes, from least to most destructive. Single source so the
|
|
211
229
|
* client dialog and the command builder cannot drift. */
|
|
212
230
|
export type ResetMode = 'soft' | 'mixed' | 'hard';
|
|
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';
|
|
231
|
+
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' | 'nested-repo' | 'nested-repo-partial' | 'empty-diff' | 'llm-unavailable' | 'llm-error' | 'llm-output' | 'suggest-disabled';
|
|
214
232
|
export type GitActionResult = {
|
|
215
233
|
readonly ok: true;
|
|
216
234
|
readonly snapshot: GitSnapshot;
|
|
@@ -226,6 +244,16 @@ export interface GitActionRequest {
|
|
|
226
244
|
readonly sessionId: string;
|
|
227
245
|
readonly action: GitAction;
|
|
228
246
|
}
|
|
247
|
+
/**
|
|
248
|
+
* True when a history `search` is a commit-hash prefix jump rather than a
|
|
249
|
+
* message regex: 7–40 hex chars after trimming.
|
|
250
|
+
*/
|
|
251
|
+
export declare function isHistoryHashSearch(search: string): boolean;
|
|
252
|
+
/** Whether Git's effective content filters hide commits; hash jumps ignore them.
|
|
253
|
+
* Shared with clients receiving history from a host that omits topology. */
|
|
254
|
+
export declare function isHistoryFiltered(query: Pick<Extract<GitQuery, {
|
|
255
|
+
kind: 'history';
|
|
256
|
+
}>, 'search' | 'author' | 'since'>): boolean;
|
|
229
257
|
export type GitQuery = {
|
|
230
258
|
readonly kind: 'history';
|
|
231
259
|
readonly limit: number;
|
|
@@ -357,6 +385,11 @@ export type GitQueryResult = {
|
|
|
357
385
|
readonly kind: 'history';
|
|
358
386
|
readonly commits: readonly GraphCommit[];
|
|
359
387
|
readonly total: number;
|
|
388
|
+
/** Raw ancestry for a filtered page: the page's commits plus the ancestors
|
|
389
|
+
* an effective search/author/since filter hid from the page itself. Absent
|
|
390
|
+
* when no effective filter is active (or on a hash jump); `[]` means the
|
|
391
|
+
* bounded auxiliary walk failed, so the graph is incomplete. */
|
|
392
|
+
readonly topology?: readonly GitGraphNode[];
|
|
360
393
|
}
|
|
361
394
|
/**
|
|
362
395
|
* Unified diff text plus the new-side line count. The count is the fact the
|
|
@@ -372,10 +405,13 @@ export type GitQueryResult = {
|
|
|
372
405
|
* trailing gap rather than a speculative one.
|
|
373
406
|
*
|
|
374
407
|
* `truncated` marks the one case where the text itself is incomplete: the
|
|
375
|
-
* runner dropped output at its collect/spill ceiling,
|
|
376
|
-
*
|
|
377
|
-
* the
|
|
378
|
-
*
|
|
408
|
+
* runner dropped output at its collect/spill ceiling, which discards the
|
|
409
|
+
* *head* of the stream, so `text` is a tail that still parses like a real
|
|
410
|
+
* diff — the surviving hunks carry git's own line numbers — while the header
|
|
411
|
+
* and opening hunks are gone and the +/- counts under-report. It is flagged
|
|
412
|
+
* rather than refused (the partial hunks stay readable), but the view must say
|
|
413
|
+
* so: it would otherwise present the lost opening as ordinary folded context,
|
|
414
|
+
* as if the diff were whole.
|
|
379
415
|
*/
|
|
380
416
|
| {
|
|
381
417
|
readonly kind: 'diff';
|
|
@@ -478,6 +514,14 @@ export type GitQueryResult = {
|
|
|
478
514
|
/** Modification time (epoch ms) from the same stat (all variants). */
|
|
479
515
|
readonly mtimeMs?: number;
|
|
480
516
|
};
|
|
517
|
+
/**
|
|
518
|
+
* Byte envelope of the file browser's "copy file content" action: a text
|
|
519
|
+
* document is copyable strictly below this size. Shared by both halves —
|
|
520
|
+
* the host floors its text read here (so every copyable document can
|
|
521
|
+
* actually load) and the client hides the copy button at/above it, keeping
|
|
522
|
+
* one boundary for load and copy.
|
|
523
|
+
*/
|
|
524
|
+
export declare const FILE_COPY_MAX_BYTES: number;
|
|
481
525
|
export type GitQueryResponse = {
|
|
482
526
|
readonly ok: true;
|
|
483
527
|
readonly value: GitQueryResult;
|
|
@@ -517,6 +561,18 @@ export declare const SUGGEST_LIMITS: {
|
|
|
517
561
|
readonly default: 15000;
|
|
518
562
|
};
|
|
519
563
|
};
|
|
564
|
+
/**
|
|
565
|
+
* Selectable commit-message languages. `auto` infers the language from the
|
|
566
|
+
* repository's own commit history (see resolveSuggestLanguage); `zh` / `en`
|
|
567
|
+
* pin it. Shared by both halves like SUGGEST_LIMITS — the detail-page form
|
|
568
|
+
* renders one segment per entry.
|
|
569
|
+
*/
|
|
570
|
+
export declare const SUGGEST_LANGUAGES: readonly ["auto", "zh", "en"];
|
|
571
|
+
export type SuggestLanguage = (typeof SUGGEST_LANGUAGES)[number];
|
|
572
|
+
/** Default is `auto`: follow the style the repository is already written in. */
|
|
573
|
+
export declare const SUGGEST_LANGUAGE_DEFAULT: SuggestLanguage;
|
|
574
|
+
/** A language a suggestion is actually written in — `auto` is always resolved. */
|
|
575
|
+
export type ResolvedLanguage = Exclude<SuggestLanguage, 'auto'>;
|
|
520
576
|
/**
|
|
521
577
|
* Wire request for the suggest endpoint: ask the configured model to write a
|
|
522
578
|
* commit message from the repository's uncommitted changes. `paths` restricts
|
package/lib/host/types.js
CHANGED
|
@@ -2,11 +2,21 @@ const DIFF_CONTEXT_DEFAULT = 3;
|
|
|
2
2
|
const DIFF_CONTEXT_MAX = 1e5;
|
|
3
3
|
const DIFF_EXPAND_CONTEXT = 2e3;
|
|
4
4
|
const DIFF_COUNT_READ_MAX = 8 * 1024 * 1024;
|
|
5
|
+
function isHistoryHashSearch(search) {
|
|
6
|
+
return /^[0-9a-fA-F]{7,40}$/.test(search.trim());
|
|
7
|
+
}
|
|
8
|
+
function isHistoryFiltered(query) {
|
|
9
|
+
const search = query.search?.trim() ?? "";
|
|
10
|
+
return !isHistoryHashSearch(search) && (search !== "" || (query.author ?? "") !== "" || (query.since ?? "") !== "");
|
|
11
|
+
}
|
|
12
|
+
const FILE_COPY_MAX_BYTES = 10 * 1024 * 1024;
|
|
5
13
|
const SUGGEST_LIMITS = {
|
|
6
|
-
maxBytes: { min: 16 * 1024, max: 512 * 1024, default:
|
|
14
|
+
maxBytes: { min: 16 * 1024, max: 512 * 1024, default: 200 * 1024 },
|
|
7
15
|
maxOutputTokens: { min: 256, max: 16384, default: 4096 },
|
|
8
16
|
timeoutMs: { min: 5e3, max: 12e4, default: 15e3 }
|
|
9
17
|
};
|
|
18
|
+
const SUGGEST_LANGUAGES = ["auto", "zh", "en"];
|
|
19
|
+
const SUGGEST_LANGUAGE_DEFAULT = "auto";
|
|
10
20
|
const IMAGE_MIME = {
|
|
11
21
|
png: "image/png",
|
|
12
22
|
jpg: "image/jpeg",
|
|
@@ -30,7 +40,12 @@ export {
|
|
|
30
40
|
DIFF_CONTEXT_MAX,
|
|
31
41
|
DIFF_COUNT_READ_MAX,
|
|
32
42
|
DIFF_EXPAND_CONTEXT,
|
|
43
|
+
FILE_COPY_MAX_BYTES,
|
|
33
44
|
IMAGE_MIME,
|
|
45
|
+
SUGGEST_LANGUAGES,
|
|
46
|
+
SUGGEST_LANGUAGE_DEFAULT,
|
|
34
47
|
SUGGEST_LIMITS,
|
|
35
|
-
imageMimeFor
|
|
48
|
+
imageMimeFor,
|
|
49
|
+
isHistoryFiltered,
|
|
50
|
+
isHistoryHashSearch
|
|
36
51
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xbzbing/dsh-git-panel",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.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",
|
|
@@ -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/
|
|
46
|
+
"test:e2e": "node build.mjs && node test/e2e/setup.mjs && node test/e2e/all.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
|
},
|