dsh-coding-sidebar 1.0.8 → 1.0.9

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.
Files changed (50) hide show
  1. package/lib/client-editor.js +223 -175
  2. package/lib/client-registry.js +763 -344
  3. package/lib/client-terminal.js +283 -179
  4. package/lib/client.js +760 -341
  5. package/lib/index.js +151 -2
  6. package/lib/types/client/DiffView.d.ts +33 -1
  7. package/lib/types/client/EditorHost.d.ts +3 -0
  8. package/lib/types/client/FileTree.d.ts +4 -0
  9. package/lib/types/client/TerminalWaitBanner.d.ts +6 -0
  10. package/lib/types/client/TreePanel.d.ts +3 -0
  11. package/lib/types/client/api.d.ts +26 -0
  12. package/lib/types/client/locales.d.ts +12 -0
  13. package/lib/types/client/state.d.ts +15 -0
  14. package/lib/types/fs-operations.d.ts +42 -0
  15. package/lib/types/git.d.ts +15 -0
  16. package/package.json +1 -1
  17. package/src/client/DiffTab.tsx +10 -1
  18. package/src/client/DiffView.tsx +174 -19
  19. package/src/client/EditorHost.tsx +8 -1
  20. package/src/client/FileTree.tsx +147 -4
  21. package/src/client/Sidebar.tsx +54 -3
  22. package/src/client/TerminalView.tsx +28 -0
  23. package/src/client/TerminalWaitBanner.tsx +32 -0
  24. package/src/client/TreePanel.tsx +6 -1
  25. package/src/client/api.ts +20 -0
  26. package/src/client/locales-ar.ts +12 -0
  27. package/src/client/locales-de.ts +12 -0
  28. package/src/client/locales-fr.ts +12 -0
  29. package/src/client/locales-hi.ts +12 -0
  30. package/src/client/locales-id.ts +12 -0
  31. package/src/client/locales-it.ts +12 -0
  32. package/src/client/locales-ja.ts +12 -0
  33. package/src/client/locales-ko.ts +12 -0
  34. package/src/client/locales-nl.ts +12 -0
  35. package/src/client/locales-pl.ts +12 -0
  36. package/src/client/locales-pt.ts +12 -0
  37. package/src/client/locales-ru.ts +12 -0
  38. package/src/client/locales-sv.ts +12 -0
  39. package/src/client/locales-th.ts +12 -0
  40. package/src/client/locales-tr.ts +12 -0
  41. package/src/client/locales-vi.ts +12 -0
  42. package/src/client/locales-zh-HK.ts +12 -0
  43. package/src/client/locales-zh-MO.ts +12 -0
  44. package/src/client/locales-zh-TW.ts +12 -0
  45. package/src/client/locales.ts +24 -0
  46. package/src/client/sidebar.module.css +68 -0
  47. package/src/client/state.ts +42 -3
  48. package/src/fs-operations.ts +126 -4
  49. package/src/git.ts +39 -2
  50. package/src/index.ts +43 -1
