dsh-coding-sidebar 1.0.8 → 1.0.10

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 (58) hide show
  1. package/README.md +2 -2
  2. package/lib/client-editor.js +257 -178
  3. package/lib/client-registry.js +959 -345
  4. package/lib/client-terminal.js +307 -172
  5. package/lib/client.js +952 -338
  6. package/lib/index.js +214 -2
  7. package/lib/types/changes-ops.d.ts +31 -0
  8. package/lib/types/client/DiffView.d.ts +33 -1
  9. package/lib/types/client/EditorHost.d.ts +3 -0
  10. package/lib/types/client/FileTree.d.ts +4 -0
  11. package/lib/types/client/SessionLens.d.ts +4 -0
  12. package/lib/types/client/TerminalWaitBanner.d.ts +6 -0
  13. package/lib/types/client/TreePanel.d.ts +3 -0
  14. package/lib/types/client/api.d.ts +36 -0
  15. package/lib/types/client/locales.d.ts +20 -0
  16. package/lib/types/client/redact.d.ts +14 -0
  17. package/lib/types/client/state.d.ts +15 -0
  18. package/lib/types/fs-operations.d.ts +42 -0
  19. package/lib/types/git.d.ts +15 -0
  20. package/package.json +1 -1
  21. package/src/changes-ops.ts +78 -0
  22. package/src/client/DiffTab.tsx +10 -1
  23. package/src/client/DiffView.tsx +174 -19
  24. package/src/client/EditorHost.tsx +8 -1
  25. package/src/client/FileTree.tsx +147 -4
  26. package/src/client/GitView.tsx +37 -0
  27. package/src/client/SessionLens.tsx +95 -0
  28. package/src/client/Sidebar.tsx +54 -3
  29. package/src/client/TerminalView.tsx +28 -0
  30. package/src/client/TerminalWaitBanner.tsx +32 -0
  31. package/src/client/TreePanel.tsx +6 -1
  32. package/src/client/api.ts +24 -0
  33. package/src/client/locales-ar.ts +20 -0
  34. package/src/client/locales-de.ts +20 -0
  35. package/src/client/locales-fr.ts +20 -0
  36. package/src/client/locales-hi.ts +20 -0
  37. package/src/client/locales-id.ts +20 -0
  38. package/src/client/locales-it.ts +20 -0
  39. package/src/client/locales-ja.ts +20 -0
  40. package/src/client/locales-ko.ts +20 -0
  41. package/src/client/locales-nl.ts +20 -0
  42. package/src/client/locales-pl.ts +20 -0
  43. package/src/client/locales-pt.ts +20 -0
  44. package/src/client/locales-ru.ts +20 -0
  45. package/src/client/locales-sv.ts +20 -0
  46. package/src/client/locales-th.ts +20 -0
  47. package/src/client/locales-tr.ts +20 -0
  48. package/src/client/locales-vi.ts +20 -0
  49. package/src/client/locales-zh-HK.ts +20 -0
  50. package/src/client/locales-zh-MO.ts +20 -0
  51. package/src/client/locales-zh-TW.ts +20 -0
  52. package/src/client/locales.ts +40 -0
  53. package/src/client/redact.ts +39 -0
  54. package/src/client/sidebar.module.css +183 -0
  55. package/src/client/state.ts +42 -3
  56. package/src/fs-operations.ts +126 -4
  57. package/src/git.ts +39 -2
  58. package/src/index.ts +56 -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,144 @@ 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
+ }
534
+ //#endregion
535
+ //#region src/changes-ops.ts
536
+ /** Argument keys a file-addressing tool may use for its target path. */
537
+ const PATH_KEYS = [
538
+ "path",
539
+ "file_path",
540
+ "filePath",
541
+ "notebook_path",
542
+ "filename"
543
+ ];
544
+ /**
545
+ * Whether a tool name looks like it MUTATES files. Deliberately coarse
546
+ * (substring match on the mutating verbs) so host-side and plugin-side
547
+ * file tools both qualify; read-only tools never match.
548
+ */
549
+ function isWriteTool(name) {
550
+ const lowered = name.toLowerCase();
551
+ return /write|edit|patch|apply|create_file|insert/.test(lowered);
552
+ }
553
+ /** Extract the addressed path from one tool call's arguments JSON. */
554
+ function argumentPath(args) {
555
+ if (args === "") return void 0;
556
+ try {
557
+ const parsed = JSON.parse(args);
558
+ for (const key of PATH_KEYS) {
559
+ const value = parsed[key];
560
+ if (typeof value === "string" && value !== "") return value;
561
+ }
562
+ } catch {}
563
+ }
564
+ /**
565
+ * Fold a session event log into the deduplicated file-operation list,
566
+ * newest first. `tool/call` events with a mutating tool name and an
567
+ * addressable path are collected; every path keeps only its latest call
568
+ * (plus a touch count). Rows outside a live sessions registry read come
569
+ * back as an empty list — the page degrades to the empty state.
570
+ * @param events - the session's append-only event log (oldest → newest).
571
+ */
572
+ function sessionFileOps(events) {
573
+ const byPath = /* @__PURE__ */ new Map();
574
+ for (let index = events.length - 1; index >= 0; index -= 1) {
575
+ const event = events[index];
576
+ if (event === void 0 || event.type !== "tool/call") continue;
577
+ const name = typeof event.data.name === "string" ? event.data.name : "";
578
+ if (name === "" || !isWriteTool(name)) continue;
579
+ const path = argumentPath(typeof event.data.arguments === "string" ? event.data.arguments : "");
580
+ if (path === void 0) continue;
581
+ const existing = byPath.get(path);
582
+ if (existing === void 0) byPath.set(path, {
583
+ path,
584
+ tool: name,
585
+ time: event.time,
586
+ count: 1
587
+ });
588
+ else existing.count += 1;
589
+ }
590
+ return [...byPath.values()].sort((left, right) => right.time - left.time);
591
+ }
446
592
  //#endregion
