@xbzbing/dsh-git-panel 0.2.0 → 0.3.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.ts';
2
- import type { GitChange, GitErrorCode, GitSnapshotResult } from './types.ts';
2
+ import type { DiffViewMode, GitChange, GitErrorCode, GitSnapshotResult } from './types.ts';
3
3
  export interface GitPanelConfig {
4
4
  readonly timeoutMs: number;
5
5
  /** Per-command stdout cap; also the per-side image-diff payload cap. */
@@ -8,9 +8,13 @@ export interface GitPanelConfig {
8
8
  readonly refreshIntervalMs: number;
9
9
  /** Whether the input-bar git marker pill is shown. */
10
10
  readonly showInputPill: boolean;
11
+ /** Default diff layout the views open with (user can switch per-diff). */
12
+ readonly defaultDiffView: DiffViewMode;
11
13
  }
12
14
  export declare const DEFAULT_CONFIG: GitPanelConfig;
13
15
  export declare function normalizeConfig(raw: unknown): GitPanelConfig;
16
+ /** Read the default-diff-view field, unwrapping a schemastery volatile ref. */
17
+ export declare function readDiffView(value: unknown, fallback: DiffViewMode): DiffViewMode;
14
18
  /**
15
19
  * Read a boolean config field, unwrapping a schemastery volatile reference
16
20
  * (`{ get() }`) so a live-editable toggle reflects the latest value. Absent or
@@ -28,6 +32,11 @@ export interface SnapshotDeps {
28
32
  }>;
29
33
  /** Raw bytes (no encoding) — image sides for the image-diff query. */
30
34
  readFile(path: string): Promise<Buffer>;
35
+ /** One directory's entries (name + is-directory) — the file browser. */
36
+ readdir(path: string): Promise<ReadonlyArray<{
37
+ name: string;
38
+ isDirectory: boolean;
39
+ }>>;
31
40
  /** Best-effort unlink (force) — temp-blob cleanup. */
32
41
  remove(path: string): Promise<void>;
33
42
  };
@@ -62,10 +71,27 @@ export type WorkspaceResolution = {
62
71
  ok: false;
63
72
  };
64
73
  };
74
+ /**
75
+ * Resolve a directory root for the file browser, which must work outside a git
76
+ * repository too. Inside a repo it is the work-tree top; otherwise it falls
77
+ * back to the session's cwd (realpath'd) so directory listing / file preview
78
+ * still function. Only a missing cwd is a hard failure.
79
+ */
80
+ export declare function resolveBrowseRoot(deps: SnapshotDeps, sessionId: string): Promise<{
81
+ ok: true;
82
+ root: string;
83
+ isGitRepo: boolean;
84
+ } | {
85
+ ok: false;
86
+ error: {
87
+ code: GitErrorCode;
88
+ message?: string;
89
+ };
90
+ }>;
65
91
  /** Resolve the git work-tree root for a session's cwd. */
66
92
  export declare function resolveWorkspace(deps: SnapshotDeps, sessionId: string): Promise<WorkspaceResolution>;
67
93
  /** Run one git command; a spawn-level failure returns { failure }. */