package/lib/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createRequire } from "node:module";
2
- import { mkdir, open, opendir, readFile, readdir, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
2
+ import { access, lstat, mkdir, open, opendir, readFile, readdir, realpath, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
3
3
  import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep, win32 } from "node:path";
4
4
  import { WebSocket, WebSocketServer } from "ws";
5
5
  import z from "schemastery";
@@ -377,7 +377,8 @@ async function ensureWorkspaceWritePath(cwd, target) {
377
377
  //#endregion
378
378
  //#region src/fs-operations.ts
379
379
  /**
380
- * Workspace-safe file mutations for the sidebar (the upload route today).
380
+ * Workspace-safe file mutations for the sidebar: the upload route plus the
381
+ * file tree's rename and delete.
381
382
  *
382
383
  * Every write is confined to the real session workspace: the upload
383
384
  * directory is resolved absolute and its target is checked through existing
@@ -387,6 +388,13 @@ async function ensureWorkspaceWritePath(cwd, target) {
387
388
  * to a uniquely named temp sibling
388
389
  * and are renamed into place, so a failed, aborted, or oversized upload never
389
390
  * leaves a partial file at the target path.
391
+ *
392
+ * The tree's rename/delete (below) are link-aware: existence and containment
393
+ * are verified against the fully resolved target (a symlink pointing outside
394
+ * the workspace is refused), but the operation itself addresses the lexical
395
+ * row path — renaming or deleting a symlink row renames/unlinks the LINK,
396
+ * never its target, matching what the tree row visually names (VS Code
397
+ * semantics). Containment is always enforced (no fence toggle on this fork).
390
398
  */
391
399
  /**
392
400
  * Stream `chunks` into `dir/relativePath` atomically: a uniquely named temp
@@ -443,6 +451,86 @@ async function writeWorkspaceUpload(input) {
443
451
  throw error;
444
452
  }
445
453
  }
454
+ /** Resolve a tree-row path against the session workspace (absolute rows pass through). */
455
+ function resolveRowPath(cwd, target) {
456
+ return isAbsolute(target) ? target : join(cwd, target);
457
+ }
458
+ /** Resolve one existing entry for a link-aware mutation: the lexical row path
459
+ * plus its fully resolved real target (containment-checked). Resolution
460
+ * failures become fs-errors, mirroring path-security's semantics. */
461
+ async function resolveEntry(cwd, target) {
462
+ const absolute = requireAbsolute(resolveRowPath(cwd, target));
463
+ let real;
464
+ let realCwd;
465
+ try {
466
+ [realCwd, real] = await Promise.all([realpath(cwd), realpath(absolute)]);
467
+ } catch (error) {
468
+ throw new SidebarError("fs-error", `cannot resolve "${target}": ${error instanceof Error ? error.message : String(error)}`, 400);
469
+ }
470
+ if (!isWithin(realCwd, real)) throw new SidebarError("forbidden", `path "${target}" is outside workspace`, 403);
471
+ return {
472
+ absolute,
473
+ real,
474
+ realCwd
475
+ };
476
+ }
477
+ /** Whether a path exists (ENOENT → false; other failures propagate). */
478
+ async function pathExists(target) {
479
+ try {
480
+ await access(target);
481
+ return true;
482
+ } catch (error) {
483
+ if (error.code === "ENOENT") return false;
484
+ throw error;
485
+ }
486
+ }
487
+ /**
488
+ * Rename one tree row within its directory: `path` → `<parent>/<name>`.
489
+ * The new name must be a single path segment (this is rename, not move);
490
+ * an existing destination is refused (POSIX rename would clobber it
491
+ * silently); the workspace root itself is never renamable; a symlink row
492
+ * renames the link, not its target. A no-op rename (same name) succeeds
493
+ * without touching the filesystem.
494
+ *
495
+ * @throws SidebarError with a wire code for shape, containment, existence
496
+ * and root failures.
497
+ */
498
+ async function renameWorkspaceEntry(input) {
499
+ const { cwd, path, name } = input;
500
+ if (name === "" || name === "." || name === ".." || name.includes("/") || name.includes("\\")) throw new SidebarError("bad-request", "name must be a single path segment", 400);
501
+ const { absolute, real, realCwd } = await resolveEntry(cwd, path);
502
+ if (real === realCwd) throw new SidebarError("fs-error", "cannot rename the workspace root", 400);
503
+ if (basename(absolute) === name) return { path: absolute };
504
+ const safeDestination = await ensureWorkspaceWritePath(cwd, join(dirname(absolute), name));
505
+ if (await pathExists(safeDestination)) throw new SidebarError("fs-error", `"${name}" already exists`, 409);
506
+ try {
507
+ await rename(absolute, safeDestination);
508
+ } catch (error) {
509
+ throw new SidebarError("fs-error", `cannot rename "${path}" to "${name}": ${error instanceof Error ? error.message : String(error)}`, 400);
510
+ }
511
+ return { path: safeDestination };
512
+ }
513
+ /**
514
+ * Delete one tree row permanently (there is no trash on the host): files are
515
+ * unlinked, directories removed recursively, a symlink row unlinks the LINK
516
+ * only (lstat decides, so a link to a directory does not recurse into its
517
+ * target). The workspace root itself is never removable.
518
+ *
519
+ * @throws SidebarError with a wire code for containment, existence and
520
+ * root failures.
521
+ */
522
+ async function removeWorkspaceEntry(input) {
523
+ const { cwd, path } = input;
524
+ const { absolute, real, realCwd } = await resolveEntry(cwd, path);
525
+ if (real === realCwd) throw new SidebarError("fs-error", "cannot remove the workspace root", 400);
526
+ try {
527
+ if ((await lstat(absolute)).isDirectory()) await rm(absolute, { recursive: true });
528
+ else await unlink(absolute);
529
+ } catch (error) {
530
+ throw new SidebarError("fs-error", `cannot remove "${path}": ${error instanceof Error ? error.message : String(error)}`, 400);
531
+ }
532
+ return { path: absolute };
533
+ }
446
534
  //#endregion
447
535
  //#region src/fs-search.ts
448
536
  /**
@@ -1201,6 +1289,36 @@ async function show(cwd, rev, path, selected) {
1201
1289
  return null;
1202
1290
  }
1203
1291
  }
1292
+ /**
1293
+ * Both sides' full file contents for a diff-fold expansion. `path` is
1294
+ * repo-relative. The sides resolve per diff kind: a commit reads
1295
+ * `<hash>^` vs `<hash>`; a staged change reads HEAD vs the index (`:`);
1296
+ * an unstaged change reads HEAD vs the working tree file on disk (a side
1297
+ * that does not exist — untracked, deleted, binary-refused — comes back
1298
+ * null and the client degrades the fold to a static marker).
1299
+ */
1300
+ async function foldContents(cwd, path, opts = {}, selected) {
1301
+ const root = await repoRoot(cwd, selected);
1302
+ if (opts.hash !== void 0) {
1303
+ const [old, neu] = await Promise.all([show(root, `${opts.hash}^`, path, selected), show(root, opts.hash, path, selected)]);
1304
+ return {
1305
+ old,
1306
+ new: neu
1307
+ };
1308
+ }
1309
+ if (opts.staged === true) {
1310
+ const [old, neu] = await Promise.all([show(root, "HEAD", path, selected), show(root, ":", path, selected)]);
1311
+ return {
1312
+ old,
1313
+ new: neu
1314
+ };
1315
+ }
1316
+ const [old, neu] = await Promise.all([show(root, "HEAD", path, selected), readFile(isAbsolute(path) ? path : join(root, path), "utf8").catch(() => null)]);
1317
+ return {
1318
+ old,
1319
+ new: neu
1320
+ };
1321
+ }
1204
1322
  /** Full patch text of one commit (`git show` with the commit header suppressed).
1205
1323
  * Merge commits show their diff against the first parent (`-m --first-parent`
1206
1324
  * is a no-op for regular commits), so a history click always has content. */
@@ -4133,6 +4251,21 @@ function buildApi(ctx, ptyManager, agentPtyRegistry, resolved, terminalShell, ge
4133
4251
  }
4134
4252
  return { ok: true };
4135
4253
  },
4254
+ "fs.rename": async (payload) => {
4255
+ const { cwd } = await cwdOf(payload);
4256
+ return renameWorkspaceEntry({
4257
+ cwd,
4258
+ path: requireString(payload, "path"),
4259
+ name: requireString(payload, "name")
4260
+ });
4261
+ },
4262
+ "fs.remove": async (payload) => {
4263
+ const { cwd } = await cwdOf(payload);
4264
+ return removeWorkspaceEntry({
4265
+ cwd,
4266
+ path: requireString(payload, "path")
4267
+ });
4268
+ },
4136
4269
  "git.worktrees": async (payload) => {
4137
4270
  const { cwd } = await gitCwdOf(payload);
4138
4271
  const selected = selectedRepoOf(payload);
@@ -4203,6 +4336,15 @@ function buildApi(ctx, ptyManager, agentPtyRegistry, resolved, terminalShell, ge
4203
4336
  const path = await resolveGitPath(cwd, requireString(payload, "path"), repoRoot);
4204
4337
  return { content: await show(cwd, requireString(payload, "rev"), path, repoRoot) };
4205
4338
  },
4339
+ "git.fold-contents": async (payload) => {
4340
+ const { cwd } = await gitCwdOf(payload);
4341
+ const repoRoot = selectedRepoOf(payload);
4342
+ const record = payload;
4343
+ return await foldContents(cwd, await resolveGitPath(cwd, requireString(record, "path"), repoRoot), {
4344
+ staged: record.staged === true,
4345
+ hash: typeof record.hash === "string" ? record.hash : void 0
4346
+ }, repoRoot);
4347
+ },
4206
4348
  "pty.close": (payload) => {
4207
4349
  const sessionId = requireString(payload, "sessionId");
4208
4350
  const tab = requireString(payload, "tab");
@@ -4214,6 +4356,13 @@ function buildApi(ctx, ptyManager, agentPtyRegistry, resolved, terminalShell, ge
4214
4356
  agentPtyRegistry?.close(uuid);
4215
4357
  return { ok: true };
4216
4358
  },
4359
+ "agent-pty.skip-wait": (payload) => {
4360
+ const uuid = requireString(payload, "uuid");
4361
+ return {
4362
+ ok: true,
4363
+ skipped: agentPtyRegistry?.skipWait(uuid) ?? 0
4364
+ };
4365
+ },
4217
4366
  "terminal.deps": () => depsStatus(),
4218
4367
  "jobs.output": (payload) => jobsApi.output(payload),
4219
4368
  "jobs.kill": (payload) => jobsApi.kill(payload),
@@ -1,3 +1,5 @@
1
+ import { type SessionScope } from './api.ts';
2
+ import type { SidebarDiffRef } from './state.ts';
1
3
  /** One rendered diff line. */
2
4
  export interface DiffLine {
3
5
  kind: 'ctx' | 'del' | 'add' | 'meta';
@@ -41,11 +43,41 @@ export interface ParsedDiff {
41
43
  * can still draw its path.
42
44
  */
43
45
  export declare function parseUnifiedDiff(text: string): ParsedDiff;
46
+ /** The old/new line range one hidden gap spans (both sides derive from the
47
+ * surrounding hunk headers and their counted rows). */
48
+ interface DiffFoldRange {
49
+ oldStart: number;
50
+ oldEnd: number;
51
+ newStart: number;
52
+ newEnd: number;
53
+ }
54
+ /**
55
+ * Materialize a git gap fold's hidden rows from the two sides' full file
56
+ * contents, by the fold's known line ranges: the old side drives context
57
+ * rows (each mapped onto the new side through the fold's offset — a gap is
58
+ * an unchanged run, so the sides align), and new-side lines the old range
59
+ * never reaches become pure additions. Line numbers clip to the actual
60
+ * content (a no-newline file's ranges can overrun by one); `\r` endings
61
+ * survive verbatim, like git's own context lines.
62
+ */
63
+ export declare function foldRowsFromContents(fold: DiffFoldRange, oldContent: string, newContent: string): DiffLine[];
44
64
  export interface DiffViewProps {
45
65
  /** Unified diff text (`git.diff` or `git.commit-diff` payloads). */
46
66
  diff: string;
47
67
  /** Untracked-file content: when present, renders as a full-file addition instead of parsing. */
48
68
  untrackedPath?: string;
49
69
  untrackedContent?: string;
70
+ /**
71
+ * When present (a worktree/commit diff ref plus its scope), hunk gaps
72
+ * render an expandable fold: clicking resolves both sides' full contents
73
+ * (`git.fold-contents`) and materializes the hidden context rows. Absent
74
+ * (or an untracked full-addition render) — no fold rows at all.
75
+ */
76
+ foldSource?: {
77
+ scope: SessionScope;
78
+ ref: SidebarDiffRef;
79
+ cwd: string | undefined;
80
+ };
50
81
  }
51
- export declare function DiffView({ diff, untrackedPath, untrackedContent }: DiffViewProps): import("react").JSX.Element | null;
82
+ export declare function DiffView({ diff, untrackedPath, untrackedContent, foldSource }: DiffViewProps): import("react").JSX.Element | null;
83
+ export {};
@@ -10,4 +10,7 @@ export declare function EditorHost(props: {
10
10
  revealed: string[];
11
11
  onToggleDir: (path: string) => void;
12
12
  onReferenceFile: (path: string, isDir: boolean) => void;
13
+ /** Tree-row mutations (passed through to the file tree; absent → hidden). */
14
+ onPathRenamed?: (oldPath: string, newPath: string) => void;
15
+ onPathRemoved?: (path: string) => void;
13
16
  }): import("react").JSX.Element;
@@ -35,4 +35,8 @@ export declare function FileTree(props: {
35
35
  onUploadRequest: (dir: string, items: UploadItem[]) => void;
36
36
  /** True while an upload is in flight (drops are ignored). */
37
37
  busy: boolean;
38
+ /** A tree row was renamed (retarget open tabs; absent → no rename entry). */
39
+ onPathRenamed?: (oldPath: string, newPath: string) => void;
40
+ /** A tree row was removed (close affected tabs; absent → no delete entry). */
41
+ onPathRemoved?: (path: string) => void;
38
42
  }): import("react").JSX.Element;
@@ -0,0 +1,6 @@
1
+ /** Truncate one needle for inline display (title attr carries the full text). */
2
+ export declare function truncateNeedle(needle: string): string;
3
+ export declare function TerminalWaitBanner(props: {
4
+ needle: string;
5
+ onSkip: () => void;
6
+ }): import("react").JSX.Element;
@@ -18,6 +18,9 @@ export declare function TreePanel(props: {
18
18
  onOpenWith?: (targetId: string, path: string) => void;
19
19
  onToggleOpenWithPin?: (targetId: string) => void;
20
20
  onReferenceFile: (path: string, isDir: boolean) => void;
21
+ /** Tree-row mutations (passed through to the file tree; absent → hidden). */
22
+ onPathRenamed?: (oldPath: string, newPath: string) => void;
23
+ onPathRemoved?: (path: string) => void;
21
24
  /** Full-window presentation: the panel fills its host instead of docking
22
25
  * at a fixed width. */
23
26
  full?: boolean;
@@ -138,6 +138,16 @@ export declare const api: {
138
138
  fsWrite: (scope: SessionScope, path: string, content: string) => Promise<{
139
139
  ok: true;
140
140
  }>;
141
+ /** Rename one tree row within its directory (single-segment name; a
142
+ * destination-existence clash is a 409; symlink rows rename the link). */
143
+ fsRename: (scope: SessionScope, path: string, name: string) => Promise<{
144
+ path: string;
145
+ }>;
146
+ /** Delete one tree row permanently (recursive for directories; a symlink
147
+ * row unlinks the link only). */
148
+ fsRemove: (scope: SessionScope, path: string) => Promise<{
149
+ path: string;
150
+ }>;
141
151
  /** Upload one file's raw bytes into `dir` (keeps the folder tree via
142
152
  * `relativePath`); the host streams it under the session workspace. */
143
153
  uploadFile: (scope: SessionScope, dir: string, relativePath: string, body: Blob, signal?: AbortSignal) => Promise<{
@@ -171,6 +181,16 @@ export declare const api: {
171
181
  gitCommitDiff: (scope: SessionScope, hash: string, worktree?: string, signal?: AbortSignal) => Promise<{
172
182
  diff: string;
173
183
  }>;
184
+ /** Both sides' full file contents for a diff-fold expansion; a missing
185
+ * side is null (untracked / deleted) and the view degrades the fold. */
186
+ gitFoldContents: (scope: SessionScope, opts: {
187
+ path: string;
188
+ staged?: boolean;
189
+ hash?: string;
190
+ }, worktree?: string, signal?: AbortSignal) => Promise<{
191
+ old: string | null;
192
+ new: string | null;
193
+ }>;
174
194
  /** Discard the worktree changes of one file (the index is untouched). */
175
195
  gitDiscard: (scope: SessionScope, path: string, worktree?: string) => Promise<{
176
196
  ok: true;
@@ -193,6 +213,12 @@ export declare const api: {
193
213
  agentPtyClose: (uuid: string) => Promise<{
194
214
  ok: true;
195
215
  }>;
216
+ /** Skip every active terminal_wait_for on one agent terminal (the wait
217
+ * banner's skip button). Idempotent: {skipped:0} when none is active. */
218
+ agentSkipWait: (uuid: string) => Promise<{
219
+ ok: true;
220
+ skipped: number;
221
+ }>;
196
222
  /** Terminal dependency status (issue #140): after a WS close 1011 with
197
223
  * reason `pty-deps-missing` the view fetches the full repair details here
198
224
  * (the close reason itself is capped at 123 bytes). */
@@ -159,6 +159,18 @@ export declare const zh: {
159
159
  producedOpen: string;
160
160
  showInFolder: string;
161
161
  disconnected: string;
162
+ terminalWaitBanner: string;
163
+ terminalSkipWait: string;
164
+ gitFoldExpand: string;
165
+ gitFoldLoading: string;
166
+ gitFoldFailed: string;
167
+ rename: string;
168
+ renameInvalid: string;
169
+ delete: string;
170
+ deleteTitle: string;
171
+ deleteDescFile: string;
172
+ deleteDescDir: string;
173
+ dismiss: string;
162
174
  exited: string;
163
175
  noSession: string;
164
176
  pluginNotLoaded: string;
@@ -110,6 +110,17 @@ export interface SidebarState {
110
110
  splits: SplitNode;
111
111
  /** Free windows (tabs dragged out onto the conversation area). */
112
112
  floats: FloatWindow[];
113
+ /**
114
+ * Live agent-terminal wait state (uuid → the wait the model currently
115
+ * blocks on in `terminal_wait_for`), mirrored from the host's
116
+ * agent-terminals push. Transient by design: sanitizeState never restores
117
+ * it, so a reload starts clean and the next push (sent immediately on WS
118
+ * attach) repopulates it.
119
+ */
120
+ agentWaits: Record<string, {
121
+ needle: string;
122
+ since: number;
123
+ }>;
113
124
  }
114
125
  export declare const PANEL_MIN = 280;
115
126
  export declare const PANEL_MAX = 640;
@@ -325,6 +336,10 @@ export declare function agentTabId(uuid: string): string;
325
336
  export declare function reconcileAgentTerminals(state: SidebarState, agentTerminals: ReadonlyArray<{
326
337
  uuid: string;
327
338
  title: string;
339
+ waiting?: {
340
+ needle: string;
341
+ since: number;
342
+ } | null;
328
343
  }>): SidebarState;
329
344
  /** Immutable snapshot handed to React (replaced only on real changes). */
330
345
  export interface SidebarSnapshot {
@@ -26,3 +26,45 @@ export declare function writeWorkspaceUpload(input: WorkspaceUploadInput): Promi
26
26
  path: string;
27
27
  size: number;
28
28
  }>;
29
+ /** Inputs of one tree-row rename. */
30
+ export interface WorkspaceRenameInput {
31
+ /** The session workspace root; the renamed entry must stay inside it. */
32
+ cwd: string;
33
+ /** Absolute path of the row as the tree displays it (may be a symlink). */
34
+ path: string;
35
+ /** The new base name (single segment — rename never moves across directories). */
36
+ name: string;
37
+ }
38
+ /**
39
+ * Rename one tree row within its directory: `path` → `<parent>/<name>`.
40
+ * The new name must be a single path segment (this is rename, not move);
41
+ * an existing destination is refused (POSIX rename would clobber it
42
+ * silently); the workspace root itself is never renamable; a symlink row
43
+ * renames the link, not its target. A no-op rename (same name) succeeds
44
+ * without touching the filesystem.
45
+ *
46
+ * @throws SidebarError with a wire code for shape, containment, existence
47
+ * and root failures.
48
+ */
49
+ export declare function renameWorkspaceEntry(input: WorkspaceRenameInput): Promise<{
50
+ path: string;
51
+ }>;
52
+ /** Inputs of one tree-row delete. */
53
+ export interface WorkspaceRemoveInput {
54
+ /** The session workspace root; the removed entry must stay inside it. */
55
+ cwd: string;
56
+ /** Absolute path of the row as the tree displays it (may be a symlink). */
57
+ path: string;
58
+ }
59
+ /**
60
+ * Delete one tree row permanently (there is no trash on the host): files are
61
+ * unlinked, directories removed recursively, a symlink row unlinks the LINK
62
+ * only (lstat decides, so a link to a directory does not recurse into its
63
+ * target). The workspace root itself is never removable.
64
+ *
65
+ * @throws SidebarError with a wire code for containment, existence and
66
+ * root failures.
67
+ */
68
+ export declare function removeWorkspaceEntry(input: WorkspaceRemoveInput): Promise<{
69
+ path: string;
70
+ }>;
@@ -112,6 +112,21 @@ export declare function log(cwd: string, count?: number, skip?: number, selected
112
112
  * revision has no such path (a new/untracked file has no HEAD side).
113
113
  */
114
114
  export declare function show(cwd: string, rev: string, path: string, selected?: string): Promise<string | null>;
115
+ /**
116
+ * Both sides' full file contents for a diff-fold expansion. `path` is
117
+ * repo-relative. The sides resolve per diff kind: a commit reads
118
+ * `<hash>^` vs `<hash>`; a staged change reads HEAD vs the index (`:`);
119
+ * an unstaged change reads HEAD vs the working tree file on disk (a side
120
+ * that does not exist — untracked, deleted, binary-refused — comes back
121
+ * null and the client degrades the fold to a static marker).
122
+ */
123
+ export declare function foldContents(cwd: string, path: string, opts?: {
124
+ staged?: boolean;
125
+ hash?: string;
126
+ }, selected?: string): Promise<{
127
+ old: string | null;
128
+ new: string | null;
129
+ }>;
115
130
  /** Full patch text of one commit (`git show` with the commit header suppressed).
116
131
  * Merge commits show their diff against the first parent (`-m --first-parent`
117
132
  * is a no-op for regular commits), so a history click always has content. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-coding-sidebar",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
4
4
  "description": "DSH web plugin: a VSCode-like right sidebar (explorer / editor / terminal / git / browser), isolated per conversation session. Exposes the betterSidebar service for other plugins to register sidebar tabs and file viewers. KCoder-maintained fork of DSH-better-sidebar 0.17.2 (bottom panel removed, upstream-decoupled release line).",
5
5
  "type": "module",
6
6
  "repository": {
@@ -103,7 +103,16 @@ export function DiffTab(props: { sessionId: string; cwd: string | undefined; dif
103
103
  <>
104
104
  {data.untracked !== undefined
105
105
  ? <DiffView diff="" untrackedPath={diff.kind === 'worktree' ? diff.path : ''} untrackedContent={data.untracked} />
106
- : <DiffView diff={data.diff} />}
106
+ : (
107
+ <DiffView
108
+ diff={data.diff}
109
+ foldSource={{
110
+ scope: { sessionId, cwd, ...(diff.repoRoot !== undefined ? { repoRoot: diff.repoRoot } : {}) },
111
+ ref: diff,
112
+ cwd,
113
+ }}
114
+ />
115
+ )}
107
116
  {data.diff === '' && data.untracked === undefined && (
108
117
  <div className={css.gitEmpty}>{t('diffEmpty')}</div>
109
118
  )}