@xbzbing/dsh-git-panel 1.8.0 → 1.8.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.
@@ -1,5 +1,6 @@
1
1
  import { join, sep } from "node:path";
2
2
  import { mapWorkspaceFailure, resolveWorkspace, runCommand, snapshotForSession } from "./core.js";
3
+ import { parseStatus } from "./parser.js";
3
4
  import { isSafeBranchName, isSafePath, isSafeRemoteName, isSafeRev } from "./validate.js";
4
5
  function withPaths(prefixes, paths) {
5
6
  if (paths.length === 0) return { error: "invalid-path", message: "no paths given" };
@@ -20,6 +21,8 @@ function planAction(action, unborn) {
20
21
  return unborn ? { argv: [["git", "rm", "--cached", "-r", "--", "."]] } : { argv: [["git", "restore", "--staged", "--", "."]] };
21
22
  case "discard":
22
23
  return withPaths([["git", "restore", "--"]], action.paths);
24
+ case "discard-all":
25
+ return { error: "invalid-path", message: "discard-all must be resolved to paths before planning" };
23
26
  case "commit": {
24
27
  const message = action.message.trim();
25
28
  const amend = action.amend === true;
@@ -93,6 +96,14 @@ function isIndexBusy(outcome) {
93
96
  if (!("run" in outcome) || outcome.run.exitCode === 0) return false;
94
97
  return /index\.lock|Unable to create.*index|another git process/i.test(outcome.run.stderr + outcome.run.stdout);
95
98
  }
99
+ function probeFailure(outcome) {
100
+ if ("failure" in outcome) {
101
+ return { ok: false, error: { code: "git-unavailable", message: outcome.failure instanceof Error ? outcome.failure.message : String(outcome.failure) } };
102
+ }
103
+ if (outcome.run.cancelled) return { ok: false, error: { code: "cancelled" } };
104
+ if (outcome.run.timedOut) return { ok: false, error: { code: "timeout" } };
105
+ return null;
106
+ }
96
107
  function scrubCredentials(text) {
97
108
  return text.replace(/([a-z][a-z0-9+.-]*:\/\/)[^/@\s]*@/gi, "$1");
98
109
  }
@@ -105,8 +116,8 @@ function classifyActionFailure(stdout, stderr, exitCode) {
105
116
  if (/index\.lock|Unable to create.*index|another git process/i.test(combined)) {
106
117
  return { code: "index-busy", message: err };
107
118
  }
108
- if (/nothing to commit|no changes added/i.test(combined)) {
109
- return { code: "git-error", message: err || "nothing to commit" };
119
+ if (/nothing to commit|nothing added to commit|no changes added/i.test(combined)) {
120
+ return { code: "nothing-to-commit", message: err };
110
121
  }
111
122
  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)) {
112
123
  return { code: "not-found", message: err || "not found" };
@@ -137,6 +148,16 @@ async function runAction(deps, config, request) {
137
148
  const root = workspace.root;
138
149
  const headProbe = await runCommand(deps.run, ["git", "rev-parse", "--verify", "HEAD"], root, "head-probe", deps.signal);
139
150
  const unborn = !("run" in headProbe) || headProbe.run.exitCode !== 0;
151
+ if (request.action.kind === "publish") {
152
+ const headBroke = probeFailure(headProbe);
153
+ if (headBroke !== null) return headBroke;
154
+ if (unborn) return { ok: false, error: { code: "no-branch", message: "no commits to publish" } };
155
+ const branchProbe = await runCommand(deps.run, ["git", "symbolic-ref", "--quiet", "--short", "HEAD"], root, "publish-branch", deps.signal);
156
+ const branchBroke = probeFailure(branchProbe);
157
+ if (branchBroke !== null) return branchBroke;
158
+ const branch = "run" in branchProbe && branchProbe.run.exitCode === 0 ? branchProbe.run.stdout.trim() : "";
159
+ if (branch === "") return { ok: false, error: { code: "no-branch", message: "detached HEAD: no branch to publish" } };
160
+ }
140
161
  const act = request.action;
141
162
  if (act.kind === "stash-apply" || act.kind === "stash-pop" || act.kind === "stash-drop") {
142
163
  if (!Number.isInteger(act.index) || act.index < 0) {
@@ -148,6 +169,27 @@ async function runAction(deps, config, request) {
148
169
  return { ok: false, error: { code: "not-found", message: "stash entry changed; refresh and retry" } };
149
170
  }
150
171
  }
172
+ if (request.action.kind === "discard-all") {
173
+ const status = await runCommand(deps.run, ["git", "status", "--porcelain=v1", "-z", "--untracked-files=all"], root, "discard-all-status", deps.signal);
174
+ if ("failure" in status) return { ok: false, error: { code: "git-unavailable", message: status.failure instanceof Error ? status.failure.message : String(status.failure) } };
175
+ if (status.run.cancelled) return { ok: false, error: { code: "cancelled" } };
176
+ if (status.run.timedOut) return { ok: false, error: { code: "timeout" } };
177
+ if (status.run.exitCode !== 0) {
178
+ const failure = classifyActionFailure(status.run.stdout, status.run.stderr, status.run.exitCode ?? -1);
179
+ return { ok: false, error: failure };
180
+ }
181
+ if (status.run.stdoutLossy) {
182
+ return { ok: false, error: { code: "status-lossy", message: "status output was clipped; refusing to discard an incomplete change set" } };
183
+ }
184
+ const all = parseStatus(status.run.stdout);
185
+ const paths = [...new Set(all.filter((c) => !c.staged).map((c) => c.path))].sort();
186
+ if (paths.length === 0) {
187
+ const snapshot2 = await snapshotForSession(deps, config, request.sessionId);
188
+ if (!snapshot2.ok) return { ok: false, error: { code: "git-error", message: "snapshot after action failed" } };
189
+ return { ok: true, snapshot: snapshot2.value, output: "" };
190
+ }
191
+ request = { ...request, action: { kind: "discard", paths } };
192
+ }
151
193
  if (request.action.kind === "discard") {
152
194
  const removed = await discardUntracked(deps, config, root, request.sessionId, request.action.paths);
153
195
  if (removed !== null) {
@@ -1,6 +1,6 @@
1
1
  import type { GitRunner } from './git.js';
2
2
  import type { AgentDefaultModelFace, LlmFace, SessionModelSelection } from './llm-face.js';
3
- import type { DiffViewMode, GraphStyle, GitChange, GitErrorCode, GitRemote, GitSnapshotResult, SuggestLanguage } from './types.js';
3
+ import type { DiffViewMode, FontDelta, GraphStyle, GitChange, GitErrorCode, GitRemote, GitSnapshotResult, SuggestLanguage } from './types.js';
4
4
  export interface GitPanelConfig {
5
5
  readonly timeoutMs: number;
6
6
  /** Per-command stdout cap; also the per-side image-diff payload cap. */
@@ -13,6 +13,8 @@ export interface GitPanelConfig {
13
13
  readonly defaultDiffView: DiffViewMode;
14
14
  /** Commit-graph line style (parallel = VSCode-like, compact = git log). */
15
15
  readonly graphStyle: GraphStyle;
16
+ /** Panel font-size step: -1 / 0 / +1. */
17
+ readonly fontDelta: FontDelta;
16
18
  /** Whether the "AI suggest" button shows in the commit box. */
17
19
  readonly suggestEnabled: boolean;
18
20
  /** Language the generated message is written in; `auto` follows repo history. */
@@ -36,6 +38,8 @@ export declare function normalizeConfig(raw: unknown): GitPanelConfig;
36
38
  export declare function readDiffView(value: unknown, fallback: DiffViewMode): DiffViewMode;
37
39
  /** Read the commit-graph style field, unwrapping a schemastery volatile ref. */
38
40
  export declare function readGraphStyle(value: unknown, fallback: GraphStyle): GraphStyle;
41
+ /** Read the panel font-size step, unwrapping a schemastery volatile ref. */
42
+ export declare function readFontDelta(value: unknown): FontDelta;
39
43
  /**
40
44
  * Read the commit-message language field, unwrapping a schemastery volatile
41
45
  * ref. An unknown string (a hand-edited profile, or a value from a newer
@@ -162,6 +166,8 @@ export declare function mapWorkspaceFailure(failure: GitSnapshotResult & {
162
166
  message?: string;
163
167
  };
164
168
  /** Produce a full snapshot for a session. */
165
- export declare function snapshotForSession(deps: SnapshotDeps, config: GitPanelConfig, sessionId: string): Promise<GitSnapshotResult>;
169
+ export declare function snapshotForSession(deps: SnapshotDeps, config: GitPanelConfig, sessionId: string, opts?: {
170
+ readonly fullChanges?: boolean;
171
+ }): Promise<GitSnapshotResult>;
166
172
  /** Max mtime among changed files (epoch ms), capped for large sets. */
167
173
  export declare function maxChangeMtime(deps: SnapshotDeps, root: string, changes: readonly GitChange[], cap?: number): Promise<number | null>;
package/lib/host/core.js CHANGED
@@ -3,7 +3,7 @@ import { commitFromFields, parseStatus, sumNumstat } from "./parser.js";
3
3
  import { parseRemote } from "./remote.js";
4
4
  import { snapshotFingerprint } from "./snapshot-fingerprint.js";
5
5
  import { isSafePath } from "./validate.js";
6
- import { SUGGEST_LANGUAGE_DEFAULT, SUGGEST_LANGUAGES, SUGGEST_LIMITS } from "./types.js";
6
+ import { SUGGEST_LANGUAGE_DEFAULT, SUGGEST_LANGUAGES, SUGGEST_LIMITS, FULL_CHANGES_MAX } from "./types.js";
7
7
  const DEFAULT_CONFIG = {
8
8
  timeoutMs: 8e3,
9
9
  maxBytes: 4 * 1024 * 1024,
@@ -12,6 +12,7 @@ const DEFAULT_CONFIG = {
12
12
  showInputPill: true,
13
13
  defaultDiffView: "unified",
14
14
  graphStyle: "parallel",
15
+ fontDelta: 0,
15
16
  suggestEnabled: true,
16
17
  suggestLanguage: SUGGEST_LANGUAGE_DEFAULT,
17
18
  suggestMaxBytes: SUGGEST_LIMITS.maxBytes.default,
@@ -41,6 +42,7 @@ function normalizeConfig(raw) {
41
42
  showInputPill: readBool(c.showInputPill, DEFAULT_CONFIG.showInputPill),
42
43
  defaultDiffView: readDiffView(c.defaultDiffView, DEFAULT_CONFIG.defaultDiffView),
43
44
  graphStyle: readGraphStyle(c.graphStyle, DEFAULT_CONFIG.graphStyle),
45
+ fontDelta: readFontDelta(c.fontDelta),
44
46
  suggestEnabled: readBool(c.suggestEnabled, DEFAULT_CONFIG.suggestEnabled),
45
47
  suggestLanguage: readSuggestLanguage(c.suggestLanguage, DEFAULT_CONFIG.suggestLanguage),
46
48
  suggestMaxBytes: clampLimit(c.suggestMaxBytes, SUGGEST_LIMITS.maxBytes),
@@ -60,6 +62,10 @@ function readGraphStyle(value, fallback) {
60
62
  const raw = unwrapVolatile(value);
61
63
  return raw === "parallel" || raw === "compact" ? raw : fallback;
62
64
  }
65
+ function readFontDelta(value) {
66
+ const raw = unwrapVolatile(value);
67
+ return raw === -1 || raw === 1 ? raw : 0;
68
+ }
63
69
  function readSuggestLanguage(value, fallback) {
64
70
  const raw = unwrapVolatile(value);
65
71
  return typeof raw === "string" && SUGGEST_LANGUAGES.includes(raw) ? raw : fallback;
@@ -175,7 +181,8 @@ function mapWorkspaceFailure(failure) {
175
181
  const message = "detail" in error ? error.detail : void 0;
176
182
  return { code: error.code, ...message !== void 0 ? { message } : {} };
177
183
  }
178
- async function snapshotForSession(deps, config, sessionId) {
184
+ async function snapshotForSession(deps, config, sessionId, opts) {
185
+ const changeLimit = opts?.fullChanges === true ? Math.max(config.maxChanges, FULL_CHANGES_MAX) : config.maxChanges;
179
186
  const workspace = await resolveWorkspace(deps, sessionId);
180
187
  if (!workspace.ok) {
181
188
  const failure = workspace.failure;
@@ -214,7 +221,7 @@ async function snapshotForSession(deps, config, sessionId) {
214
221
  let untrackedCollapsed = false;
215
222
  const untrackedEntries = allChanges.filter((c) => c.status === "untracked");
216
223
  const collapseCandidate = untrackedEntries.some((c) => c.path.includes("/"));
217
- if (statusLossy || untrackedEntries.length > config.maxChanges && collapseCandidate) {
224
+ if (statusLossy || untrackedEntries.length > changeLimit && collapseCandidate) {
218
225
  const collapsed = await runCommand(deps.run, ["git", "status", "--porcelain=v1", "-z", "--untracked-files=normal"], root, "status-collapsed", deps.signal);
219
226
  if ("run" in collapsed && !collapsed.run.timedOut && collapsed.run.exitCode === 0) {
220
227
  const list = parseStatus(collapsed.run.stdout);
@@ -223,9 +230,9 @@ async function snapshotForSession(deps, config, sessionId) {
223
230
  statusLossy = collapsed.run.stdoutLossy;
224
231
  }
225
232
  }
226
- const truncated = allChanges.length > config.maxChanges || statusLossy;
227
- const truncatedReason = statusLossy ? "lossy" : allChanges.length > config.maxChanges ? "cap" : void 0;
228
- const changes = allChanges.length > config.maxChanges ? allChanges.slice(0, config.maxChanges) : allChanges;
233
+ const truncated = allChanges.length > changeLimit || statusLossy;
234
+ const truncatedReason = statusLossy ? "lossy" : allChanges.length > changeLimit ? "cap" : void 0;
235
+ const changes = allChanges.length > changeLimit ? allChanges.slice(0, changeLimit) : allChanges;
229
236
  let staged = 0;
230
237
  let modified = 0;
231
238
  let untracked = 0;
@@ -300,6 +307,7 @@ async function snapshotForSession(deps, config, sessionId) {
300
307
  showInputPill: config.showInputPill,
301
308
  defaultDiffView: config.defaultDiffView,
302
309
  graphStyle: config.graphStyle,
310
+ fontDelta: config.fontDelta,
303
311
  suggestEnabled: config.suggestEnabled,
304
312
  checkedAt: Date.now()
305
313
  };
@@ -328,6 +336,7 @@ export {
328
336
  parsePrimaryRemote,
329
337
  readBool,
330
338
  readDiffView,
339
+ readFontDelta,
331
340
  readGraphStyle,
332
341
  readSuggestLanguage,
333
342
  resolveBrowseRoot,
@@ -9,13 +9,14 @@
9
9
  import { TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
10
10
  import type { Context } from '@deepseek-ai/cordis';
11
11
  import Schema from '@deepseek-ai/schemastery';
12
- import type { GitActionRequest, GitActionResult, GitQueryRequest, GitQueryResponse, GitSnapshotRequest, GitSnapshotResult, GitSuggestRequest, GitSuggestResult, GitVersionInfo, GitVersionRequest } from './types.js';
13
- export type { GitSnapshot, GitSnapshotResult, GitSnapshotRequest, GitFailure, GitCommit, GraphCommit, GitRef, GitChange, GitChangeStatus, GitAction, GitActionRequest, GitActionResult, GitErrorCode, GitQuery, GitQueryRequest, GitQueryResponse, GitQueryResult, GitBranch, GitFileStat, WorktreeStats, StashEntry, GitVersionRequest, GitVersionInfo, DiffViewMode, GraphStyle, DirEntry, ResetMode, GitSuggestRequest, GitSuggestResult, GitRemote, RemoteHostKind, SuggestLanguage, ResolvedLanguage, ModelCatalogGroup, ModelCatalogModel, ModelCatalogEffort, ModelCatalogFailure, } from './types.js';
12
+ import type { GitActionRequest, GitActionResult, GitOpenFileRequest, GitOpenFileResult, GitQueryRequest, GitQueryResponse, GitSnapshotRequest, GitSnapshotResult, GitSuggestRequest, GitSuggestResult, GitVersionInfo, GitVersionRequest } from './types.js';
13
+ export type { GitSnapshot, GitSnapshotResult, GitSnapshotRequest, GitFailure, GitCommit, GraphCommit, GitRef, GitChange, GitChangeStatus, GitAction, GitActionRequest, GitActionResult, GitErrorCode, GitQuery, GitQueryRequest, GitQueryResponse, GitQueryResult, GitBranch, GitFileStat, WorktreeStats, StashEntry, GitVersionRequest, GitVersionInfo, DiffViewMode, GraphStyle, DirEntry, ResetMode, GitSuggestRequest, GitSuggestResult, GitRemote, RemoteHostKind, SuggestLanguage, ResolvedLanguage, GitOpenFileRequest, GitOpenFileResult, ModelCatalogGroup, ModelCatalogModel, ModelCatalogEffort, ModelCatalogFailure, } from './types.js';
14
14
  export { normalizeConfig, DEFAULT_CONFIG, snapshotForSession, resolveWorkspace, parsePrimaryRemote, readSuggestLanguage } from './core.js';
15
15
  export { createGitRunner } from './git.js';
16
16
  export { parseStatus, parseGraphLog, parseBranches, parseNameStatus, parseStashList, parseTags, sumNumstat } from './parser.js';
17
17
  export { parseRemote, classifyHost } from './remote.js';
18
18
  export { isSafePath, planAction, runAction, classifyActionFailure } from './actions.js';
19
+ export { openerArgv, canOpenWithSystem, openWithSystem } from './open-file.js';
19
20
  export { runQuery } from './queries.js';
20
21
  export { runSuggest, resolveSuggestLanguage, parseStyleHistory, isCjkText, compactLongLines, LONG_LINE_MAX_CHARS } from './suggest.js';
21
22
  export { readVersionInfo, checkLatestVersion, compareVersions, parseRepository } from './version.js';
@@ -31,6 +32,7 @@ export declare class GitPanelService extends TypertRemoteService {
31
32
  showInputPill: Schema<boolean, boolean, "volatile-defined">;
32
33
  defaultDiffView: Schema<"split" | "unified", "split" | "unified", "volatile-defined">;
33
34
  graphStyle: Schema<"compact" | "parallel", "compact" | "parallel", "volatile-defined">;
35
+ fontDelta: Schema<0 | 1 | -1, 0 | 1 | -1, "volatile-defined">;
34
36
  suggestEnabled: Schema<boolean, boolean, "volatile-defined">;
35
37
  suggestLanguage: Schema<"auto" | "zh" | "en", "auto" | "zh" | "en", "volatile-defined">;
36
38
  suggestMaxBytes: Schema<number, number, "volatile-defined">;
@@ -43,6 +45,7 @@ export declare class GitPanelService extends TypertRemoteService {
43
45
  showInputPill: Schema<boolean, boolean, "volatile-defined">;
44
46
  defaultDiffView: Schema<"split" | "unified", "split" | "unified", "volatile-defined">;
45
47
  graphStyle: Schema<"compact" | "parallel", "compact" | "parallel", "volatile-defined">;
48
+ fontDelta: Schema<0 | 1 | -1, 0 | 1 | -1, "volatile-defined">;
46
49
  suggestEnabled: Schema<boolean, boolean, "volatile-defined">;
47
50
  suggestLanguage: Schema<"auto" | "zh" | "en", "auto" | "zh" | "en", "volatile-defined">;
48
51
  suggestMaxBytes: Schema<number, number, "volatile-defined">;
@@ -63,6 +66,9 @@ export declare class GitPanelService extends TypertRemoteService {
63
66
  suggest(request: GitSuggestRequest, signal?: AbortSignal): Promise<GitSuggestResult>;
64
67
  version(request: GitVersionRequest): Promise<GitVersionInfo>;
65
68
  /** Re-read config so a live-edited volatile field (showInputPill) is current. */
69
+ /** Runs one OS-opener argv; null when the host has no subprocess service. */
70
+ private spawnOpener;
71
+ openFile(request: GitOpenFileRequest): Promise<GitOpenFileResult>;
66
72
  private liveConfig;
67
73
  private withSignal;
68
74
  }
package/lib/host/index.js CHANGED
@@ -46,12 +46,13 @@ var __privateIn = (member, obj) => Object(obj) !== obj ? __typeError('Cannot use
46
46
  var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
47
47
  var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
48
48
  var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
49
- var _version_dec, _suggest_dec, _query_dec, _run_dec, _snapshot_dec, _a, _init;
49
+ var _openFile_dec, _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
52
  import { createGitRunner } from "./git.js";
53
53
  import { createFsDeps } from "./fs-deps.js";
54
- import { normalizeConfig, snapshotForSession, DEFAULT_CONFIG } from "./core.js";
54
+ import { normalizeConfig, resolveBrowseRoot, snapshotForSession, DEFAULT_CONFIG } from "./core.js";
55
+ import { canOpenWithSystem, openWithSystem } from "./open-file.js";
55
56
  import { runAction } from "./actions.js";
56
57
  import { runQuery } from "./queries.js";
57
58
  import { runSuggest } from "./suggest.js";
@@ -61,16 +62,38 @@ import { createGitRunner as createGitRunner2 } from "./git.js";
61
62
  import { parseStatus, parseGraphLog, parseBranches, parseNameStatus, parseStashList, parseTags, sumNumstat } from "./parser.js";
62
63
  import { parseRemote, classifyHost } from "./remote.js";
63
64
  import { isSafePath, planAction, runAction as runAction2, classifyActionFailure } from "./actions.js";
65
+ import { openerArgv, canOpenWithSystem as canOpenWithSystem2, openWithSystem as openWithSystem2 } from "./open-file.js";
64
66
  import { runQuery as runQuery2 } from "./queries.js";
65
67
  import { runSuggest as runSuggest2, resolveSuggestLanguage, parseStyleHistory, isCjkText, compactLongLines, LONG_LINE_MAX_CHARS } from "./suggest.js";
66
68
  import { readVersionInfo as readVersionInfo2, checkLatestVersion as checkLatestVersion2, compareVersions, parseRepository } from "./version.js";
67
- class GitPanelService extends (_a = TypertRemoteService, _snapshot_dec = [Remote("snapshot")], _run_dec = [Remote("run")], _query_dec = [Remote("query")], _suggest_dec = [Remote("suggest")], _version_dec = [Remote("version")], _a) {
69
+ async function spawnDetached(subprocess, argv) {
70
+ try {
71
+ const handle = subprocess.spawn({
72
+ argv,
73
+ cwd: process.cwd(),
74
+ stdio: {
75
+ stdin: "ignore",
76
+ stdout: { collect: { maxBytes: 4096 } },
77
+ stderr: { collect: { maxBytes: 4096 } }
78
+ },
79
+ graceMs: 200
80
+ });
81
+ const outcome = await handle.done;
82
+ return outcome.exitCode;
83
+ } catch {
84
+ return null;
85
+ }
86
+ }
87
+ class GitPanelService extends (_a = TypertRemoteService, _snapshot_dec = [Remote("snapshot")], _run_dec = [Remote("run")], _query_dec = [Remote("query")], _suggest_dec = [Remote("suggest")], _version_dec = [Remote("version")], _openFile_dec = [Remote("openFile")], _a) {
68
88
  constructor(ctx, config) {
69
89
  super(ctx, "gitPanel");
70
90
  __runInitializers(_init, 5, this);
71
91
  __publicField(this, "deps");
72
92
  __publicField(this, "config");
73
93
  __publicField(this, "rawConfig");
94
+ /** Re-read config so a live-edited volatile field (showInputPill) is current. */
95
+ /** Runs one OS-opener argv; null when the host has no subprocess service. */
96
+ __publicField(this, "spawnOpener", null);
74
97
  this.rawConfig = config;
75
98
  this.config = normalizeConfig(config);
76
99
  this.deps = this.buildDeps(ctx, this.config);
@@ -84,6 +107,7 @@ class GitPanelService extends (_a = TypertRemoteService, _snapshot_dec = [Remote
84
107
  const run = subprocess === void 0 ? { run: async () => {
85
108
  throw new Error("subprocess service unavailable");
86
109
  } } : createGitRunner(subprocess, config.timeoutMs, config.maxBytes);
110
+ this.spawnOpener = subprocess === void 0 ? null : (argv) => spawnDetached(subprocess, argv);
87
111
  const sessions = get("sessions");
88
112
  const persistence = get("sessionPersistence");
89
113
  const projections = get("sessionProjections");
@@ -131,7 +155,7 @@ class GitPanelService extends (_a = TypertRemoteService, _snapshot_dec = [Remote
131
155
  };
132
156
  }
133
157
  async snapshot(request, signal) {
134
- return snapshotForSession(this.withSignal(signal), this.liveConfig(), request.sessionId);
158
+ return snapshotForSession(this.withSignal(signal), this.liveConfig(), request.sessionId, { fullChanges: request.fullChanges === true });
135
159
  }
136
160
  async run(request, signal) {
137
161
  return runAction(this.withSignal(signal), this.liveConfig(), request);
@@ -149,7 +173,21 @@ class GitPanelService extends (_a = TypertRemoteService, _snapshot_dec = [Remote
149
173
  return { current: "0.0.0", updateAvailable: false, checkedRemote: false, error: error instanceof Error ? error.message : "version unavailable" };
150
174
  }
151
175
  }
152
- /** Re-read config so a live-edited volatile field (showInputPill) is current. */
176
+ async openFile(request) {
177
+ if (!canOpenWithSystem(process.platform)) {
178
+ return { ok: false, error: { code: "git-error", message: "this host cannot open files in a desktop application" } };
179
+ }
180
+ if (this.spawnOpener === null) {
181
+ return { ok: false, error: { code: "git-error", message: "subprocess service unavailable" } };
182
+ }
183
+ const browse = await resolveBrowseRoot(this.withSignal(void 0), request.sessionId);
184
+ if (!browse.ok) return { ok: false, error: browse.error };
185
+ try {
186
+ return await openWithSystem(this.deps, browse.root, request.path, this.spawnOpener);
187
+ } catch (error) {
188
+ return { ok: false, error: { code: "git-error", message: error instanceof Error ? error.message : String(error) } };
189
+ }
190
+ }
153
191
  liveConfig() {
154
192
  this.config = normalizeConfig(this.rawConfig);
155
193
  return this.config;
@@ -165,6 +203,7 @@ __decorateElement(_init, 1, "run", _run_dec, GitPanelService);
165
203
  __decorateElement(_init, 1, "query", _query_dec, GitPanelService);
166
204
  __decorateElement(_init, 1, "suggest", _suggest_dec, GitPanelService);
167
205
  __decorateElement(_init, 1, "version", _version_dec, GitPanelService);
206
+ __decorateElement(_init, 1, "openFile", _openFile_dec, GitPanelService);
168
207
  __decoratorMetadata(_init, GitPanelService);
169
208
  __publicField(GitPanelService, "inject", ["subprocess", "sessions", "sessionPersistence"]);
170
209
  /**
@@ -183,6 +222,11 @@ __publicField(GitPanelService, "Config", Schema.object({
183
222
  Schema.const("parallel").description("\u5E73\u884C\u7EBF\uFF08VSCode \u98CE\u683C\uFF0C\u5206\u652F\u5404\u5360\u4E00\u5217\uFF09"),
184
223
  Schema.const("compact").description("\u7D27\u51D1\uFF08git log \u98CE\u683C\uFF0C\u5171\u4EAB\u7956\u5148\u5408\u5E76\u4E3A\u4E00\u5217\uFF09")
185
224
  ]).default("parallel").volatile().description("\u63D0\u4EA4\u5386\u53F2\u56FE\u7EBF\u6761\u6837\u5F0F"),
225
+ fontDelta: Schema.union([
226
+ Schema.const(-1).description("\u8F83\u5C0F"),
227
+ Schema.const(0).description("\u9ED8\u8BA4"),
228
+ Schema.const(1).description("\u8F83\u5927")
229
+ ]).default(0).volatile().description("\u9762\u677F\u5B57\u53F7"),
186
230
  suggestEnabled: Schema.boolean().default(true).volatile().description("\u63D0\u4EA4\u6846\u663E\u793A AI \u751F\u6210\u6309\u94AE"),
187
231
  suggestLanguage: Schema.union([
188
232
  Schema.const("auto").description("\u81EA\u52A8\u8DDF\u968F\u4ED3\u5E93\u5386\u53F2\u63D0\u4EA4\u7684\u8BED\u8A00"),
@@ -206,6 +250,7 @@ export {
206
250
  DEFAULT_CONFIG2 as DEFAULT_CONFIG,
207
251
  GitPanelService,
208
252
  LONG_LINE_MAX_CHARS,
253
+ canOpenWithSystem2 as canOpenWithSystem,
209
254
  checkLatestVersion2 as checkLatestVersion,
210
255
  classifyActionFailure,
211
256
  classifyHost,
@@ -216,6 +261,8 @@ export {
216
261
  isCjkText,
217
262
  isSafePath,
218
263
  normalizeConfig2 as normalizeConfig,
264
+ openWithSystem2 as openWithSystem,
265
+ openerArgv,
219
266
  parseBranches,
220
267
  parseGraphLog,
221
268
  parseNameStatus,
@@ -0,0 +1,16 @@
1
+ import type { SnapshotDeps } from './core.js';
2
+ import type { GitOpenFileResult } from './types.js';
3
+ /** Platform argv that opens one file with its default handler. */
4
+ export declare function openerArgv(platform: string, target: string): readonly string[] | null;
5
+ /** Whether this host can hand a file to the desktop at all. */
6
+ export declare function canOpenWithSystem(platform: string): boolean;
7
+ /**
8
+ * Resolve `path` inside `root` and open it with the OS default application.
9
+ *
10
+ * @param deps Host fs face (realpath containment).
11
+ * @param root Browse root for the session.
12
+ * @param path Browse-root-relative path from the client (untrusted).
13
+ * @param spawn Runs one argv and resolves with its exit code, or null on failure.
14
+ * @returns The absolute path on success, a typed failure otherwise.
15
+ */
16
+ export declare function openWithSystem(deps: SnapshotDeps, root: string, path: string, spawn: (argv: readonly string[]) => Promise<number | null>, platform?: string): Promise<GitOpenFileResult>;
@@ -0,0 +1,29 @@
1
+ import { join } from "node:path";
2
+ import { isSafePath } from "./validate.js";
3
+ import { isInsideRoot } from "./queries.js";
4
+ function openerArgv(platform, target) {
5
+ if (platform === "darwin") return ["open", target];
6
+ if (platform === "win32") return ["rundll32", "url.dll,FileProtocolHandler", target];
7
+ if (platform === "linux") return ["xdg-open", target];
8
+ return null;
9
+ }
10
+ function canOpenWithSystem(platform) {
11
+ return openerArgv(platform, "x") !== null;
12
+ }
13
+ async function openWithSystem(deps, root, path, spawn, platform = process.platform) {
14
+ if (!isSafePath(path)) return { ok: false, error: { code: "invalid-path", message: path } };
15
+ const file = join(root, path);
16
+ const inside = await isInsideRoot(deps, root, file, true);
17
+ if (inside === "missing") return { ok: false, error: { code: "file-not-found", message: path } };
18
+ if (inside !== "inside") return { ok: false, error: { code: "invalid-path", message: path } };
19
+ const argv = openerArgv(platform, file);
20
+ if (argv === null) return { ok: false, error: { code: "git-error", message: "no system opener on this platform" } };
21
+ const code = await spawn(argv);
22
+ if (code !== 0) return { ok: false, error: { code: "git-error", message: `the system opener exited ${String(code)}` } };
23
+ return { ok: true, absolutePath: file };
24
+ }
25
+ export {
26
+ canOpenWithSystem,
27
+ openWithSystem,
28
+ openerArgv
29
+ };
@@ -1,3 +1,11 @@
1
1
  import type { SnapshotDeps, GitPanelConfig } from './core.js';
2
2
  import type { GitQueryRequest, GitQueryResponse } from './types.js';
3
3
  export declare function runQuery(deps: SnapshotDeps, config: GitPanelConfig, request: GitQueryRequest): Promise<GitQueryResponse>;
4
+ /** Whether a path resolves inside the browse root, outside its .git tree, or
5
+ * is absent (a deleted entry still listed by a stale tree). */
6
+ export type PathState = 'inside' | 'outside' | 'missing';
7
+ /**
8
+ * Where `path` sits relative to `root` after symlinks are followed: the one
9
+ * containment check shared by every endpoint that touches a browsable file.
10
+ */
11
+ export declare function isInsideRoot(deps: SnapshotDeps, root: string, path: string, hideGit?: boolean): Promise<PathState>;
@@ -561,5 +561,6 @@ async function queryModelCatalog(deps) {
561
561
  return { ok: true, value: { kind: "model-catalog", groups, failures } };
562
562
  }
563
563
  export {
564
+ isInsideRoot,
564
565
  runQuery
565
566
  };
@@ -60,6 +60,9 @@ function systemPrompt(language) {
60
60
  }
61
61
  const UNTRACKED_FILE_CAP = 16;
62
62
  const PROMPT_FILE_CAP = 200;
63
+ function routeNeedsSessionId(provider) {
64
+ return provider.toLowerCase().includes("opencode");
65
+ }
63
66
  function resolveRoute(config, sessionModel, agentDefaultModel) {
64
67
  if (config.suggestProvider !== void 0 && config.suggestModel !== void 0) {
65
68
  return {
@@ -279,7 +282,8 @@ async function runSuggest(deps, config, request) {
279
282
  }],
280
283
  maxTokens: config.suggestMaxOutputTokens,
281
284
  temperature: 0.3,
282
- signal: deadline.signal
285
+ signal: deadline.signal,
286
+ ...routeNeedsSessionId(route.provider) ? { sessionId: request.sessionId } : {}
283
287
  };
284
288
  let output = "";
285
289
  let finish;
@@ -9,6 +9,8 @@ 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
+ /** Panel font-size step: -1 smaller, 0 default, +1 larger (CSS zoom on .gp-panel). */
13
+ export type FontDelta = -1 | 0 | 1;
12
14
  /**
13
15
  * Diff context (-U) values shared by both halves. `DEFAULT` matches git's own
14
16
  * default and is what the host applies when a request omits `context`.
@@ -29,9 +31,16 @@ export declare const DIFF_EXPAND_CONTEXT = 2000;
29
31
  * size the count reports 0 (unknown) and no trailing control is offered.
30
32
  */
31
33
  export declare const DIFF_COUNT_READ_MAX: number;
32
- /** Wire request: the browser sends only session identity, never a path. */
34
+ /** Hard ceiling for a `fullChanges` snapshot (the client's "expand all"):
35
+ * well above the default `maxChanges` (1000) so a big-but-sane change set
36
+ * expands completely, while an absurd one stays bounded on the wire. */
37
+ export declare const FULL_CHANGES_MAX = 10000;
38
+ /** Wire request: the browser sends only session identity, never a path.
39
+ * `fullChanges` asks the host to skip the `maxChanges` slice (still bounded
40
+ * by `FULL_CHANGES_MAX`) so the client can expand a truncated list once. */
33
41
  export interface GitSnapshotRequest {
34
42
  readonly sessionId: string;
43
+ readonly fullChanges?: boolean;
35
44
  }
36
45
  export type GitSnapshotResult = {
37
46
  readonly ok: true;
@@ -116,6 +125,8 @@ export interface GitSnapshot {
116
125
  readonly defaultDiffView: DiffViewMode;
117
126
  /** Commit-graph line style (user preference). */
118
127
  readonly graphStyle: GraphStyle;
128
+ /** Panel font-size step (user preference). */
129
+ readonly fontDelta: FontDelta;
119
130
  /** Whether the commit box shows the AI-suggest button (user preference). */
120
131
  readonly suggestEnabled: boolean;
121
132
  /** SHA-256 of the final business snapshot, excluding checkedAt and itself.
@@ -173,6 +184,8 @@ export type GitAction = {
173
184
  } | {
174
185
  readonly kind: 'discard';
175
186
  readonly paths: readonly string[];
187
+ } | {
188
+ readonly kind: 'discard-all';
176
189
  } | {
177
190
  readonly kind: 'commit';
178
191
  readonly message: string;
@@ -228,7 +241,7 @@ export type GitAction = {
228
241
  /** `git reset` modes, from least to most destructive. Single source so the
229
242
  * client dialog and the command builder cannot drift. */
230
243
  export type ResetMode = 'soft' | 'mixed' | 'hard';
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' | 'file-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';
244
+ export type GitErrorCode = 'cwd-unavailable' | 'not-a-git-repo' | 'git-unavailable' | 'invalid-path' | 'invalid-name' | 'invalid-index' | 'git-error' | 'timeout' | 'cancelled' | 'empty-message' | 'nothing-to-commit' | 'local-changes-block' | 'not-ff' | 'push-rejected' | 'auth-failed' | 'conflict' | 'revert-conflict' | 'revert-stuck' | 'revert-merge' | 'not-found' | 'file-not-found' | 'branch-in-use' | 'stash-switch-failed' | 'index-busy' | 'nested-repo' | 'nested-repo-partial' | 'status-lossy' | 'no-branch' | 'empty-diff' | 'llm-unavailable' | 'llm-error' | 'llm-output' | 'suggest-disabled';
232
245
  export type GitActionResult = {
233
246
  readonly ok: true;
234
247
  readonly snapshot: GitSnapshot;
@@ -649,6 +662,26 @@ export type GitSuggestResult = {
649
662
  export declare const IMAGE_MIME: Readonly<Record<string, string>>;
650
663
  /** MIME for a path's extension; null when it is not a served image type. */
651
664
  export declare function imageMimeFor(path: string): string | null;
665
+ /**
666
+ * Hand one browsable file to the operating system so it opens outside the
667
+ * panel. The path is validated and confirmed inside the browse root before the
668
+ * OS sees it; `sessionId` picks that root.
669
+ */
670
+ export interface GitOpenFileRequest {
671
+ readonly sessionId: string;
672
+ /** Browse-root-relative path of the file to open. */
673
+ readonly path: string;
674
+ }
675
+ export type GitOpenFileResult = {
676
+ readonly ok: true;
677
+ readonly absolutePath: string;
678
+ } | {
679
+ readonly ok: false;
680
+ readonly error: {
681
+ readonly code: GitErrorCode;
682
+ readonly message?: string;
683
+ };
684
+ };
652
685
  export interface GitVersionRequest {
653
686
  /** True → query the GitHub releases API; false/absent → local view only. */
654
687
  readonly check?: boolean;
package/lib/host/types.js CHANGED
@@ -2,6 +2,7 @@ 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
+ const FULL_CHANGES_MAX = 1e4;
5
6
  function isHistoryHashSearch(search) {
6
7
  return /^[0-9a-fA-F]{7,40}$/.test(search.trim());
7
8
  }
@@ -41,6 +42,7 @@ export {
41
42
  DIFF_COUNT_READ_MAX,
42
43
  DIFF_EXPAND_CONTEXT,
43
44
  FILE_COPY_MAX_BYTES,
45
+ FULL_CHANGES_MAX,
44
46
  IMAGE_MIME,
45
47
  SUGGEST_LANGUAGES,
46
48
  SUGGEST_LANGUAGE_DEFAULT,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xbzbing/dsh-git-panel",
3
- "version": "1.8.0",
3
+ "version": "1.8.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",