447
593
  //#region src/fs-search.ts
448
594
  /**
@@ -1201,6 +1347,36 @@ async function show(cwd, rev, path, selected) {
1201
1347
  return null;
1202
1348
  }
1203
1349
  }
1350
+ /**
1351
+ * Both sides' full file contents for a diff-fold expansion. `path` is
1352
+ * repo-relative. The sides resolve per diff kind: a commit reads
1353
+ * `<hash>^` vs `<hash>`; a staged change reads HEAD vs the index (`:`);
1354
+ * an unstaged change reads HEAD vs the working tree file on disk (a side
1355
+ * that does not exist — untracked, deleted, binary-refused — comes back
1356
+ * null and the client degrades the fold to a static marker).
1357
+ */
1358
+ async function foldContents(cwd, path, opts = {}, selected) {
1359
+ const root = await repoRoot(cwd, selected);
1360
+ if (opts.hash !== void 0) {
1361
+ const [old, neu] = await Promise.all([show(root, `${opts.hash}^`, path, selected), show(root, opts.hash, path, selected)]);
1362
+ return {
1363
+ old,
1364
+ new: neu
1365
+ };
1366
+ }
1367
+ if (opts.staged === true) {
1368
+ const [old, neu] = await Promise.all([show(root, "HEAD", path, selected), show(root, ":", path, selected)]);
1369
+ return {
1370
+ old,
1371
+ new: neu
1372
+ };
1373
+ }
1374
+ const [old, neu] = await Promise.all([show(root, "HEAD", path, selected), readFile(isAbsolute(path) ? path : join(root, path), "utf8").catch(() => null)]);
1375
+ return {
1376
+ old,
1377
+ new: neu
1378
+ };
1379
+ }
1204
1380
  /** Full patch text of one commit (`git show` with the commit header suppressed).
1205
1381
  * Merge commits show their diff against the first parent (`-m --first-parent`
1206
1382
  * is a no-op for regular commits), so a history click always has content. */
@@ -4133,6 +4309,21 @@ function buildApi(ctx, ptyManager, agentPtyRegistry, resolved, terminalShell, ge
4133
4309
  }
4134
4310
  return { ok: true };
4135
4311
  },
