@xbzbing/dsh-git-panel 1.7.0 → 1.8.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,5 +1,5 @@
1
1
  import type { GitRunner } from './git.js';
2
- import type { AgentDefaultModelFace, LlmFace } from './llm-face.js';
2
+ import type { AgentDefaultModelFace, LlmFace, SessionModelSelection } from './llm-face.js';
3
3
  import type { DiffViewMode, GraphStyle, GitChange, GitErrorCode, GitRemote, GitSnapshotResult, SuggestLanguage } from './types.js';
4
4
  export interface GitPanelConfig {
5
5
  readonly timeoutMs: number;
@@ -26,6 +26,9 @@ export interface GitPanelConfig {
26
26
  /** Optional provider/model route override; absent → the agent default. */
27
27
  readonly suggestProvider?: string;
28
28
  readonly suggestModel?: string;
29
+ /** Adapter-owned effort for the fixed route; absent → omit from the request
30
+ * (the model / adapter default applies). Ignored when the route is not fixed. */
31
+ readonly suggestReasoningEffort?: string;
29
32
  }
30
33
  export declare const DEFAULT_CONFIG: GitPanelConfig;
31
34
  export declare function normalizeConfig(raw: unknown): GitPanelConfig;
@@ -94,14 +97,17 @@ export interface SnapshotDeps {
94
97
  */
95
98
  readonly rootNegCache?: Map<string, number>;
96
99
  /**
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
100
+ * Optional per-request resolvers for the host model services (see
101
+ * llm-face.ts). Resolved on every suggest call — never frozen at
102
+ * construction — so a provider fiber that activates late or restarts is
100
103
  * picked up. Absent in deployments without a model backend; the suggest
101
104
  * endpoint then reports `llm-unavailable` instead of failing activation.
102
105
  */
103
106
  readonly getLlm?: () => LlmFace | undefined;
104
107
  readonly getAgentDefaultModel?: () => AgentDefaultModelFace | undefined;
108
+ /** The session's durable model selection (projection next); undefined when
109
+ * the session has never selected a model. */
110
+ readonly getSessionModel?: (sessionId: string) => SessionModelSelection | undefined;
105
111
  }
106
112
  /**
107
113
  * Pick the primary remote from `git remote -v` output and parse its fetch URL.
package/lib/host/core.js CHANGED
@@ -25,9 +25,14 @@ function normalizeConfig(raw) {
25
25
  const raw2 = unwrapVolatile(v);
26
26
  return typeof raw2 === "number" && Number.isFinite(raw2) ? Math.min(limit.max, Math.max(limit.min, Math.floor(raw2))) : limit.default;
27
27
  };
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 } : {};
28
+ const provider = unwrapVolatile(c.suggestProvider);
29
+ const model = unwrapVolatile(c.suggestModel);
30
+ const effort = unwrapVolatile(c.suggestReasoningEffort);
31
+ const route = typeof provider === "string" && provider !== "" && typeof model === "string" && model !== "" ? {
32
+ suggestProvider: provider,
33
+ suggestModel: model,
34
+ ...typeof effort === "string" && effort !== "" ? { suggestReasoningEffort: effort } : {}
35
+ } : {};
31
36
  return {
32
37
  timeoutMs: num(c.timeoutMs, DEFAULT_CONFIG.timeoutMs),
33
38
  maxBytes: num(c.maxBytes, DEFAULT_CONFIG.maxBytes),
@@ -10,7 +10,7 @@ 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
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';
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';
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';
@@ -36,6 +36,9 @@ export declare class GitPanelService extends TypertRemoteService {
36
36
  suggestMaxBytes: Schema<number, number, "volatile-defined">;
37
37
  suggestMaxOutputTokens: Schema<number, number, "volatile-defined">;
38
38
  suggestTimeoutMs: Schema<number, number, "volatile-defined">;
39
+ suggestProvider: Schema<string, string, "volatile">;
40
+ suggestModel: Schema<string, string, "volatile">;
41
+ suggestReasoningEffort: Schema<string, string, "volatile">;
39
42
  }>>, Schemastery.ObjectT<NoInfer<{
40
43
  showInputPill: Schema<boolean, boolean, "volatile-defined">;
41
44
  defaultDiffView: Schema<"split" | "unified", "split" | "unified", "volatile-defined">;
@@ -45,6 +48,9 @@ export declare class GitPanelService extends TypertRemoteService {
45
48
  suggestMaxBytes: Schema<number, number, "volatile-defined">;
46
49
  suggestMaxOutputTokens: Schema<number, number, "volatile-defined">;
47
50
  suggestTimeoutMs: Schema<number, number, "volatile-defined">;
51
+ suggestProvider: Schema<string, string, "volatile">;
52
+ suggestModel: Schema<string, string, "volatile">;
53
+ suggestReasoningEffort: Schema<string, string, "volatile">;
48
54
  }>>, "plain">;
49
55
  private readonly deps;
50
56
  private config;
package/lib/host/index.js CHANGED
@@ -86,6 +86,7 @@ class GitPanelService extends (_a = TypertRemoteService, _snapshot_dec = [Remote
86
86
  } } : createGitRunner(subprocess, config.timeoutMs, config.maxBytes);
87
87
  const sessions = get("sessions");
88
88
  const persistence = get("sessionPersistence");
89
+ const projections = get("sessionProjections");
89
90
  return {
90
91
  run,
91
92
  fs,
@@ -108,7 +109,25 @@ class GitPanelService extends (_a = TypertRemoteService, _snapshot_dec = [Remote
108
109
  // a model backend, where the suggest endpoint reports `llm-unavailable`
109
110
  // rather than failing activation (kept out of `static inject` on purpose).
110
111
  getLlm: () => get("llm"),
111
- getAgentDefaultModel: () => get("agentDefaultModel")
112
+ getAgentDefaultModel: () => get("agentDefaultModel"),
113
+ // Session model selection: projection `next` is pending ?? lastUsed —
114
+ // the route the next prompt in this session would use.
115
+ getSessionModel: (sessionId) => {
116
+ const session = sessions?.get(sessionId);
117
+ if (session === void 0 || projections === void 0) return void 0;
118
+ try {
119
+ const next = projections.snapshot(session, ["modelSelection"]).values?.modelSelection?.next;
120
+ if (next == null || typeof next.provider !== "string" || typeof next.model !== "string") return void 0;
121
+ if (next.provider === "" || next.model === "") return void 0;
122
+ return {
123
+ provider: next.provider,
124
+ model: next.model,
125
+ ...typeof next.reasoningEffort === "string" && next.reasoningEffort !== "" ? { reasoningEffort: next.reasoningEffort } : {}
126
+ };
127
+ } catch {
128
+ return void 0;
129
+ }
130
+ }
112
131
  };
113
132
  }
114
133
  async snapshot(request, signal) {
@@ -174,7 +193,13 @@ __publicField(GitPanelService, "Config", Schema.object({
174
193
  // a literal here could silently drift from what normalizeConfig enforces.
175
194
  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
195
  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")
196
+ suggestTimeoutMs: Schema.number().default(DEFAULT_CONFIG.suggestTimeoutMs).volatile().description("AI \u751F\u6210\u8D85\u65F6\uFF08\u6BEB\u79D2\uFF0C5\u2013120 \u79D2\uFF09"),
197
+ // Fixed model route: both halves of the pair must be non-empty to take
198
+ // effect (normalizeConfig); effort is omitted from the request when blank.
199
+ // No .default(): without one the field stays optional in the object schema.
200
+ suggestProvider: Schema.string().volatile().description("AI \u751F\u6210\u56FA\u5B9A\u6A21\u578B\u7684 provider\uFF08\u7559\u7A7A = \u8DDF\u968F\u4F1A\u8BDD\u9ED8\u8BA4\uFF09"),
201
+ suggestModel: Schema.string().volatile().description("AI \u751F\u6210\u56FA\u5B9A\u6A21\u578B\u7684 model id\uFF08\u987B\u4E0E provider \u6210\u5BF9\uFF09"),
202
+ 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
203
  }));
179
204
  var index_default = GitPanelService;
180
205
  export {
@@ -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
+ }
@@ -9,6 +9,13 @@ const FIRST_PARENT_DIFF = "--diff-merges=first-parent";
9
9
  const VISIBLE_REFS = ["--branches", "--remotes", "--tags", "HEAD"];
10
10
  async function runQuery(deps, config, request) {
11
11
  const q = request.query;
12
+ if (q.kind === "model-catalog") {
13
+ try {
14
+ return await queryModelCatalog(deps);
15
+ } catch (error) {
16
+ return { ok: false, error: { code: "git-error", message: error instanceof Error ? error.message : String(error) } };
17
+ }
18
+ }
12
19
  if (q.kind === "dir-list" || q.kind === "file-content") {
13
20
  const browse = await resolveBrowseRoot(deps, request.sessionId);
14
21
  if (!browse.ok) return { ok: false, error: browse.error };
@@ -307,7 +314,8 @@ async function queryDirList(deps, root, q, isGitRepo) {
307
314
  if (rel !== "" && !isSafePath(rel) || hasGitSegment(rel)) return { ok: false, error: { code: "invalid-path", message: rel } };
308
315
  const dir = rel === "" ? root : join(root, rel);
309
316
  const inside = await isInsideRoot(deps, root, dir, true);
310
- if (!inside) return { ok: false, error: { code: "invalid-path", message: rel } };
317
+ if (inside === "missing") return { ok: false, error: { code: "file-not-found", message: rel } };
318
+ if (inside === "outside") return { ok: false, error: { code: "invalid-path", message: rel } };
311
319
  let raw;
312
320
  try {
313
321
  raw = await deps.fs.readdir(dir);
@@ -358,12 +366,13 @@ async function queryFileContent(deps, config, root, q) {
358
366
  if (!isSafePath(q.path) || hasGitSegment(q.path)) return { ok: false, error: { code: "invalid-path", message: q.path } };
359
367
  const file = join(root, q.path);
360
368
  const inside = await isInsideRoot(deps, root, file, true);
361
- if (!inside) return { ok: false, error: { code: "invalid-path", message: q.path } };
369
+ if (inside === "missing") return { ok: false, error: { code: "file-not-found", message: q.path } };
370
+ if (inside === "outside") return { ok: false, error: { code: "invalid-path", message: q.path } };
362
371
  let info;
363
372
  try {
364
373
  info = await deps.fs.stat(file);
365
374
  } catch (error) {
366
- return { ok: false, error: { code: "git-error", message: error instanceof Error ? error.message : "stat failed" } };
375
+ return { ok: false, error: { code: isMissingPath(error) ? "file-not-found" : "git-error", message: error instanceof Error ? error.message : "stat failed" } };
367
376
  }
368
377
  const mime = imageMimeFor(q.path);
369
378
  const cap = mime !== null ? config.maxBytes : Math.max(config.maxBytes, FILE_COPY_MAX_BYTES);
@@ -374,7 +383,7 @@ async function queryFileContent(deps, config, root, q) {
374
383
  try {
375
384
  buf = await deps.fs.readFile(file);
376
385
  } catch (error) {
377
- return { ok: false, error: { code: "git-error", message: error instanceof Error ? error.message : "read failed" } };
386
+ return { ok: false, error: { code: isMissingPath(error) ? "file-not-found" : "git-error", message: error instanceof Error ? error.message : "read failed" } };
378
387
  }
379
388
  if (buf.length > cap) return tooLarge;
380
389
  if (mime !== null) {
@@ -383,19 +392,22 @@ async function queryFileContent(deps, config, root, q) {
383
392
  if (isBinaryBuffer(buf)) return { ok: true, value: { kind: "file-content", path: q.path, variant: "binary", ...statFields } };
384
393
  const content = buf.toString("utf8");
385
394
  const lines = content === "" ? 0 : content.split("\n").length - (content.endsWith("\n") ? 1 : 0);
386
- return { ok: true, value: { kind: "file-content", path: q.path, variant: "text", content, lines, ...statFields } };
395
+ return { ok: true, value: { kind: "file-content", path: q.path, variant: "text", content, lines, absolutePath: file, ...statFields } };
387
396
  }
388
397
  function hasGitSegment(path) {
389
398
  return path.split(/[\\/]/).includes(".git");
390
399
  }
400
+ function isMissingPath(error) {
401
+ return error?.code === "ENOENT";
402
+ }
391
403
  async function isInsideRoot(deps, root, path, hideGit = false) {
392
404
  try {
393
405
  const real = await deps.fs.realpath(path);
394
406
  const rootReal = await deps.fs.realpath(root);
395
- if (real !== rootReal && !real.startsWith(rootReal + sep)) return false;
396
- return !hideGit || !hasGitSegment(real.slice(rootReal.length + 1));
397
- } catch {
398
- return false;
407
+ if (real !== rootReal && !real.startsWith(rootReal + sep)) return "outside";
408
+ return !hideGit || !hasGitSegment(real.slice(rootReal.length + 1)) ? "inside" : "outside";
409
+ } catch (error) {
410
+ return isMissingPath(error) ? "missing" : "outside";
399
411
  }
400
412
  }
401
413
  function isBinaryBuffer(buf) {
@@ -501,6 +513,53 @@ async function queryWorktreeStats(deps, config, sessionId) {
501
513
  if (!snapshot.ok) return { ok: false, error: { code: "git-error", message: "snapshot failed" } };
502
514
  return { ok: true, value: { kind: "worktree-stats", stats: snapshot.value.stats } };
503
515
  }
516
+ async function queryModelCatalog(deps) {
517
+ const llm = deps.getLlm?.();
518
+ const empty = { kind: "model-catalog", groups: [], failures: [] };
519
+ if (llm === void 0 || typeof llm.listModels !== "function") {
520
+ return { ok: true, value: empty };
521
+ }
522
+ const listModels = llm.listModels.bind(llm);
523
+ const resolveModelInfo = typeof llm.resolveModelInfo === "function" ? llm.resolveModelInfo.bind(llm) : void 0;
524
+ const providers = llm.listProviders?.() ?? [];
525
+ const results = await Promise.all(providers.map(async (provider) => {
526
+ const name = provider.name ?? provider.id;
527
+ let listed;
528
+ try {
529
+ listed = await listModels(provider.id);
530
+ } catch (error) {
531
+ return { id: provider.id, name, message: error instanceof Error ? error.message : String(error) };
532
+ }
533
+ const models = await Promise.all(listed.map(async (model) => {
534
+ const entry = { id: model.id, name: model.name ?? model.id };
535
+ if (resolveModelInfo === void 0) return entry;
536
+ try {
537
+ const info = await resolveModelInfo(provider.id, model.id, deps.signal);
538
+ const reasoning = info.reasoning;
539
+ if (reasoning !== void 0 && reasoning.efforts.length > 0) {
540
+ return {
541
+ ...entry,
542
+ reasoning: {
543
+ efforts: reasoning.efforts.map((effort) => ({ id: effort.id, name: effort.name }))
544
+ }
545
+ };
546
+ }
547
+ } catch {
548
+ }
549
+ return entry;
550
+ }));
551
+ if (models.length === 0) return null;
552
+ return { id: provider.id, name, models };
553
+ }));
554
+ const groups = [];
555
+ const failures = [];
556
+ for (const one of results) {
557
+ if (one === null) continue;
558
+ if ("models" in one) groups.push(one);
559
+ else failures.push(one);
560
+ }
561
+ return { ok: true, value: { kind: "model-catalog", groups, failures } };
562
+ }
504
563
  export {
505
564
  runQuery
506
565
  };
@@ -60,9 +60,20 @@ function systemPrompt(language) {
60
60
  }
61
61
  const UNTRACKED_FILE_CAP = 16;
62
62
  const PROMPT_FILE_CAP = 200;
63
- function resolveRoute(config, agentDefaultModel) {
63
+ function resolveRoute(config, sessionModel, agentDefaultModel) {
64
64
  if (config.suggestProvider !== void 0 && config.suggestModel !== void 0) {
65
- return { provider: config.suggestProvider, model: config.suggestModel };
65
+ return {
66
+ provider: config.suggestProvider,
67
+ model: config.suggestModel,
68
+ ...config.suggestReasoningEffort !== void 0 ? { reasoningEffort: config.suggestReasoningEffort } : {}
69
+ };
70
+ }
71
+ if (sessionModel !== void 0) {
72
+ return {
73
+ provider: sessionModel.provider,
74
+ model: sessionModel.model,
75
+ ...sessionModel.reasoningEffort !== void 0 ? { reasoningEffort: sessionModel.reasoningEffort } : {}
76
+ };
66
77
  }
67
78
  return agentDefaultModel?.currentSelection();
68
79
  }
@@ -238,7 +249,7 @@ async function runSuggest(deps, config, request) {
238
249
  diff = untracked.diff;
239
250
  truncated = truncated || untracked.truncated;
240
251
  const llm = deps.getLlm?.();
241
- const route = resolveRoute(config, deps.getAgentDefaultModel?.());
252
+ const route = resolveRoute(config, deps.getSessionModel?.(request.sessionId), deps.getAgentDefaultModel?.());
242
253
  if (llm === void 0 || route === void 0) {
243
254
  return { ok: false, error: { code: "llm-unavailable", message: "no model service or route is configured" } };
244
255
  }
@@ -252,6 +263,10 @@ async function runSuggest(deps, config, request) {
252
263
  const options = {
253
264
  provider: route.provider,
254
265
  model: route.model,
266
+ // Catalog effort ids come from the same adapter surface GenerateOptions
267
+ // accepts; the brand is compile-time only (the runtime factory lives in
268
+ // @deepseek-ai/dsh-llm, which is externalized), so a cast is the boundary.
269
+ ...route.reasoningEffort !== void 0 ? { reasoningEffort: route.reasoningEffort } : {},
255
270
  system: systemPrompt(language),
256
271
  messages: [{
257
272
  id: "git-panel-suggest",
@@ -292,6 +307,7 @@ async function runSuggest(deps, config, request) {
292
307
  message,
293
308
  provider: route.provider,
294
309
  model: route.model,
310
+ ...route.reasoningEffort !== void 0 ? { reasoningEffort: route.reasoningEffort } : {},
295
311
  ...truncated ? { truncated: true } : {}
296
312
  }
297
313
  };
@@ -228,7 +228,7 @@ export type GitAction = {
228
228
  /** `git reset` modes, from least to most destructive. Single source so the
229
229
  * client dialog and the command builder cannot drift. */
230
230
  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' | 'branch-in-use' | 'stash-switch-failed' | 'index-busy' | 'nested-repo' | 'nested-repo-partial' | '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' | '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';
232
232
  export type GitActionResult = {
233
233
  readonly ok: true;
234
234
  readonly snapshot: GitSnapshot;
@@ -324,6 +324,8 @@ export type GitQuery = {
324
324
  readonly kind: 'pull-preview';
325
325
  } | {
326
326
  readonly kind: 'push-preview';
327
+ } | {
328
+ readonly kind: 'model-catalog';
327
329
  };
328
330
  /** One entry in a `dir-list` result. */
329
331
  export interface DirEntry {
@@ -358,6 +360,33 @@ export interface StashEntry {
358
360
  /** Human relative time (e.g. "2 hours ago"). */
359
361
  readonly relTime: string;
360
362
  }
363
+ /** Selectable reasoning effort for one exact model route (ids go straight
364
+ * into `GenerateOptions.reasoningEffort`). */
365
+ export interface ModelCatalogEffort {
366
+ readonly id: string;
367
+ readonly name: string;
368
+ }
369
+ /** One model inside a provider group of the host-level catalog. */
370
+ export interface ModelCatalogModel {
371
+ readonly id: string;
372
+ readonly name: string;
373
+ /** Adapter-advertised efforts; absent when the model exposes none. */
374
+ readonly reasoning?: {
375
+ readonly efforts: readonly ModelCatalogEffort[];
376
+ };
377
+ }
378
+ /** One provider and its successfully listed models. */
379
+ export interface ModelCatalogGroup {
380
+ readonly id: string;
381
+ readonly name: string;
382
+ readonly models: readonly ModelCatalogModel[];
383
+ }
384
+ /** One provider / model whose catalog lookup failed in isolation. */
385
+ export interface ModelCatalogFailure {
386
+ readonly id: string;
387
+ readonly name: string;
388
+ readonly message: string;
389
+ }
361
390
  /** Working-tree statistics for the changes page header. */
362
391
  export interface WorktreeStats {
363
392
  /** Distinct changed-file paths (staged ∪ modified ∪ untracked). */
@@ -492,6 +521,10 @@ export type GitQueryResult = {
492
521
  readonly files: number;
493
522
  readonly insertions: number;
494
523
  readonly deletions: number;
524
+ } | {
525
+ readonly kind: 'model-catalog';
526
+ readonly groups: readonly ModelCatalogGroup[];
527
+ readonly failures: readonly ModelCatalogFailure[];
495
528
  } | {
496
529
  readonly kind: 'dir-list';
497
530
  readonly path: string;
@@ -505,6 +538,11 @@ export type GitQueryResult = {
505
538
  readonly content?: string;
506
539
  /** text variant: total line count. */
507
540
  readonly lines?: number;
541
+ /** text variant: absolute path of this file — the rendered-Markdown
542
+ * image resolver needs the document's own location to resolve relative
543
+ * destinations onto the authenticated `api/file` route. Absent on older
544
+ * hosts; the client then keeps the alt-text fallback. */
545
+ readonly absolutePath?: string;
508
546
  /** image variant: a `data:<mime>;base64,…` URL. */
509
547
  readonly dataUrl?: string;
510
548
  /** The file exceeded the byte cap, so no content is returned. */
@@ -591,6 +629,8 @@ export type GitSuggestResult = {
591
629
  /** Actual provider/model route used (display only). */
592
630
  readonly provider?: string;
593
631
  readonly model?: string;
632
+ /** Effort actually sent; absent when the request omitted it. */
633
+ readonly reasoningEffort?: string;
594
634
  /** The diff reached the model truncated: over the input cap, or cut
595
635
  * by the per-command runner stdout cap before it. */
596
636
  readonly truncated?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xbzbing/dsh-git-panel",
3
- "version": "1.7.0",
3
+ "version": "1.8.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",
@@ -31,6 +31,7 @@
31
31
  },
32
32
  "files": [
33
33
  "lib/client.js",
34
+ "lib/client.mermaid.js",
34
35
  "lib/host",
35
36
  "locale/*.json",
36
37
  "icon.svg",
@@ -41,11 +42,12 @@
41
42
  ],
42
43
  "scripts": {
43
44
  "build": "node build.mjs",
45
+ "lint": "eslint .",
44
46
  "typecheck": "tsc -p tsconfig.json --noEmit",
45
- "test:unit": "node build.mjs && node --test --test-force-exit test/unit/*.test.mjs",
47
+ "test:unit": "node build.mjs && node --test test/unit/*.test.mjs",
46
48
  "test:e2e": "node build.mjs && node test/e2e/setup.mjs && node test/e2e/all.mjs",
47
49
  "test": "npm run test:unit && npm run test:e2e",
48
- "check": "npm run typecheck && npm run test && git diff --exit-code -- lib/"
50
+ "check": "npm run lint && npm run typecheck && npm run test"
49
51
  },
50
52
  "dsh": {
51
53
  "bundle": {
@@ -63,7 +65,10 @@
63
65
  "compatibility": {
64
66
  "dsh": ">=0.1.7-rc.2",
65
67
  "dshReleases": {
66
- "0.2.1-alpha.2": "compatible"
68
+ "0.1.7-rc.2": "compatible",
69
+ "0.2.0-rc.2": "compatible",
70
+ "0.2.1-rc.1": "compatible",
71
+ "0.2.1-rc.2": "compatible"
67
72
  }
68
73
  }
69
74
  },
@@ -84,14 +89,23 @@
84
89
  "@deepseek-ai/dsh-typert-protocol": ">=0.1.2-rc.1",
85
90
  "@deepseek-ai/schemastery": ">=3.18.4"
86
91
  },
92
+ "peerDependenciesMeta": {
93
+ "@deepseek-ai/dsh": {
94
+ "optional": true
95
+ }
96
+ },
87
97
  "devDependencies": {
88
98
  "@types/node": "22.10.5",
89
99
  "@types/react": "18.3.18",
90
100
  "@types/react-dom": "18.3.5",
91
101
  "esbuild": "0.24.2",
102
+ "eslint": "9.39.5",
103
+ "eslint-plugin-react-hooks": "7.1.1",
104
+ "mermaid": "12.2.0",
92
105
  "playwright-core": "1.63.0",
93
106
  "react": "18.3.1",
94
107
  "react-dom": "18.3.1",
95
- "typescript": "5.7.3"
108
+ "typescript": "5.7.3",
109
+ "typescript-eslint": "8.71.2"
96
110
  }
97
111
  }