dsh-coding-sidebar 1.0.7 → 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.
- package/README.md +2 -2
- package/lib/client-editor.js +485 -228
- package/lib/client-registry.js +1238 -7583
- package/lib/client-terminal.js +345 -189
- package/lib/client.js +1213 -7558
- package/lib/index.js +518 -69
- package/lib/types/agent-pty.d.ts +62 -4
- package/lib/types/bundle-route.d.ts +1 -1
- package/lib/types/client/DiffView.d.ts +33 -1
- package/lib/types/client/EditorHost.d.ts +4 -1
- package/lib/types/client/FileTree.d.ts +6 -2
- package/lib/types/client/TerminalWaitBanner.d.ts +6 -0
- package/lib/types/client/TreePanel.d.ts +4 -1
- package/lib/types/client/api.d.ts +26 -0
- package/lib/types/client/chunk-loader.d.ts +1 -1
- package/lib/types/client/chunks/locale.d.ts +3 -0
- package/lib/types/client/conversation-draft.d.ts +85 -4
- package/lib/types/client/locales.d.ts +13 -20
- package/lib/types/client/selection-popup.d.ts +58 -0
- package/lib/types/client/service.d.ts +1 -1
- package/lib/types/client/state.d.ts +15 -0
- package/lib/types/client/terminal-font.d.ts +25 -2
- package/lib/types/context-types.d.ts +3 -1
- package/lib/types/fs-operations.d.ts +42 -0
- package/lib/types/git.d.ts +15 -0
- package/lib/types/index.d.ts +9 -0
- package/lib/types/prefs-shared.d.ts +7 -5
- package/lib/types/pty-manager.d.ts +53 -0
- package/lib/types/wire.d.ts +6 -2
- package/package.json +1 -1
- package/src/agent-pty.ts +221 -33
- package/src/bundle-route.ts +1 -1
- package/src/client/DiffTab.tsx +10 -1
- package/src/client/DiffView.tsx +174 -19
- package/src/client/EditorHost.tsx +9 -2
- package/src/client/FileTree.tsx +151 -8
- package/src/client/Sidebar.tsx +95 -25
- package/src/client/TerminalView.tsx +41 -0
- package/src/client/TerminalWaitBanner.tsx +32 -0
- package/src/client/TextEditor.tsx +27 -45
- package/src/client/TreePanel.tsx +22 -2
- package/src/client/api.ts +20 -0
- package/src/client/chunk-loader.ts +1 -1
- package/src/client/chunks/locale.tsx +60 -0
- package/src/client/conversation-draft.ts +233 -7
- package/src/client/index.tsx +19 -8
- package/src/client/locales-ar.ts +17 -4
- package/src/client/locales-de.ts +17 -4
- package/src/client/locales-fr.ts +17 -4
- package/src/client/locales-hi.ts +17 -4
- package/src/client/locales-id.ts +17 -4
- package/src/client/locales-it.ts +17 -4
- package/src/client/locales-ja.ts +17 -4
- package/src/client/locales-ko.ts +17 -4
- package/src/client/locales-nl.ts +17 -4
- package/src/client/locales-pl.ts +17 -4
- package/src/client/locales-pt.ts +17 -4
- package/src/client/locales-ru.ts +17 -4
- package/src/client/locales-sv.ts +17 -4
- package/src/client/locales-th.ts +17 -4
- package/src/client/locales-tr.ts +17 -4
- package/src/client/locales-vi.ts +17 -4
- package/src/client/locales-zh-HK.ts +17 -4
- package/src/client/locales-zh-MO.ts +17 -4
- package/src/client/locales-zh-TW.ts +17 -4
- package/src/client/locales.ts +41 -51
- package/src/client/selection-popup.ts +155 -0
- package/src/client/service.ts +1 -1
- package/src/client/sidebar.module.css +68 -0
- package/src/client/state.ts +56 -10
- package/src/client/terminal-font.ts +34 -3
- package/src/context-types.ts +3 -2
- package/src/fs-operations.ts +126 -4
- package/src/git.ts +56 -3
- package/src/index.ts +82 -7
- package/src/open-external.ts +3 -4
- package/src/prefs-shared.ts +7 -5
- package/src/pty-manager.ts +176 -5
- package/src/sidechat-routes.ts +13 -1
- package/src/tools.ts +24 -6
- package/src/wire.ts +3 -0
package/lib/types/agent-pty.d.ts
CHANGED
|
@@ -15,6 +15,20 @@ export declare function clampDims(cols: number, rows: number): {
|
|
|
15
15
|
cols: number;
|
|
16
16
|
rows: number;
|
|
17
17
|
};
|
|
18
|
+
/**
|
|
19
|
+
* Arm the Windows pre-ready resize gate for one freshly spawned pty.
|
|
20
|
+
* No-op on POSIX and for injected ptys without `onData`.
|
|
21
|
+
*/
|
|
22
|
+
export declare function armPtyResizeGate(pty: IPty): void;
|
|
23
|
+
/**
|
|
24
|
+
* Best-effort resize for WebSocket-driven terminal views. Layout animation
|
|
25
|
+
* can briefly produce unusable dimensions, and node-pty can reject a resize
|
|
26
|
+
* after the socket setup's outer try/catch has returned. Ignore that one
|
|
27
|
+
* frame so the host stays alive and a later valid measurement can retry.
|
|
28
|
+
* Returns whether node-pty accepted the resize (or parked it for replay on
|
|
29
|
+
* the first output — the Windows pre-ready window).
|
|
30
|
+
*/
|
|
31
|
+
export declare function tryResizePty(pty: Pick<IPty, 'resize'>, cols: number, rows: number): boolean;
|
|
18
32
|
/**
|
|
19
33
|
* Serializable snapshot of one agent terminal — the shape the model sees
|
|
20
34
|
* through `terminal_list` and the sidebar sees through the push endpoint.
|
|
@@ -36,6 +50,15 @@ export interface AgentTerminalSnapshot {
|
|
|
36
50
|
exitCode?: number | null;
|
|
37
51
|
/** Exit signal name if the process was killed by a signal; null otherwise. */
|
|
38
52
|
exitSignal?: string | null;
|
|
53
|
+
/**
|
|
54
|
+
* The model's active `terminal_wait_for` on this terminal (the sidebar
|
|
55
|
+
* renders the wait banner from it). Present only while a wait is
|
|
56
|
+
* registered; carries the LATEST wait when several overlap.
|
|
57
|
+
*/
|
|
58
|
+
waiting?: {
|
|
59
|
+
needle: string;
|
|
60
|
+
since: number;
|
|
61
|
+
};
|
|
39
62
|
}
|
|
40
63
|
/** One live agent terminal. */
|
|
41
64
|
export interface AgentTerminalHandle {
|
|
@@ -59,6 +82,17 @@ export interface AgentTerminalHandle {
|
|
|
59
82
|
exitCode?: number | null;
|
|
60
83
|
/** Exit signal number once known (POSIX only; undefined on Windows). */
|
|
61
84
|
exitSignal?: number | null;
|
|
85
|
+
/** Active wait_for registrations (skip bookkeeping; empty while idle). */
|
|
86
|
+
waits: AgentTerminalActiveWait[];
|
|
87
|
+
}
|
|
88
|
+
/** One active wait_for registration on a handle (banner + skip bookkeeping). */
|
|
89
|
+
export interface AgentTerminalActiveWait {
|
|
90
|
+
/** The needle being awaited (shown on the sidebar wait banner). */
|
|
91
|
+
needle: string;
|
|
92
|
+
/** Epoch ms when the wait registered (age display / debugging). */
|
|
93
|
+
since: number;
|
|
94
|
+
/** Flipped by `skipWait()`; the waiting poll returns `skipped` within one tick. */
|
|
95
|
+
skipped: boolean;
|
|
62
96
|
}
|
|
63
97
|
/** Read result shape (mirrors the official tool-pty terminal_read contract). */
|
|
64
98
|
export interface AgentTerminalReadResult {
|
|
@@ -73,14 +107,18 @@ export interface AgentTerminalReadResult {
|
|
|
73
107
|
}
|
|
74
108
|
/** Outcome of {@link AgentPtyRegistry.waitFor}. */
|
|
75
109
|
export type AgentTerminalWaitResult = {
|
|
76
|
-
/** The
|
|
110
|
+
/** The pattern that was awaited. */
|
|
77
111
|
kind: 'found';
|
|
78
|
-
/** The matched substring. */
|
|
79
112
|
needle: string;
|
|
80
113
|
/** 0-based line index (in the retained transcript) where the needle first appeared. */
|
|
81
114
|
line: number;
|
|
82
115
|
/** 0-based column index within that line where the match starts. */
|
|
83
116
|
column: number;
|
|
117
|
+
/**
|
|
118
|
+
* The text that actually matched — for multi-outcome patterns
|
|
119
|
+
* (e.g. `(BUILD_OK|BUILD_FAIL)`) this tells which alternative matched.
|
|
120
|
+
*/
|
|
121
|
+
match: string;
|
|
84
122
|
/** Elapsed wall-clock milliseconds from the wait start to the match. */
|
|
85
123
|
elapsedMs: number;
|
|
86
124
|
} | {
|
|
@@ -101,6 +139,11 @@ export type AgentTerminalWaitResult = {
|
|
|
101
139
|
exitCode?: number | null;
|
|
102
140
|
/** The exit signal name, if the process was killed by a signal. */
|
|
103
141
|
exitSignal?: string | null;
|
|
142
|
+
} | {
|
|
143
|
+
/** The user skipped the wait from the sidebar banner. */
|
|
144
|
+
kind: 'skipped';
|
|
145
|
+
/** The needle that was awaited. */
|
|
146
|
+
needle: string;
|
|
104
147
|
};
|
|
105
148
|
/** Snapshot projection of a handle (drops the pty reference and transcript). */
|
|
106
149
|
export declare function snapshotOf(handle: AgentTerminalHandle): AgentTerminalSnapshot;
|
|
@@ -181,12 +224,27 @@ export declare class AgentPtyRegistry {
|
|
|
181
224
|
* make event-driven wakeups unreliable. A 50ms poll is fast enough for
|
|
182
225
|
* interactive use and simple enough to be obviously correct.
|
|
183
226
|
* @param uuid - terminal to watch.
|
|
184
|
-
* @param needle -
|
|
227
|
+
* @param needle - JavaScript regular expression to search for
|
|
228
|
+
* (case-sensitive); a pattern that fails to compile falls back to
|
|
229
|
+
* verbatim substring matching. May cover several outcomes at once
|
|
230
|
+
* (e.g. `(BUILD_OK|BUILD_FAIL)` for build success vs failure) — the
|
|
231
|
+
* returned `match` reports the text that actually matched, so callers
|
|
232
|
+
* can tell which outcome hit.
|
|
185
233
|
* @param timeoutMs - max wait; default 10000 (10s). Clamped to ≥100ms.
|
|
186
234
|
* @param signal - caller-owned cancellation; aborts the wait re-throwing.
|
|
187
|
-
*
|
|
235
|
+
* A wait can also be skipped by the user from the sidebar banner
|
|
236
|
+
* (`skipWait`), which resolves it with `{kind:'skipped'}`.
|
|
237
|
+
* @returns one of `found` / `timeout` / `exited` / `skipped`.
|
|
188
238
|
*/
|
|
189
239
|
waitFor(uuid: string, needle: string, timeoutMs?: number, signal?: AbortSignal): Promise<AgentTerminalWaitResult>;
|
|
240
|
+
/**
|
|
241
|
+
* Mark every active wait on one terminal as skipped (the sidebar banner's
|
|
242
|
+
* skip button). Each waiting poll loop observes its record's flag within
|
|
243
|
+
* one 50ms tick and returns `{kind:'skipped'}`. Idempotent: 0 when nothing
|
|
244
|
+
* is waiting (a stale banner racing a wait that already resolved).
|
|
245
|
+
* @returns the number of waits that transitioned to skipped.
|
|
246
|
+
*/
|
|
247
|
+
skipWait(uuid: string): number;
|
|
190
248
|
/**
|
|
191
249
|
* Send a POSIX signal to a terminal's foreground process.
|
|
192
250
|
*
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Context, SidebarHttpRequest, SidebarHttpResponse } from './context-types.ts';
|
|
2
2
|
/** The chunk names the client may request (mirror of src/client/chunk-loader.ts). */
|
|
3
|
-
export declare const CHUNK_NAMES: readonly ["terminal", "editor"];
|
|
3
|
+
export declare const CHUNK_NAMES: readonly ["terminal", "editor", "locale"];
|
|
4
4
|
export type ChunkName = (typeof CHUNK_NAMES)[number];
|
|
5
5
|
/**
|
|
6
6
|
* Build the /sidebar/bundle route handler. `fence` is the shared browser-
|
|
@@ -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 {};
|
|
@@ -9,5 +9,8 @@ export declare function EditorHost(props: {
|
|
|
9
9
|
expanded: string[];
|
|
10
10
|
revealed: string[];
|
|
11
11
|
onToggleDir: (path: string) => void;
|
|
12
|
-
onReferenceFile: (path: string) => void;
|
|
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;
|
|
@@ -27,12 +27,16 @@ export declare function FileTree(props: {
|
|
|
27
27
|
onOpenWith?: (targetId: string, path: string) => void;
|
|
28
28
|
/** Toggle one target's pinned state (the submenu row's pushpin). */
|
|
29
29
|
onToggleOpenWithPin?: (targetId: string) => void;
|
|
30
|
-
/** Insert `@<relative path>` into the composer draft. */
|
|
31
|
-
onReferenceFile: (path: string) => void;
|
|
30
|
+
/** Insert `@<relative path>` into the composer draft (file vs directory). */
|
|
31
|
+
onReferenceFile: (path: string, isDir: boolean) => void;
|
|
32
32
|
/** Bump to wipe the level cache and reload the visible set. */
|
|
33
33
|
refreshTick: number;
|
|
34
34
|
/** Upload into `dir` (absolute, inside the workspace); runs in the caller. */
|
|
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;
|
|
@@ -17,7 +17,10 @@ export declare function TreePanel(props: {
|
|
|
17
17
|
openWithSsh?: boolean;
|
|
18
18
|
onOpenWith?: (targetId: string, path: string) => void;
|
|
19
19
|
onToggleOpenWithPin?: (targetId: string) => void;
|
|
20
|
-
onReferenceFile: (path: string) => void;
|
|
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). */
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
* client.js); an edit that does land while a core HMR happens is caught by
|
|
49
49
|
* the ETag comparison on the next activation.
|
|
50
50
|
*/
|
|
51
|
-
export type ChunkName = 'terminal' | 'editor';
|
|
51
|
+
export type ChunkName = 'terminal' | 'editor' | 'locale';
|
|
52
52
|
/** The module exports a chunk factory provides (namespace-ish record). */
|
|
53
53
|
export type ChunkExports = Record<string, unknown>;
|
|
54
54
|
/**
|
|
@@ -1,14 +1,95 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Insert text into the current session's composer draft through the
|
|
3
3
|
* conversation service — the shared path behind the explorer's @-reference
|
|
4
4
|
* button and the viewer selection popup. The service is resolved lazily
|
|
5
5
|
* through `ctx.get` (the inject-free read the app's own plugins use); a
|
|
6
6
|
* missing service or scope degrades to a logged no-op, never a crash.
|
|
7
|
+
*
|
|
8
|
+
* File references additionally use DSH's own structured insert event
|
|
9
|
+
* (`slash/input-insert-reference`, see `insertFileReference`) instead of
|
|
10
|
+
* plain draft text: the native `@file` picker emits this event, and the
|
|
11
|
+
* conversation input machine mints one occurrence whose chip covers the
|
|
12
|
+
* whole reference. Plain text `@folder/file.ts` only ever gets DSH's
|
|
13
|
+
* folder-ref decoration (`@folder/`) — the file name stays undecorated — so
|
|
14
|
+
* plain append is the fallback, not the primary path, for files.
|
|
15
|
+
*
|
|
16
|
+
* Insert position (upstream issue #425): the draft store only exposes the
|
|
17
|
+
* whole string (`getSnapshot().draft` + `setDraft(text)`) — there is no
|
|
18
|
+
* caret API on the conversation service. The composer's `<textarea>` keeps
|
|
19
|
+
* its last selection even while unfocused, so the live caret is probed from
|
|
20
|
+
* the DOM (guarded by a value-sync check), and the text is spliced at that
|
|
21
|
+
* position, replacing any live selection — with whitespace-aware joins, an
|
|
22
|
+
* insert into the middle of a sentence keeps single-space separation like
|
|
23
|
+
* the append path. An unknown/stale caret falls back to appending at the
|
|
24
|
+
* end (the pre-fix behavior).
|
|
25
|
+
*
|
|
26
|
+
* The caret is also *restored* after the insert: committing a programmatic
|
|
27
|
+
* draft change resets the controlled textarea's caret (observed landing at
|
|
28
|
+
* the start of the value), which would make every later insert probe the
|
|
29
|
+
* reset position and drift the stack (A|B + C + D ended up as |DACB). The
|
|
30
|
+
* placement (`placeComposerCaretAfterInsert`) puts the caret right after the
|
|
31
|
+
* inserted text once the value commit lands — the index accounts for the
|
|
32
|
+
* separating space on the left, so stacked inserts stay at their running
|
|
33
|
+
* position (A|B + C + D → ACD|B).
|
|
7
34
|
*/
|
|
8
35
|
import type { Context } from '../context-types.ts';
|
|
36
|
+
/** A resolved composer caret/selection in draft coordinates. */
|
|
37
|
+
export interface DraftCaret {
|
|
38
|
+
start: number;
|
|
39
|
+
end: number;
|
|
40
|
+
}
|
|
9
41
|
/**
|
|
10
|
-
*
|
|
11
|
-
|
|
12
|
-
|
|
42
|
+
* The spliced draft string (see {@link spliceInsert}); pure string math.
|
|
43
|
+
*/
|
|
44
|
+
export declare function insertAtCaret(draft: string, text: string, caret: DraftCaret | null): string;
|
|
45
|
+
/**
|
|
46
|
+
* Resolve the composer's live caret from its DOM `<textarea>`. The draft
|
|
47
|
+
* store has no caret API, so the sidebar reads the composed input's selection
|
|
48
|
+
* directly; the value-sync check (`el.value === draft`) discards stale or
|
|
49
|
+
* wrong-composer reads — a caret must never be applied against a draft it
|
|
50
|
+
* was not measured on.
|
|
51
|
+
*
|
|
52
|
+
* Returns null when the composer is missing, disabled/read-only, out of
|
|
53
|
+
* sync with the store draft, or has no measurable selection (odd hosts
|
|
54
|
+
* report null selectionStart/End).
|
|
55
|
+
*/
|
|
56
|
+
export declare function probeComposerCaret(draft: string): DraftCaret | null;
|
|
57
|
+
/**
|
|
58
|
+
* Restore the composer caret to `caretIndex` after a programmatic
|
|
59
|
+
* `setDraft` commit. A controlled textarea update resets the caret (React
|
|
60
|
+
* commits the value asynchronously and the browser moves the caret to the
|
|
61
|
+
* start/end), so the placement is scheduled and retried across at most two
|
|
62
|
+
* animation frames (setTimeout fallback), and only applied when the textarea
|
|
63
|
+
* still matches `expectedDraft` — a newer edit or a different composer wins
|
|
64
|
+
* the race untouched. The caret is clamped into the value bounds, mirroring
|
|
65
|
+
* how browsers clamp type-in positions.
|
|
66
|
+
*/
|
|
67
|
+
export declare function placeComposerCaretAfterInsert(expectedDraft: string, caretIndex: number): void;
|
|
68
|
+
/**
|
|
69
|
+
* Insert `text` into the session's composer draft at the composer's live
|
|
70
|
+
* caret (see {@link probeComposerCaret}), falling back to appending at the
|
|
71
|
+
* end when the caret cannot be resolved. Returns false — and logs — when the
|
|
72
|
+
* conversation service or the session scope is unavailable.
|
|
13
73
|
*/
|
|
14
74
|
export declare function appendToDraft(ctx: Context, sessionId: string, text: string): boolean;
|
|
75
|
+
/**
|
|
76
|
+
* The DSH `@file` spelling for one relative path, mirroring the host grammar
|
|
77
|
+
* (`formatFileMention` in `@deepseek-ai/dsh-file-reference`): plain when
|
|
78
|
+
* there is no whitespace, quoted when there is, and `undefined` when the
|
|
79
|
+
* path contains a control character or an embedded quote the editor grammar
|
|
80
|
+
* cannot represent.
|
|
81
|
+
*/
|
|
82
|
+
export declare function fileMention(relativePath: string): {
|
|
83
|
+
mention: string;
|
|
84
|
+
label: string;
|
|
85
|
+
} | undefined;
|
|
86
|
+
/**
|
|
87
|
+
* Insert one FILE reference as a structured chip (like DSH's own `@` picker).
|
|
88
|
+
* The chip displays `@<basename>` but serializes to `@<relative path>` on
|
|
89
|
+
* send, so the reference stays a single link from trigger to basename.
|
|
90
|
+
*
|
|
91
|
+
* Directories are NOT handled here: DSH's folder grammar wants the trailing
|
|
92
|
+
* slash as plain text (`@dir/`) so completion can descend, which
|
|
93
|
+
* `appendToDraft` already covers.
|
|
94
|
+
*/
|
|
95
|
+
export declare function insertFileReference(ctx: Context, sessionId: string, relativePath: string): boolean;
|
|
@@ -94,6 +94,7 @@ export declare const zh: {
|
|
|
94
94
|
terminalDepsFailed: string;
|
|
95
95
|
terminalDepsHint: string;
|
|
96
96
|
terminalDepsProfile: string;
|
|
97
|
+
terminalShellNotFound: string;
|
|
97
98
|
refresh: string;
|
|
98
99
|
refreshUnsavedConfirm: string;
|
|
99
100
|
save: string;
|
|
@@ -158,6 +159,18 @@ export declare const zh: {
|
|
|
158
159
|
producedOpen: string;
|
|
159
160
|
showInFolder: string;
|
|
160
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;
|
|
161
174
|
exited: string;
|
|
162
175
|
noSession: string;
|
|
163
176
|
pluginNotLoaded: string;
|
|
@@ -362,26 +375,6 @@ export declare const en: Record<keyof typeof zh, string>;
|
|
|
362
375
|
* (`'sidebar'` is taken by DSH's own ui-sidebar, hence this distinct name).
|
|
363
376
|
*/
|
|
364
377
|
export declare const LOCALE_NS = "betterSidebar";
|
|
365
|
-
/** The ja dictionary (key-set-equal to zh, enforced by the type annotation). */
|
|
366
|
-
export declare const ja: Record<keyof typeof zh, string>;
|
|
367
|
-
export declare const de: Record<keyof typeof zh, string>;
|
|
368
|
-
export declare const fr: Record<keyof typeof zh, string>;
|
|
369
|
-
export declare const pt: Record<keyof typeof zh, string>;
|
|
370
|
-
export declare const ko: Record<keyof typeof zh, string>;
|
|
371
|
-
export declare const ar: Record<keyof typeof zh, string>;
|
|
372
|
-
export declare const hi: Record<keyof typeof zh, string>;
|
|
373
|
-
export declare const id: Record<keyof typeof zh, string>;
|
|
374
|
-
export declare const tr: Record<keyof typeof zh, string>;
|
|
375
|
-
export declare const vi: Record<keyof typeof zh, string>;
|
|
376
|
-
export declare const th: Record<keyof typeof zh, string>;
|
|
377
|
-
export declare const ru: Record<keyof typeof zh, string>;
|
|
378
|
-
export declare const it: Record<keyof typeof zh, string>;
|
|
379
|
-
export declare const nl: Record<keyof typeof zh, string>;
|
|
380
|
-
export declare const sv: Record<keyof typeof zh, string>;
|
|
381
|
-
export declare const pl: Record<keyof typeof zh, string>;
|
|
382
|
-
export declare const zhHK: Record<keyof typeof zh, string>;
|
|
383
|
-
export declare const zhTW: Record<keyof typeof zh, string>;
|
|
384
|
-
export declare const zhMO: Record<keyof typeof zh, string>;
|
|
385
378
|
/**
|
|
386
379
|
* The better-locale override store attached by the client apply
|
|
387
380
|
* (absent → no override; the zh/en chain runs). The store's `active`
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The floating "add selection to conversation" popup shared by the text
|
|
3
|
+
* viewer: a viewport-anchored button portaled to `document.body`, kept alive
|
|
4
|
+
* across the selection gesture and committed on click.
|
|
5
|
+
*
|
|
6
|
+
* Dismissal contract (upstream issue #425): the popup must never outlive its
|
|
7
|
+
* editor surface. The sidebar keeps every tab MOUNTED — switching tabs only
|
|
8
|
+
* flips the pane cell to `display:none` and collapsing the panel translates
|
|
9
|
+
* it off-screen — while the portaled `position:fixed` button stays pinned to
|
|
10
|
+
* its viewport anchor, ignoring both. The caller already hides on surface
|
|
11
|
+
* scrolls, selection collapse and content swaps; this hook adds the global
|
|
12
|
+
* dismissal that covers everything else:
|
|
13
|
+
*
|
|
14
|
+
* - any `mousedown` outside the button (tab bar, composer, another pane,
|
|
15
|
+
* the collapse toggle, …) closes it;
|
|
16
|
+
* - `Escape` closes it;
|
|
17
|
+
* - the document going hidden (`visibilitychange`) or the window losing
|
|
18
|
+
* focus closes it;
|
|
19
|
+
* - an `IntersectionObserver` on the editor surface closes it as soon as the
|
|
20
|
+
* surface leaves the viewport — the tab-switch (`display:none`) and
|
|
21
|
+
* panel-collapse (translated off-screen) paths have no DOM events of their
|
|
22
|
+
* own, so the geometry signal is the only reliable one.
|
|
23
|
+
*
|
|
24
|
+
* The button's own `mousedown` is never treated as an outside click: the
|
|
25
|
+
* caller preventDefaults it to keep the selection/caret alive until the
|
|
26
|
+
* click commits (the hook's capture-phase listener runs first and must not
|
|
27
|
+
* hide for it).
|
|
28
|
+
*/
|
|
29
|
+
import { type RefObject } from 'react';
|
|
30
|
+
/** The floating "add to conversation" action: payload + viewport anchor. */
|
|
31
|
+
export interface SelectionPopup {
|
|
32
|
+
insert: string;
|
|
33
|
+
left: number;
|
|
34
|
+
top: number;
|
|
35
|
+
}
|
|
36
|
+
export interface SelectionPopupOptions {
|
|
37
|
+
/** Commit the payload into the composer draft (button click). */
|
|
38
|
+
onCommit(insert: string): void;
|
|
39
|
+
/**
|
|
40
|
+
* The DOM surface that must stay on screen for the popup to live: the
|
|
41
|
+
* CodeMirror host element. Called lazily (refs are null until the content
|
|
42
|
+
* loads).
|
|
43
|
+
*/
|
|
44
|
+
getSurface(): HTMLElement | null;
|
|
45
|
+
}
|
|
46
|
+
export interface SelectionPopupControls {
|
|
47
|
+
/** The current popup (null = hidden). */
|
|
48
|
+
popup: SelectionPopup | null;
|
|
49
|
+
/** Attach to the portaled button element. */
|
|
50
|
+
buttonRef: RefObject<HTMLButtonElement>;
|
|
51
|
+
/** Anchor the popup above a selection (viewport-clamped). */
|
|
52
|
+
show(insert: string, left: number, top: number): void;
|
|
53
|
+
/** Hide the popup (idempotent). */
|
|
54
|
+
hide(): void;
|
|
55
|
+
/** The button's click: commit the stored payload, then hide. */
|
|
56
|
+
commit(): void;
|
|
57
|
+
}
|
|
58
|
+
export declare function useSelectionPopup(options: SelectionPopupOptions): SelectionPopupControls;
|
|
@@ -132,7 +132,7 @@ export interface TabComponentProps {
|
|
|
132
132
|
/** The explorer's reveal-highlight set (ExplorerView; "Show in folder" targets). */
|
|
133
133
|
revealed?: string[];
|
|
134
134
|
onToggleDir?: (path: string) => void;
|
|
135
|
-
onReferenceFile?: (path: string) => void;
|
|
135
|
+
onReferenceFile?: (path: string, isDir: boolean) => void;
|
|
136
136
|
onOpenFile?: (path: string) => void;
|
|
137
137
|
onOpenDiff?: (tab: SidebarTab) => void;
|
|
138
138
|
onSubagentJump?: (childSessionId: string) => void;
|
|
@@ -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 {
|
|
@@ -60,12 +60,35 @@ export declare const ICON_FONT_FALLBACKS: readonly string[];
|
|
|
60
60
|
* @returns the stack with icon fallbacks merged in.
|
|
61
61
|
*/
|
|
62
62
|
export declare function withIconFontFallbacks(stack: string): string;
|
|
63
|
+
/**
|
|
64
|
+
* Guarantee the stack ends in a generic family, so a font the browser cannot
|
|
65
|
+
* resolve degrades to a monospace one.
|
|
66
|
+
*
|
|
67
|
+
* Without this a stack of unresolvable names (a misspelled family, or one only
|
|
68
|
+
* installed on the machine running the shell rather than the one running the
|
|
69
|
+
* browser) falls through to the browser's *standard* font, which is
|
|
70
|
+
* proportional — xterm then measures its cell from a proportional advance and
|
|
71
|
+
* the whole grid breaks, rather than merely losing the requested typeface.
|
|
72
|
+
*
|
|
73
|
+
* This does not sniff whether the named families exist (an explicit non-goal
|
|
74
|
+
* of the terminal font design): it only terminates the stack. A stack that
|
|
75
|
+
* already names a generic family is returned untouched, so a user who
|
|
76
|
+
* deliberately wrote one keeps it. Generics are matched unquoted only — a
|
|
77
|
+
* quoted `"monospace"` is a family *name*, not the keyword.
|
|
78
|
+
*
|
|
79
|
+
* @param stack - the resolved font-family value.
|
|
80
|
+
* @returns the stack, ending in a generic family.
|
|
81
|
+
*/
|
|
82
|
+
export declare function withMonospaceFallback(stack: string): string;
|
|
63
83
|
/**
|
|
64
84
|
* Resolve the xterm font options for the given prefs.
|
|
65
85
|
*
|
|
66
86
|
* The base family keeps its existing precedence — user pref > theme code
|
|
67
|
-
* font > built-in stack
|
|
68
|
-
*
|
|
87
|
+
* font > built-in stack. {@link withMonospaceFallback} then terminates the
|
|
88
|
+
* stack with a generic family so an unresolvable name degrades to a
|
|
89
|
+
* monospace instead of the proportional standard font, and finally
|
|
90
|
+
* {@link withIconFontFallbacks} tops it up so prompt icons resolve
|
|
91
|
+
* regardless of which base won.
|
|
69
92
|
*
|
|
70
93
|
* @param prefs - the current side card preferences.
|
|
71
94
|
* @param themeFontFamily - the app's theme code font (`--ds-font-family-code`
|
|
@@ -430,10 +430,12 @@ export interface SidebarLocaleService {
|
|
|
430
430
|
}
|
|
431
431
|
/** The composer draft face the sidebar reaches through `ctx.conversation.input`. */
|
|
432
432
|
export interface SidebarSessionInput {
|
|
433
|
-
/** The live input store (draft read for append).
|
|
433
|
+
/** The live input store (draft read for append). `draftRev` is the machine's
|
|
434
|
+
* span-CAS revision — required to mint a structured file-reference chip. */
|
|
434
435
|
state: {
|
|
435
436
|
getSnapshot(): {
|
|
436
437
|
draft: string;
|
|
438
|
+
draftRev?: number;
|
|
437
439
|
};
|
|
438
440
|
};
|
|
439
441
|
/** Replace the draft text (the input machine's single public write path). */
|
|
@@ -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
|
+
}>;
|