@xbzbing/dsh-git-panel 1.7.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
- import type { AgentDefaultModelFace, LlmFace } from './llm-face.js';
3
- import type { DiffViewMode, GraphStyle, GitChange, GitErrorCode, GitRemote, GitSnapshotResult, SuggestLanguage } from './types.js';
2
+ import type { AgentDefaultModelFace, LlmFace, SessionModelSelection } from './llm-face.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. */
@@ -26,6 +28,9 @@ export interface GitPanelConfig {
26
28
  /** Optional provider/model route override; absent → the agent default. */
27
29
  readonly suggestProvider?: string;
28
30
  readonly suggestModel?: string;
31
+ /** Adapter-owned effort for the fixed route; absent → omit from the request
32
+ * (the model / adapter default applies). Ignored when the route is not fixed. */
33
+ readonly suggestReasoningEffort?: string;
29
34
  }
30
35
  export declare const DEFAULT_CONFIG: GitPanelConfig;
31
36
  export declare function normalizeConfig(raw: unknown): GitPanelConfig;
@@ -33,6 +38,8 @@ export declare function normalizeConfig(raw: unknown): GitPanelConfig;
33
38
  export declare function readDiffView(value: unknown, fallback: DiffViewMode): DiffViewMode;
34
39
  /** Read the commit-graph style field, unwrapping a schemastery volatile ref. */
35
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;
36
43
  /**
37
44
  * Read the commit-message language field, unwrapping a schemastery volatile
38
45
  * ref. An unknown string (a hand-edited profile, or a value from a newer
@@ -94,14 +101,17 @@ export interface SnapshotDeps {
94
101
  */
95
102
  readonly rootNegCache?: Map<string, number>;
96
103
  /**
97
- * Optional per-request resolvers for the host `llm` and `agentDefaultModel`
98
- * services (see llm-face.ts). Resolved on every suggest call — never frozen
99
- * at construction — so a provider fiber that activates late or restarts is
104
+ * Optional per-request resolvers for the host model services (see
105
+ * llm-face.ts). Resolved on every suggest call — never frozen at
106
+ * construction — so a provider fiber that activates late or restarts is
100
107
  * picked up. Absent in deployments without a model backend; the suggest
101
108
  * endpoint then reports `llm-unavailable` instead of failing activation.
102
109
  */
103
110
  readonly getLlm?: () => LlmFace | undefined;
104
111
  readonly getAgentDefaultModel?: () => AgentDefaultModelFace | undefined;
112
+ /** The session's durable model selection (projection next); undefined when
113
+ * the session has never selected a model. */
114
+ readonly getSessionModel?: (sessionId: string) => SessionModelSelection | undefined;
105
115
  }
106
116
  /**
107
117
  * Pick the primary remote from `git remote -v` output and parse its fetch URL.
@@ -156,6 +166,8 @@ export declare function mapWorkspaceFailure(failure: GitSnapshotResult & {
156
166
  message?: string;
157
167
  };
158
168
  /** Produce a full snapshot for a session. */
159
- 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>;
160
172
  /** Max mtime among changed files (epoch ms), capped for large sets. */
161
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,
@@ -25,9 +26,14 @@ function normalizeConfig(raw) {
25
26
  const raw2 = unwrapVolatile(v);
26
27
  return typeof raw2 === "number" && Number.isFinite(raw2) ? Math.min(limit.max, Math.max(limit.min, Math.floor(raw2))) : limit.default;
27
28
  };
