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.
- package/README.md +2 -2
- package/lib/client-editor.js +257 -178
- package/lib/client-registry.js +959 -345
- package/lib/client-terminal.js +307 -172
- package/lib/client.js +952 -338
- package/lib/index.js +214 -2
- package/lib/types/changes-ops.d.ts +31 -0
- package/lib/types/client/DiffView.d.ts +33 -1
- package/lib/types/client/EditorHost.d.ts +3 -0
- package/lib/types/client/FileTree.d.ts +4 -0
- package/lib/types/client/SessionLens.d.ts +4 -0
- package/lib/types/client/TerminalWaitBanner.d.ts +6 -0
- package/lib/types/client/TreePanel.d.ts +3 -0
- package/lib/types/client/api.d.ts +36 -0
- package/lib/types/client/locales.d.ts +20 -0
- package/lib/types/client/redact.d.ts +14 -0
- package/lib/types/client/state.d.ts +15 -0
- package/lib/types/fs-operations.d.ts +42 -0
- package/lib/types/git.d.ts +15 -0
- package/package.json +1 -1
- package/src/changes-ops.ts +78 -0
- package/src/client/DiffTab.tsx +10 -1
- package/src/client/DiffView.tsx +174 -19
- package/src/client/EditorHost.tsx +8 -1
- package/src/client/FileTree.tsx +147 -4
- package/src/client/GitView.tsx +37 -0
- package/src/client/SessionLens.tsx +95 -0
- package/src/client/Sidebar.tsx +54 -3
- package/src/client/TerminalView.tsx +28 -0
- package/src/client/TerminalWaitBanner.tsx +32 -0
- package/src/client/TreePanel.tsx +6 -1
- package/src/client/api.ts +24 -0
- package/src/client/locales-ar.ts +20 -0
- package/src/client/locales-de.ts +20 -0
- package/src/client/locales-fr.ts +20 -0
- package/src/client/locales-hi.ts +20 -0
- package/src/client/locales-id.ts +20 -0
- package/src/client/locales-it.ts +20 -0
- package/src/client/locales-ja.ts +20 -0
- package/src/client/locales-ko.ts +20 -0
- package/src/client/locales-nl.ts +20 -0
- package/src/client/locales-pl.ts +20 -0
- package/src/client/locales-pt.ts +20 -0
- package/src/client/locales-ru.ts +20 -0
- package/src/client/locales-sv.ts +20 -0
- package/src/client/locales-th.ts +20 -0
- package/src/client/locales-tr.ts +20 -0
- package/src/client/locales-vi.ts +20 -0
- package/src/client/locales-zh-HK.ts +20 -0
- package/src/client/locales-zh-MO.ts +20 -0
- package/src/client/locales-zh-TW.ts +20 -0
- package/src/client/locales.ts +40 -0
- package/src/client/redact.ts +39 -0
- package/src/client/sidebar.module.css +183 -0
- package/src/client/state.ts +42 -3
- package/src/fs-operations.ts +126 -4
- package/src/git.ts +39 -2
- 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
|
|
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,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
|
+
}>;
|
package/lib/types/git.d.ts
CHANGED
|
@@ -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.
|
|
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": {
|