68
- export declare function runCommand(runner: GitRunner, argv: readonly string[], cwd: string, _label: string, signal?: AbortSignal): Promise<{
94
+ export declare function runCommand(runner: GitRunner, argv: readonly string[], cwd: string, _label: string, signal?: AbortSignal, stdinData?: string): Promise<{
69
95
  run: Awaited<ReturnType<GitRunner['run']>>;
70
96
  } | {
71
97
  failure: unknown;
package/lib/host/git.d.ts CHANGED
@@ -10,7 +10,9 @@ interface SpawnSpec {
10
10
  readonly argv: readonly string[];
11
11
  readonly cwd: string;
12
12
  readonly stdio: {
13
- readonly stdin: 'ignore';
13
+ readonly stdin: 'ignore' | {
14
+ readonly data: string;
15
+ };
14
16
  readonly stdout: CollectDisposition;
15
17
  readonly stderr: CollectDisposition;
16
18
  };
@@ -53,6 +55,7 @@ export interface GitRunner {
53
55
  run(argv: readonly string[], opts: {
54
56
  readonly cwd: string;
55
57
  readonly signal?: AbortSignal;
58
+ readonly stdinData?: string;
56
59
  }): Promise<GitRunResult>;
57
60
  }
58
61
  /**
@@ -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, GitVersionInfo, GitVersionRequest } from './types.ts';
13
- export type { GitSnapshot, GitSnapshotResult, GitSnapshotRequest, GitFailure, GitCommit, GraphCommit, GitRef, GitChange, GitChangeStatus, GitAction, GitActionRequest, GitActionResult, GitErrorCode, GitQuery, GitQueryRequest, GitQueryResponse, GitQueryResult, GitBranch, GitFileStat, WorktreeStats, GitVersionRequest, GitVersionInfo, } from './types.ts';
13
+ export type { GitSnapshot, GitSnapshotResult, GitSnapshotRequest, GitFailure, GitCommit, GraphCommit, GitRef, GitChange, GitChangeStatus, GitAction, GitActionRequest, GitActionResult, GitErrorCode, GitQuery, GitQueryRequest, GitQueryResponse, GitQueryResult, GitBranch, GitFileStat, WorktreeStats, GitVersionRequest, GitVersionInfo, DiffViewMode, DirEntry, } from './types.ts';
14
14
  export { normalizeConfig, DEFAULT_CONFIG, snapshotForSession, resolveWorkspace } from './core.ts';
15
15
  export { createGitRunner } from './git.ts';
16
16
  export { parseStatus, parseGraphLog, parseBranches, parseNameStatus, sumNumstat } from './parser.ts';
@@ -20,14 +20,17 @@ export { readVersionInfo, checkLatestVersion, compareVersions, parseRepository }
20
20
  export declare class GitPanelService extends TypertRemoteService {
21
21
  static inject: string[];
22
22
  /**
23
- * Config schema surfaced on the plugin detail page. Only `showInputPill` is
24
- * `.volatile()`, so the settings host renders it as a live-editable toggle;
25
- * the operational limits stay profile-only and out of the UI form.
23
+ * Config schema surfaced on the plugin detail page. `showInputPill` and
24
+ * `defaultDiffView` are `.volatile()`, so the settings host renders them as
25
+ * live-editable controls; the operational limits stay profile-only and out
26
+ * of the UI form.
26
27
  */
27
28
  static Config: Schema<Schemastery.ObjectS<NoInfer<{
28
29
  showInputPill: Schema<boolean, boolean, "volatile-defined">;
30
+ defaultDiffView: Schema<"split" | "unified", "split" | "unified", "volatile-defined">;
29
31
  }>>, Schemastery.ObjectT<NoInfer<{
30
32
  showInputPill: Schema<boolean, boolean, "volatile-defined">;
33
+ defaultDiffView: Schema<"split" | "unified", "split" | "unified", "volatile-defined">;
31
34
  }>>, "plain">;
32
35
  private readonly deps;
33
36
  private config;
package/lib/host/index.js CHANGED
@@ -50,7 +50,7 @@ var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "acce
50
50
  // src/host/index.ts
51
51
  import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
52
52
  import Schema from "@deepseek-ai/schemastery";
53
- import { readFile as readFile3, realpath, rm, stat } from "node:fs/promises";
53
+ import { readFile as readFile3, readdir, realpath, rm, stat } from "node:fs/promises";
54
54
 
55
55
  // src/host/git.ts
56
56
  import { readFile } from "node:fs/promises";
@@ -66,7 +66,7 @@ function createGitRunner(subprocess, timeoutMs, maxBytes) {
66
66
  argv,
67
67
  cwd: opts.cwd,
68
68
  stdio: {
69
- stdin: "ignore",
69
+ stdin: opts.stdinData === void 0 ? "ignore" : { data: opts.stdinData },
70
70
  stdout: { collect: { maxBytes, spill: { maxBytes: spillMaxBytes } } },
71
71
  stderr: { collect: { maxBytes, spill: { maxBytes: spillMaxBytes } } }
72
72
  },
@@ -305,7 +305,8 @@ var DEFAULT_CONFIG = {
305
305
  maxBytes: 4 * 1024 * 1024,
306
306
  maxChanges: 1e3,
307
307
  refreshIntervalMs: 3e4,
308
- showInputPill: true
308
+ showInputPill: true,
309
+ defaultDiffView: "unified"
309
310
  };
310
311
  function normalizeConfig(raw) {
311
312
  const c = raw ?? {};
@@ -315,14 +316,35 @@ function normalizeConfig(raw) {
315
316
  maxBytes: num(c.maxBytes, DEFAULT_CONFIG.maxBytes),
316
317
  maxChanges: num(c.maxChanges, DEFAULT_CONFIG.maxChanges),
317
318
  refreshIntervalMs: num(c.refreshIntervalMs, DEFAULT_CONFIG.refreshIntervalMs),
318
- showInputPill: readBool(c.showInputPill, DEFAULT_CONFIG.showInputPill)
319
+ showInputPill: readBool(c.showInputPill, DEFAULT_CONFIG.showInputPill),
320
+ defaultDiffView: readDiffView(c.defaultDiffView, DEFAULT_CONFIG.defaultDiffView)
319
321
  };
320
322
  }
323
+ function unwrapVolatile(value) {
324
+ return value !== null && typeof value === "object" && "get" in value && typeof value.get === "function" ? value.get() : value;
325
+ }
326
+ function readDiffView(value, fallback) {
327
+ const raw = unwrapVolatile(value);
328
+ return raw === "unified" || raw === "split" ? raw : fallback;
329
+ }
321
330
  function readBool(value, fallback) {
322
- const raw = value !== null && typeof value === "object" && "get" in value && typeof value.get === "function" ? value.get() : value;
331
+ const raw = unwrapVolatile(value);
323
332
  return typeof raw === "boolean" ? raw : fallback;
324
333
  }
325
334
  var NEG_CACHE_MS = 15e3;
335
+ async function resolveBrowseRoot(deps, sessionId) {
336
+ const ws = await resolveWorkspace(deps, sessionId);
337
+ if (ws.ok) return { ok: true, root: ws.root, isGitRepo: true };
338
+ const err = ws.failure.error;
339
+ if (err.code === "not-a-git-repo" && err.cwd !== void 0 && err.cwd !== "") {
340
+ try {
341
+ return { ok: true, root: await deps.fs.realpath(err.cwd), isGitRepo: false };
342
+ } catch {
343
+ return { ok: false, error: { code: "git-error" } };
344
+ }
345
+ }
346
+ return { ok: false, error: mapWorkspaceFailure(ws.failure) };
347
+ }
326
348
  async function resolveWorkspace(deps, sessionId) {
327
349
  let cwd = deps.sessions.liveCwd(sessionId);
328
350
  if (cwd === void 0 || cwd === "") {
@@ -363,9 +385,9 @@ async function resolveWorkspace(deps, sessionId) {
363
385
  deps.rootCache?.set(cwd, root);
364
386
  return { ok: true, root };
365
387
  }
366
- async function runCommand(runner, argv, cwd, _label, signal) {
388
+ async function runCommand(runner, argv, cwd, _label, signal, stdinData) {
367
389
  try {
368
- const run = await runner.run(argv, { cwd, ...signal ? { signal } : {} });
390
+ const run = await runner.run(argv, { cwd, ...signal ? { signal } : {}, ...stdinData !== void 0 ? { stdinData } : {} });
369
391
  return { run };
370
392
  } catch (error) {
371
393
  return { failure: error };
@@ -478,6 +500,7 @@ async function snapshotForSession(deps, config, sessionId) {
478
500
  truncated,
479
501
  refreshIntervalMs: config.refreshIntervalMs,
480
502
  showInputPill: config.showInputPill,
503
+ defaultDiffView: config.defaultDiffView,
481
504
  checkedAt: Date.now()
482
505
  };
483
506
  return { ok: true, value: snapshot };
@@ -649,10 +672,19 @@ function isHexLike(text) {
649
672
  return /^[0-9a-fA-F]{7,40}$/.test(text.trim());
650
673
  }
651
674
  async function runQuery(deps, config, request) {
675
+ const q = request.query;
676
+ if (q.kind === "dir-list" || q.kind === "file-content") {
677
+ const browse = await resolveBrowseRoot(deps, request.sessionId);
678
+ if (!browse.ok) return { ok: false, error: browse.error };
679
+ try {
680
+ return q.kind === "dir-list" ? await queryDirList(deps, browse.root, q, browse.isGitRepo) : await queryFileContent(deps, config, browse.root, q);
681
+ } catch (error) {
682
+ return { ok: false, error: { code: "git-error", message: error instanceof Error ? error.message : String(error) } };
683
+ }
684
+ }
652
685
  const workspace = await resolveWorkspace(deps, request.sessionId);
653
686
  if (!workspace.ok) return { ok: false, error: mapWorkspaceFailure(workspace.failure) };
654
687
  const root = workspace.root;
655
- const q = request.query;
656
688
  try {
657
689
  switch (q.kind) {
658
690
  case "history":
@@ -892,6 +924,106 @@ async function worktreeSide(deps, root, path, cap) {
892
924
  return void 0;
893
925
  }
894
926
  }
927
+ var DIR_ENTRY_CAP = 2e3;
928
+ async function queryDirList(deps, root, q, isGitRepo) {
929
+ const rel = q.path;
930
+ if (rel !== "" && !isSafePath(rel) || hasGitSegment(rel)) return { ok: false, error: { code: "invalid-path", message: rel } };
931
+ const dir = rel === "" ? root : join3(root, rel);
932
+ const inside = await isInsideRoot(deps, root, dir, true);
933
+ if (!inside) return { ok: false, error: { code: "invalid-path", message: rel } };
934
+ let raw;
935
+ try {
936
+ raw = await deps.fs.readdir(dir);
937
+ } catch (error) {
938
+ return { ok: false, error: { code: "git-error", message: error instanceof Error ? error.message : "readdir failed" } };
939
+ }
940
+ const filtered = raw.filter((e) => e.name !== ".git");
941
+ filtered.sort((a, b) => a.isDirectory !== b.isDirectory ? a.isDirectory ? -1 : 1 : a.name.localeCompare(b.name));
942
+ const truncated = filtered.length > DIR_ENTRY_CAP;
943
+ const slice = truncated ? filtered.slice(0, DIR_ENTRY_CAP) : filtered;
944
+ const ignored = isGitRepo ? await ignoredEntries(deps, root, rel, slice.map((e) => e.name)) : /* @__PURE__ */ new Set();
945
+ const entries = await Promise.all(slice.map(async (e) => {
946
+ const ignoreFlag = ignored.has(e.name) ? { ignored: true } : {};
947
+ if (e.isDirectory) return { name: e.name, dir: true, ...ignoreFlag };
948
+ let size;
949
+ try {
950
+ size = (await deps.fs.stat(join3(dir, e.name))).size;
951
+ } catch {
952
+ size = void 0;
953
+ }
954
+ return { name: e.name, dir: false, ...size !== void 0 ? { size } : {}, ...ignoreFlag };
955
+ }));
956
+ const path = rel === "" ? "" : rel.replace(/\/+$/, "");
957
+ return { ok: true, value: { kind: "dir-list", path, entries, truncated } };
958
+ }
959
+ async function ignoredEntries(deps, root, parent, names) {
960
+ const ignored = /* @__PURE__ */ new Set();
961
+ const paths = names.map((name) => parent === "" ? name : `${parent}/${name}`);
962
+ for (let index = 0; index < paths.length; ) {
963
+ const batch = [];
964
+ let bytes = 0;
965
+ while (index < paths.length && batch.length < 128) {
966
+ const candidate = paths[index];
967
+ const size = Buffer.byteLength(candidate) + 1;
968
+ if (batch.length > 0 && bytes + size > 32 * 1024) break;
969
+ batch.push(candidate);
970
+ bytes += size;
971
+ index++;
972
+ }
973
+ const result = await runCommand(deps.run, ["git", "check-ignore", "-z", "--stdin"], root, "dir-ignore", deps.signal, `${batch.join("\0")}\0`);
974
+ if ("failure" in result || result.run.timedOut || result.run.cancelled || result.run.stdoutLossy || ![0, 1].includes(result.run.exitCode ?? -1)) continue;
975
+ const matches = new Set(result.run.stdout.split("\0"));
976
+ for (const path of batch) if (matches.has(path)) ignored.add(parent === "" ? path : path.slice(parent.length + 1));
977
+ }
978
+ return ignored;
979
+ }
980
+ async function queryFileContent(deps, config, root, q) {
981
+ if (!isSafePath(q.path) || hasGitSegment(q.path)) return { ok: false, error: { code: "invalid-path", message: q.path } };
982
+ const file = join3(root, q.path);
983
+ const inside = await isInsideRoot(deps, root, file, true);
984
+ if (!inside) return { ok: false, error: { code: "invalid-path", message: q.path } };
985
+ const cap = config.maxBytes;
986
+ let info;
987
+ try {
988
+ info = await deps.fs.stat(file);
989
+ } catch (error) {
990
+ return { ok: false, error: { code: "git-error", message: error instanceof Error ? error.message : "stat failed" } };
991
+ }
992
+ const mime = imageMimeFor(q.path);
993
+ if (info.size > cap) return { ok: true, value: { kind: "file-content", path: q.path, variant: mime !== null ? "image" : "text", tooLarge: true } };
994
+ let buf;
995
+ try {
996
+ buf = await deps.fs.readFile(file);
997
+ } catch (error) {
998
+ return { ok: false, error: { code: "git-error", message: error instanceof Error ? error.message : "read failed" } };
999
+ }
1000
+ if (buf.length > cap) return { ok: true, value: { kind: "file-content", path: q.path, variant: mime !== null ? "image" : "text", tooLarge: true } };
1001
+ if (mime !== null) {
1002
+ return { ok: true, value: { kind: "file-content", path: q.path, variant: "image", dataUrl: `data:${mime};base64,${buf.toString("base64")}` } };
1003
+ }
1004
+ if (isBinaryBuffer(buf)) return { ok: true, value: { kind: "file-content", path: q.path, variant: "binary" } };
1005
+ const content = buf.toString("utf8");
1006
+ const lines = content === "" ? 0 : content.split("\n").length - (content.endsWith("\n") ? 1 : 0);
1007
+ return { ok: true, value: { kind: "file-content", path: q.path, variant: "text", content, lines } };
1008
+ }
1009
+ function hasGitSegment(path) {
1010
+ return path.split(/[\\/]/).includes(".git");
1011
+ }
1012
+ async function isInsideRoot(deps, root, path, hideGit = false) {
1013
+ try {
1014
+ const real = await deps.fs.realpath(path);
1015
+ const rootReal = await deps.fs.realpath(root);
1016
+ if (real !== rootReal && !real.startsWith(rootReal + sep2)) return false;
1017
+ return !hideGit || !hasGitSegment(real.slice(rootReal.length + 1));
1018
+ } catch {
1019
+ return false;
1020
+ }
1021
+ }
1022
+ function isBinaryBuffer(buf) {
1023
+ const n = Math.min(buf.length, 8192);
1024
+ for (let i = 0; i < n; i++) if (buf[i] === 0) return true;
1025
+ return false;
1026
+ }
895
1027
  async function queryShow(deps, root, ref) {
896
1028
  if (!isSafeRev(ref)) return { ok: false, error: { code: "invalid-name", message: `unsafe ref: ${ref}` } };
897
1029
  const metaFormat = "--format=%H%x1f%h%x1f%s%x1f%an%x1f%aI%x1f%b";
@@ -1107,6 +1239,7 @@ var GitPanelService = class extends (_a = TypertRemoteService, _snapshot_dec = [
1107
1239
  realpath,
1108
1240
  stat: async (p) => stat(p),
1109
1241
  readFile: readFile3,
1242
+ readdir: async (p) => (await readdir(p, { withFileTypes: true })).map((e) => ({ name: e.name, isDirectory: e.isDirectory() })),
1110
1243
  remove: async (p) => {
1111
1244
  await rm(p, { force: true });
1112
1245
  }
@@ -1170,12 +1303,17 @@ __decorateElement(_init, 1, "version", _version_dec, GitPanelService);
1170
1303
  __decoratorMetadata(_init, GitPanelService);
1171
1304
  __publicField(GitPanelService, "inject", ["subprocess", "sessions", "sessionPersistence"]);
1172
1305
  /**
1173
- * Config schema surfaced on the plugin detail page. Only `showInputPill` is
1174
- * `.volatile()`, so the settings host renders it as a live-editable toggle;
1175
- * the operational limits stay profile-only and out of the UI form.
1306
+ * Config schema surfaced on the plugin detail page. `showInputPill` and
1307
+ * `defaultDiffView` are `.volatile()`, so the settings host renders them as
1308
+ * live-editable controls; the operational limits stay profile-only and out
1309
+ * of the UI form.
1176
1310
  */
1177
1311
  __publicField(GitPanelService, "Config", Schema.object({
1178
- showInputPill: Schema.boolean().default(true).volatile().description("\u663E\u793A\u8F93\u5165\u6846\u7684 Git \u5206\u652F\u6807\u8BB0")
1312
+ showInputPill: Schema.boolean().default(true).volatile().description("\u663E\u793A\u8F93\u5165\u6846\u7684 Git \u5206\u652F\u6807\u8BB0"),
1313
+ defaultDiffView: Schema.union([
1314
+ Schema.const("unified").description("\u7EDF\u4E00\u89C6\u56FE\uFF08\u5355\u680F\u884C\u5185\u5BF9\u6BD4\uFF09"),
1315
+ Schema.const("split").description("\u5E76\u6392\u89C6\u56FE\uFF08\u5DE6\u53F3\u5206\u680F\u5BF9\u6BD4\uFF09")
1316
+ ]).default("unified").volatile().description("\u5DEE\u5F02\u5BF9\u6BD4\u9ED8\u8BA4\u89C6\u56FE")
1179
1317
  }));
1180
1318
  var index_default = GitPanelService;
1181
1319
  export {
@@ -3,6 +3,8 @@
3
3
  * The client half (src/client/rpc.ts) reuses these types directly, so the
4
4
  * wire contract has a single definition and cannot drift.
5
5
  */
6
+ /** Diff layout: `unified` = single inline column, `split` = side-by-side. */
7
+ export type DiffViewMode = 'unified' | 'split';
6
8
  /** Wire request: the browser sends only session identity, never a path. */
7
9
  export interface GitSnapshotRequest {
8
10
  readonly sessionId: string;
@@ -56,6 +58,8 @@ export interface GitSnapshot {
56
58
  readonly refreshIntervalMs: number;
57
59
  /** Whether the input-bar git marker pill should render (user preference). */
58
60
  readonly showInputPill: boolean;
61
+ /** Default diff layout the views open with (user preference). */
62
+ readonly defaultDiffView: DiffViewMode;
59
63
  /** Epoch millis of the snapshot. */
60
64
  readonly checkedAt: number;
61
65
  }
@@ -176,6 +180,12 @@ export type GitQuery = {
176
180
  readonly path: string;
177
181
  readonly base: 'commit';
178
182
  readonly commit: string;
183
+ } | {
184
+ readonly kind: 'dir-list';
185
+ readonly path: string;
186
+ } | {
187
+ readonly kind: 'file-content';
188
+ readonly path: string;
179
189
  } | {
180
190
  readonly kind: 'show';
181
191
  readonly ref: string;
@@ -190,6 +200,15 @@ export type GitQuery = {
190
200
  } | {
191
201
  readonly kind: 'worktree-stats';
192
202
  };
203
+ /** One entry in a `dir-list` result. */
204
+ export interface DirEntry {
205
+ readonly name: string;
206
+ readonly dir: boolean;
207
+ /** File byte size; absent for directories. */
208
+ readonly size?: number;
209
+ /** True when Git excludes an untracked item; absent outside a repository. */
210
+ readonly ignored?: boolean;
211
+ }
193
212
  /** One commit's changed-file line (from --name-status). */
194
213
  export interface GitFileStat {
195
214
  readonly path: string;
@@ -272,6 +291,23 @@ export type GitQueryResult = {
272
291
  } | {
273
292
  readonly kind: 'worktree-stats';
274
293
  readonly stats: WorktreeStats;
294
+ } | {
295
+ readonly kind: 'dir-list';
296
+ readonly path: string;
297
+ readonly entries: readonly DirEntry[];
298
+ readonly truncated: boolean;
299
+ } | {
300
+ readonly kind: 'file-content';
301
+ readonly path: string;
302
+ readonly variant: 'text' | 'image' | 'binary';
303
+ /** text variant: the file's full UTF-8 content. */
304
+ readonly content?: string;
305
+ /** text variant: total line count. */
306
+ readonly lines?: number;
307
+ /** image variant: a `data:<mime>;base64,…` URL. */
308
+ readonly dataUrl?: string;
309
+ /** The file exceeded the byte cap, so no content is returned. */
310
+ readonly tooLarge?: true;
275
311
  };
276
312
  export type GitQueryResponse = {
277
313
  readonly ok: true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xbzbing/dsh-git-panel",
3
- "version": "0.2.0",
3
+ "version": "0.3.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",
@@ -44,8 +44,7 @@
44
44
  "test:unit": "node build.mjs && node --test --test-force-exit test/unit/*.test.mjs",
45
45
  "test:e2e": "node build.mjs && node test/e2e/setup.mjs && node test/e2e/run.mjs && node test/e2e/run2.mjs",
46
46
  "test": "npm run test:unit && npm run test:e2e",
47
- "check": "npm run typecheck && npm run test && git diff --exit-code -- lib/",
48
- "prepare": "node build.mjs"
47
+ "check": "npm run typecheck && npm run test && git diff --exit-code -- lib/"
49
48
  },
50
49
  "dsh": {
51
50
  "bundle": {
@@ -56,7 +55,8 @@
56
55
  "inject": [
57
56
  "@deepseek-ai/dsh-client-connection",
58
57
  "@deepseek-ai/dsh-client-locale",
59
- "@deepseek-ai/dsh-client-ui-conversation"
58
+ "@deepseek-ai/dsh-client-ui-conversation",
59
+ "@deepseek-ai/dsh-client-ui-primitives"
60
60
  ]
61
61
  }
62
62
  },
@@ -76,9 +76,6 @@
76
76
  "@deepseek-ai/dsh-typert-protocol": ">=0.1.2-rc.1",
77
77
  "@deepseek-ai/schemastery": ">=3.18.4"
78
78
  },
79
- "dependencies": {
80
- "highlight.js": "11.11.1"
81
- },
82
79
  "devDependencies": {
83
80
  "@types/node": "22.10.5",
84
81
  "@types/react": "18.3.18",