28
- const hasProvider = typeof c.suggestProvider === "string" && c.suggestProvider !== "";
29
- const hasModel = typeof c.suggestModel === "string" && c.suggestModel !== "";
30
- const route = hasProvider && hasModel ? { suggestProvider: c.suggestProvider, suggestModel: c.suggestModel } : {};
29
+ const provider = unwrapVolatile(c.suggestProvider);
30
+ const model = unwrapVolatile(c.suggestModel);
31
+ const effort = unwrapVolatile(c.suggestReasoningEffort);
32
+ const route = typeof provider === "string" && provider !== "" && typeof model === "string" && model !== "" ? {
33
+ suggestProvider: provider,
34
+ suggestModel: model,
35
+ ...typeof effort === "string" && effort !== "" ? { suggestReasoningEffort: effort } : {}
36
+ } : {};
31
37
  return {
32
38
  timeoutMs: num(c.timeoutMs, DEFAULT_CONFIG.timeoutMs),
33
39
  maxBytes: num(c.maxBytes, DEFAULT_CONFIG.maxBytes),
@@ -36,6 +42,7 @@ function normalizeConfig(raw) {
36
42
  showInputPill: readBool(c.showInputPill, DEFAULT_CONFIG.showInputPill),
37
43
  defaultDiffView: readDiffView(c.defaultDiffView, DEFAULT_CONFIG.defaultDiffView),
38
44
  graphStyle: readGraphStyle(c.graphStyle, DEFAULT_CONFIG.graphStyle),
45
+ fontDelta: readFontDelta(c.fontDelta),
39
46
  suggestEnabled: readBool(c.suggestEnabled, DEFAULT_CONFIG.suggestEnabled),
40
47
  suggestLanguage: readSuggestLanguage(c.suggestLanguage, DEFAULT_CONFIG.suggestLanguage),
41
48
  suggestMaxBytes: clampLimit(c.suggestMaxBytes, SUGGEST_LIMITS.maxBytes),
@@ -55,6 +62,10 @@ function readGraphStyle(value, fallback) {
55
62
  const raw = unwrapVolatile(value);
56
63
  return raw === "parallel" || raw === "compact" ? raw : fallback;
57
64
  }
65
+ function readFontDelta(value) {
66
+ const raw = unwrapVolatile(value);
67
+ return raw === -1 || raw === 1 ? raw : 0;
68
+ }
58
69
  function readSuggestLanguage(value, fallback) {
59
70
  const raw = unwrapVolatile(value);
60
71
  return typeof raw === "string" && SUGGEST_LANGUAGES.includes(raw) ? raw : fallback;
@@ -170,7 +181,8 @@ function mapWorkspaceFailure(failure) {
170
181
  const message = "detail" in error ? error.detail : void 0;
171
182
  return { code: error.code, ...message !== void 0 ? { message } : {} };
172
183
  }
173
- 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;
174
186
  const workspace = await resolveWorkspace(deps, sessionId);
175
187
  if (!workspace.ok) {
176
188
  const failure = workspace.failure;
@@ -209,7 +221,7 @@ async function snapshotForSession(deps, config, sessionId) {
209
221
  let untrackedCollapsed = false;
210
222
  const untrackedEntries = allChanges.filter((c) => c.status === "untracked");
211
223
  const collapseCandidate = untrackedEntries.some((c) => c.path.includes("/"));
212
- if (statusLossy || untrackedEntries.length > config.maxChanges && collapseCandidate) {
224
+ if (statusLossy || untrackedEntries.length > changeLimit && collapseCandidate) {
213
225
  const collapsed = await runCommand(deps.run, ["git", "status", "--porcelain=v1", "-z", "--untracked-files=normal"], root, "status-collapsed", deps.signal);
214
226
  if ("run" in collapsed && !collapsed.run.timedOut && collapsed.run.exitCode === 0) {
215
227
  const list = parseStatus(collapsed.run.stdout);
@@ -218,9 +230,9 @@ async function snapshotForSession(deps, config, sessionId) {
218
230
  statusLossy = collapsed.run.stdoutLossy;
219
231
  }
220
232
  }
221
- const truncated = allChanges.length > config.maxChanges || statusLossy;
222
- const truncatedReason = statusLossy ? "lossy" : allChanges.length > config.maxChanges ? "cap" : void 0;
223
- 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;
224
236
  let staged = 0;
225
237
  let modified = 0;
226
238
  let untracked = 0;
@@ -295,6 +307,7 @@ async function snapshotForSession(deps, config, sessionId) {
295
307
  showInputPill: config.showInputPill,
296
308
  defaultDiffView: config.defaultDiffView,
297
309
  graphStyle: config.graphStyle,
310
+ fontDelta: config.fontDelta,
298
311
  suggestEnabled: config.suggestEnabled,
299
312
  checkedAt: Date.now()
300
313
  };
@@ -323,6 +336,7 @@ export {
323
336
  parsePrimaryRemote,
324
337
  readBool,
325
338
  readDiffView,
339
+ readFontDelta,
326
340
  readGraphStyle,
327
341
  readSuggestLanguage,
328
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, } 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,20 +32,28 @@ 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">;
37
39
  suggestMaxOutputTokens: Schema<number, number, "volatile-defined">;
38
40
  suggestTimeoutMs: Schema<number, number, "volatile-defined">;
41
+ suggestProvider: Schema<string, string, "volatile">;
42
+ suggestModel: Schema<string, string, "volatile">;
43
+ suggestReasoningEffort: Schema<string, string, "volatile">;
39
44
  }>>, Schemastery.ObjectT<NoInfer<{
40
45
  showInputPill: Schema<boolean, boolean, "volatile-defined">;
41
46
  defaultDiffView: Schema<"split" | "unified", "split" | "unified", "volatile-defined">;
42
47
  graphStyle: Schema<"compact" | "parallel", "compact" | "parallel", "volatile-defined">;
48
+ fontDelta: Schema<0 | 1 | -1, 0 | 1 | -1, "volatile-defined">;
43
49
  suggestEnabled: Schema<boolean, boolean, "volatile-defined">;
44
50
  suggestLanguage: Schema<"auto" | "zh" | "en", "auto" | "zh" | "en", "volatile-defined">;
45
51
  suggestMaxBytes: Schema<number, number, "volatile-defined">;
46
52
  suggestMaxOutputTokens: Schema<number, number, "volatile-defined">;
47
53
  suggestTimeoutMs: Schema<number, number, "volatile-defined">;
54
+ suggestProvider: Schema<string, string, "volatile">;
55
+ suggestModel: Schema<string, string, "volatile">;
56
+ suggestReasoningEffort: Schema<string, string, "volatile">;
48
57
  }>>, "plain">;
49
58
  private readonly deps;
50
59
  private config;
@@ -57,6 +66,9 @@ export declare class GitPanelService extends TypertRemoteService {
57
66
  suggest(request: GitSuggestRequest, signal?: AbortSignal): Promise<GitSuggestResult>;
58
67
  version(request: GitVersionRequest): Promise<GitVersionInfo>;
59
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>;
60
72
  private liveConfig;
61
73
  private withSignal;
62
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,8 +107,10 @@ 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");
113
+ const projections = get("sessionProjections");
89
114
  return {
90
115
  run,
91
116
  fs,
@@ -108,11 +133,29 @@ class GitPanelService extends (_a = TypertRemoteService, _snapshot_dec = [Remote
108
133
  // a model backend, where the suggest endpoint reports `llm-unavailable`
109
134
  // rather than failing activation (kept out of `static inject` on purpose).
110
135
  getLlm: () => get("llm"),
111
- getAgentDefaultModel: () => get("agentDefaultModel")
136
+ getAgentDefaultModel: () => get("agentDefaultModel"),
137
+ // Session model selection: projection `next` is pending ?? lastUsed —
138
+ // the route the next prompt in this session would use.
139
+ getSessionModel: (sessionId) => {
140
+ const session = sessions?.get(sessionId);
141
+ if (session === void 0 || projections === void 0) return void 0;
142
+ try {
143
+ const next = projections.snapshot(session, ["modelSelection"]).values?.modelSelection?.next;
144
+ if (next == null || typeof next.provider !== "string" || typeof next.model !== "string") return void 0;
145
+ if (next.provider === "" || next.model === "") return void 0;
146
+ return {
147
+ provider: next.provider,
148
+ model: next.model,
149
+ ...typeof next.reasoningEffort === "string" && next.reasoningEffort !== "" ? { reasoningEffort: next.reasoningEffort } : {}
150
+ };
151
+ } catch {
152
+ return void 0;
153
+ }
154
+ }
112
155
  };
113
156
  }
114
157
  async snapshot(request, signal) {
115
- return snapshotForSession(this.withSignal(signal), this.liveConfig(), request.sessionId);
158
+ return snapshotForSession(this.withSignal(signal), this.liveConfig(), request.sessionId, { fullChanges: request.fullChanges === true });
116
159
  }
117
160
  async run(request, signal) {
118
161
  return runAction(this.withSignal(signal), this.liveConfig(), request);
@@ -130,7 +173,21 @@ class GitPanelService extends (_a = TypertRemoteService, _snapshot_dec = [Remote
130
173
  return { current: "0.0.0", updateAvailable: false, checkedRemote: false, error: error instanceof Error ? error.message : "version unavailable" };
131
174
  }
132
175
  }
133
- /** 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
+ }
134
191
  liveConfig() {
135
192
  this.config = normalizeConfig(this.rawConfig);
136
193
  return this.config;
@@ -146,6 +203,7 @@ __decorateElement(_init, 1, "run", _run_dec, GitPanelService);
146
203
  __decorateElement(_init, 1, "query", _query_dec, GitPanelService);
147
204
  __decorateElement(_init, 1, "suggest", _suggest_dec, GitPanelService);
148
205
  __decorateElement(_init, 1, "version", _version_dec, GitPanelService);
206
+ __decorateElement(_init, 1, "openFile", _openFile_dec, GitPanelService);
149
207
  __decoratorMetadata(_init, GitPanelService);
150
208
  __publicField(GitPanelService, "inject", ["subprocess", "sessions", "sessionPersistence"]);
151
209
  /**
@@ -164,6 +222,11 @@ __publicField(GitPanelService, "Config", Schema.object({
164
222
  Schema.const("parallel").description("\u5E73\u884C\u7EBF\uFF08VSCode \u98CE\u683C\uFF0C\u5206\u652F\u5404\u5360\u4E00\u5217\uFF09"),
165
223
  Schema.const("compact").description("\u7D27\u51D1\uFF08git log \u98CE\u683C\uFF0C\u5171\u4EAB\u7956\u5148\u5408\u5E76\u4E3A\u4E00\u5217\uFF09")
166
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"),
167
230
  suggestEnabled: Schema.boolean().default(true).volatile().description("\u63D0\u4EA4\u6846\u663E\u793A AI \u751F\u6210\u6309\u94AE"),
168
231
  suggestLanguage: Schema.union([
169
232
  Schema.const("auto").description("\u81EA\u52A8\u8DDF\u968F\u4ED3\u5E93\u5386\u53F2\u63D0\u4EA4\u7684\u8BED\u8A00"),
@@ -174,13 +237,20 @@ __publicField(GitPanelService, "Config", Schema.object({
174
237
  // a literal here could silently drift from what normalizeConfig enforces.
175
238
  suggestMaxBytes: Schema.number().default(DEFAULT_CONFIG.suggestMaxBytes).volatile().description("AI \u751F\u6210\u8F93\u5165\u9884\u7B97\uFF08diff \u5B57\u8282\u4E0A\u9650\uFF0C16\u2013512 KiB\uFF09"),
176
239
  suggestMaxOutputTokens: Schema.number().default(DEFAULT_CONFIG.suggestMaxOutputTokens).volatile().description("AI \u751F\u6210\u8F93\u51FA\u4E0A\u9650\uFF08tokens\uFF0C256\u201316384\uFF09"),
177
- suggestTimeoutMs: Schema.number().default(DEFAULT_CONFIG.suggestTimeoutMs).volatile().description("AI \u751F\u6210\u8D85\u65F6\uFF08\u6BEB\u79D2\uFF0C5\u2013120 \u79D2\uFF09")
240
+ suggestTimeoutMs: Schema.number().default(DEFAULT_CONFIG.suggestTimeoutMs).volatile().description("AI \u751F\u6210\u8D85\u65F6\uFF08\u6BEB\u79D2\uFF0C5\u2013120 \u79D2\uFF09"),
241
+ // Fixed model route: both halves of the pair must be non-empty to take
242
+ // effect (normalizeConfig); effort is omitted from the request when blank.
243
+ // No .default(): without one the field stays optional in the object schema.
244
+ suggestProvider: Schema.string().volatile().description("AI \u751F\u6210\u56FA\u5B9A\u6A21\u578B\u7684 provider\uFF08\u7559\u7A7A = \u8DDF\u968F\u4F1A\u8BDD\u9ED8\u8BA4\uFF09"),
245
+ suggestModel: Schema.string().volatile().description("AI \u751F\u6210\u56FA\u5B9A\u6A21\u578B\u7684 model id\uFF08\u987B\u4E0E provider \u6210\u5BF9\uFF09"),
246
+ suggestReasoningEffort: Schema.string().volatile().description("AI \u751F\u6210\u56FA\u5B9A\u6A21\u578B\u7684\u63A8\u7406\u5F3A\u5EA6\uFF08\u7559\u7A7A = \u4E0D\u8BBE\u7F6E\uFF0C\u7528\u6A21\u578B\u9ED8\u8BA4\uFF09")
178
247
  }));
179
248
  var index_default = GitPanelService;
180
249
  export {
181
250
  DEFAULT_CONFIG2 as DEFAULT_CONFIG,
182
251
  GitPanelService,
183
252
  LONG_LINE_MAX_CHARS,
253
+ canOpenWithSystem2 as canOpenWithSystem,
184
254
  checkLatestVersion2 as checkLatestVersion,
185
255
  classifyActionFailure,
186
256
  classifyHost,
@@ -191,6 +261,8 @@ export {
191
261
  isCjkText,
192
262
  isSafePath,
193
263
  normalizeConfig2 as normalizeConfig,
264
+ openWithSystem2 as openWithSystem,
265
+ openerArgv,
194
266
  parseBranches,
195
267
  parseGraphLog,
196
268
  parseNameStatus,
@@ -16,7 +16,22 @@ export interface LlmFace {
16
16
  stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
17
17
  listProviders?(): readonly {
18
18
  readonly id: string;
19
+ readonly name?: string;
19
20
  }[];
21
+ /** Adapter-advertised models for one provider; optional on the wire. */
22
+ listModels?(provider: string): Promise<readonly {
23
+ readonly id: string;
24
+ readonly name?: string;
25
+ }[]>;
26
+ /** Exact-route metadata (reasoning efforts etc.); optional on the wire. */
27
+ resolveModelInfo?(provider: string, model: string, signal?: AbortSignal): Promise<{
28
+ readonly reasoning?: {
29
+ readonly efforts: readonly {
30
+ readonly id: string;
31
+ readonly name: string;
32
+ }[];
33
+ };
34
+ }>;
20
35
  }
21
36
  /** Minimal face of the host `agentDefaultModel` service. */
22
37
  export interface AgentDefaultModelFace {
@@ -25,3 +40,9 @@ export interface AgentDefaultModelFace {
25
40
  readonly model: string;
26
41
  } | undefined;
27
42
  }
43
+ /** One session's durable model selection (projection `next`: pending, else lastUsed). */
44
+ export interface SessionModelSelection {
45
+ readonly provider: string;
46
+ readonly model: string;
47
+ readonly reasoningEffort?: string;
48
+ }
@@ -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>;