4312
+ "fs.rename": async (payload) => {
4313
+ const { cwd } = await cwdOf(payload);
4314
+ return renameWorkspaceEntry({
4315
+ cwd,
4316
+ path: requireString(payload, "path"),
4317
+ name: requireString(payload, "name")
4318
+ });
4319
+ },
4320
+ "fs.remove": async (payload) => {
4321
+ const { cwd } = await cwdOf(payload);
4322
+ return removeWorkspaceEntry({
4323
+ cwd,
4324
+ path: requireString(payload, "path")
4325
+ });
4326
+ },
4136
4327
  "git.worktrees": async (payload) => {
4137
4328
  const { cwd } = await gitCwdOf(payload);
4138
4329
  const selected = selectedRepoOf(payload);
@@ -4203,6 +4394,15 @@ function buildApi(ctx, ptyManager, agentPtyRegistry, resolved, terminalShell, ge
4203
4394
  const path = await resolveGitPath(cwd, requireString(payload, "path"), repoRoot);
4204
4395
  return { content: await show(cwd, requireString(payload, "rev"), path, repoRoot) };
4205
4396
  },
4397
+ "git.fold-contents": async (payload) => {
4398
+ const { cwd } = await gitCwdOf(payload);
4399
+ const repoRoot = selectedRepoOf(payload);
4400
+ const record = payload;
4401
+ return await foldContents(cwd, await resolveGitPath(cwd, requireString(record, "path"), repoRoot), {
4402
+ staged: record.staged === true,
4403
+ hash: typeof record.hash === "string" ? record.hash : void 0
4404
+ }, repoRoot);
4405
+ },
4206
4406
  "pty.close": (payload) => {
4207
4407
  const sessionId = requireString(payload, "sessionId");
4208
4408
  const tab = requireString(payload, "tab");
@@ -4214,9 +4414,21 @@ function buildApi(ctx, ptyManager, agentPtyRegistry, resolved, terminalShell, ge
4214
4414
  agentPtyRegistry?.close(uuid);
4215
4415
  return { ok: true };
4216
4416
  },
4417
+ "agent-pty.skip-wait": (payload) => {
4418
+ const uuid = requireString(payload, "uuid");
4419
+ return {
4420
+ ok: true,
4421
+ skipped: agentPtyRegistry?.skipWait(uuid) ?? 0
4422
+ };
4423
+ },
4217
4424
  "terminal.deps": () => depsStatus(),
4218
4425
  "jobs.output": (payload) => jobsApi.output(payload),
4219
4426
  "jobs.kill": (payload) => jobsApi.kill(payload),
4427
+ "changes.ops": async (payload) => {
4428
+ const sessionId = requireString(payload, "sessionId");
4429
+ const stored = ctx.sessions.get(sessionId);
4430
+ return { ops: sessionFileOps(stored?.snapshotEvents !== void 0 ? stored.snapshotEvents() : []) };
4431
+ },
4220
4432
  "subagents.live": (payload) => subagentLiveApi.live(payload),
4221
4433
  "shell.get": () => ({
4222
4434
  shell: terminalShell,
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Pure derivation of the "session lens": the file operations the model
3
+ * performed in one session, parsed from the session's own event log (the
4
+ * same durable log the side chat reads — nothing here touches the host
5
+ * registry or the model's cursors). Kept framework-free so the parser is
6
+ * unit-testable in the node environment.
7
+ */
8
+ import type { SidebarSessionEvent } from './context-types.ts';
9
+ /**
10
+ * One deduplicated file operation: the LATEST write-shaped tool call that
11
+ * touched `path` (earlier calls to the same file fold into it).
12
+ */
13
+ export interface SessionFileOp {
14
+ /** The file path as the tool call addressed it (verbatim). */
15
+ path: string;
16
+ /** The tool that performed the latest operation (e.g. write_file). */
17
+ tool: string;
18
+ /** Epoch ms of the event. */
19
+ time: number;
20
+ /** How many write-shaped calls touched this path in total. */
21
+ count: number;
22
+ }
23
+ /**
24
+ * Fold a session event log into the deduplicated file-operation list,
25
+ * newest first. `tool/call` events with a mutating tool name and an
26
+ * addressable path are collected; every path keeps only its latest call
27
+ * (plus a touch count). Rows outside a live sessions registry read come
28
+ * back as an empty list — the page degrades to the empty state.
29
+ * @param events - the session's append-only event log (oldest → newest).
30
+ */
31
+ export declare function sessionFileOps(events: readonly SidebarSessionEvent[]): SessionFileOp[];
@@ -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,4 @@
1
+ import { type SessionScope } from './api.ts';
2
+ export declare function SessionLens(props: {
3
+ scope: SessionScope;
4
+ }): 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,22 @@ 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
+ }>;
222
+ /** The session lens: file operations the model performed in one session
223
+ * (parsed from the session's own event log; newest first). */
224
+ changesOps: (scope: SessionScope, signal?: AbortSignal) => Promise<{
225
+ ops: Array<{
226
+ path: string;
227
+ tool: string;
228
+ time: number;
229
+ count: number;
230
+ }>;
231
+ }>;
196
232
  /** Terminal dependency status (issue #140): after a WS close 1011 with
197
233
  * reason `pty-deps-missing` the view fetches the full repair details here
198
234
  * (the close reason itself is capped at 123 bytes). */
@@ -159,6 +159,26 @@ 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;
174
+ changesSessionGit: string;
175
+ changesSessionLens: string;
176
+ changesEmpty: string;
177
+ changesCount: string;
178
+ changesRedacted: string;
179
+ changesBinary: string;
180
+ changesPreviewError: string;
181
+ changesLens: string;
162
182
  exited: string;
163
183
  noSession: string;
164
184
  pluginNotLoaded: string;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Secret redaction for the session lens' file preview: heuristic masking of
3
+ * credential-shaped strings (API keys, bearer tokens, private key blocks,
4
+ * password assignments) before file content is shown in the sidebar. This
5
+ * layer applies ONLY to the session lens' preview pane — ordinary file reads
6
+ * (editor, untracked diff fallback) never pass through it, so ordinary
7
+ * files' content is untouched.
8
+ */
9
+ /**
10
+ * Mask credential-shaped strings in `text`. Best-effort by design: the goal
11
+ * is to keep the common accident (a key echoed into a file the model wrote)
12
+ * out of the sidebar, not to parse every secret format ever shipped.
13
+ */
14
+ export declare function redactSecrets(text: string): 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.10",
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": {