@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.
@@ -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>;
@@ -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 SYSTEM_PROMPT = [
5
- "You are an expert at writing concise, accurate git commit messages.",
6
- "Given a repository\u2019s uncommitted changes, write ONE commit message for them.",
7
- "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.",
8
- "Return only the message text: no quotes, code fences, prefixes, bullet lists, or explanations.",
9
- "Match the language and style of the recent commit message when one is provided; otherwise write in English.",
10
- "Focus on the most important change; do not mechanically list every file."
11
- ].join("\n");
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
- function frameChanges(files, diff, recentCommitMessage) {
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
- recentCommitMessage: recentCommitMessage === "" ? null : recentCommitMessage
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, lastRes] = await Promise.all([
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", "-1", "--format=%B"], root, "suggest-last", deps.signal)
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 recentCommitMessage = "run" in lastRes && lastRes.run.exitCode === 0 ? lastRes.run.stdout.replace(/\n+$/, "") : "";
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) => pathSet.has(c.path)) : all;
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
- recentCommitMessage
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: SYSTEM_PROMPT,
255
+ system: systemPrompt(language),
178
256
  messages: [{
179
257
  id: "git-panel-suggest",
180
258
  role: "user",
181
- content: [{ type: "text", text: frame }],
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
  };
@@ -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 was capped at maxChanges. */
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
- /** A commit with parent links and ref decorations (graph rendering). */
123
- export interface GraphCommit extends GitCommit {
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, 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.
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: 128 * 1024 },
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.6.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/run.mjs && node test/e2e/run2.mjs && node test/e2e/run3.mjs && node test/e2e/run4.mjs && node test/e2e/run5.mjs",
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
  },