@aiwayds/dsh-tui-pi 0.28.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/README.zh-CN.md +3 -0
- package/lib/ask-user.d.ts +72 -0
- package/lib/ask-user.js +212 -0
- package/lib/ask-user.js.map +1 -1
- package/lib/clipboard.d.ts +114 -0
- package/lib/clipboard.js +236 -0
- package/lib/clipboard.js.map +1 -0
- package/lib/index.js +139 -41
- package/lib/index.js.map +1 -1
- package/lib/log-repair.d.ts +161 -0
- package/lib/log-repair.js +273 -0
- package/lib/log-repair.js.map +1 -0
- package/lib/remote-tail.d.ts +49 -0
- package/lib/remote-tail.js +114 -0
- package/lib/remote-tail.js.map +1 -0
- package/lib/repair-dialog.d.ts +69 -0
- package/lib/repair-dialog.js +153 -0
- package/lib/repair-dialog.js.map +1 -0
- package/lib/session.d.ts +96 -1
- package/lib/session.js +346 -39
- package/lib/session.js.map +1 -1
- package/lib/sessions.d.ts +21 -0
- package/lib/sessions.js +30 -8
- package/lib/sessions.js.map +1 -1
- package/lib/tui.js +17 -1
- package/lib/tui.js.map +1 -1
- package/lib/writer-lock.d.ts +70 -0
- package/lib/writer-lock.js +206 -0
- package/lib/writer-lock.js.map +1 -0
- package/package.json +1 -1
- package/scripts/repair-session-log.mjs +246 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clipboard helper for the ask-user "Type something." sentinel editor.
|
|
3
|
+
*
|
|
4
|
+
* The editor only ever needs to push free-text into the system clipboard
|
|
5
|
+
* (Ctrl+Shift+C) and read it back (right-click → paste). Terminal-side
|
|
6
|
+
* paste is already handled by the bracketed-paste path in `handleCustomInput`
|
|
7
|
+
* (see ask-user.ts); this module is the bridge to a real OS clipboard.
|
|
8
|
+
*
|
|
9
|
+
* Strategy:
|
|
10
|
+
* - `writeClipboard` writes the OSC 52 "set primary clipboard" sequence so a
|
|
11
|
+
* host terminal that follows the convention picks the text up without
|
|
12
|
+
* touching the OS. The sequence is capped at 64 KiB to keep the terminal
|
|
13
|
+
* from being spammed with a multi-MB escape run for a pathological paste;
|
|
14
|
+
* a >64 KiB OSC 52 is skipped and the local command alone is relied on.
|
|
15
|
+
* In parallel, the best-matching local command for the platform is tried
|
|
16
|
+
* (`pbcopy` / `wl-copy` / `xclip` / `xsel` / `clip`). Whichever path lands
|
|
17
|
+
* first resolves `true`; the local failure is swallowed (the OSC 52 path
|
|
18
|
+
* may still succeed, the next call may not, we never want a thrown error
|
|
19
|
+
* to interrupt the user's keystroke).
|
|
20
|
+
* - `readClipboard` is a fall-back ladder. WAYLAND-aware first
|
|
21
|
+
* (`wl-paste` when `WAYLAND_DISPLAY` is set), then the X11 ladder
|
|
22
|
+
* (`xclip` → `xsel`), then macOS `pbpaste`, then Windows PowerShell.
|
|
23
|
+
* The same command is not retried within a single process: when a binary
|
|
24
|
+
* is missing (`ENOENT`) the platform is cached as "unavailable" and the
|
|
25
|
+
* read is short-circuited to `null` for the remainder of the run.
|
|
26
|
+
*
|
|
27
|
+
* Every command runs through `execFile` (no shell, parameter-array argv,
|
|
28
|
+
* bounded timeout, large-enough buffer). The text path itself is raw —
|
|
29
|
+
* the ask-user side (which is the only in-tree caller) sanitizes the
|
|
30
|
+
* payload through `sanitizePastedText` before it lands in the editor
|
|
31
|
+
* buffer. Splitting the sanitize to the call site means a future caller
|
|
32
|
+
* of `readClipboard` (e.g. a feature that streams the clipboard into
|
|
33
|
+
* the chat composer) gets a faithful copy of what the OS gave us, and
|
|
34
|
+
* the editor gets its one sanitization pass.
|
|
35
|
+
*
|
|
36
|
+
* Everything is injectable through `ClipboardImpl` so the module is fully
|
|
37
|
+
* unit-testable without spawning real clipboard processes.
|
|
38
|
+
*/
|
|
39
|
+
/**
|
|
40
|
+
* execFile envelope. The result is `{ stdout }` on success; for write
|
|
41
|
+
* commands the caller passes a non-null `stdinPayload` that is piped in
|
|
42
|
+
* before the process exits. A `null` payload skips the stdin pipe (read
|
|
43
|
+
* commands).
|
|
44
|
+
*/
|
|
45
|
+
export type ExecFileFn = (file: string, args: readonly string[], options: {
|
|
46
|
+
timeout: number;
|
|
47
|
+
maxBuffer: number;
|
|
48
|
+
windowsHide: boolean;
|
|
49
|
+
}, stdinPayload: string | null) => Promise<{
|
|
50
|
+
stdout: string;
|
|
51
|
+
}>;
|
|
52
|
+
/**
|
|
53
|
+
* Subset of the platform the resolver reads. Pulled out so tests can fake
|
|
54
|
+
* darwin/linux/win32 without touching `process.platform` / `process.env`.
|
|
55
|
+
*/
|
|
56
|
+
export interface ClipboardEnv {
|
|
57
|
+
platform: NodeJS.Platform;
|
|
58
|
+
waylandDisplay: string | undefined;
|
|
59
|
+
display: string | undefined;
|
|
60
|
+
}
|
|
61
|
+
/** All I/O the helper needs: command execution + stdout writing. */
|
|
62
|
+
export interface ClipboardImpl {
|
|
63
|
+
execFile: ExecFileFn;
|
|
64
|
+
write: (chunk: string) => void;
|
|
65
|
+
env: ClipboardEnv;
|
|
66
|
+
}
|
|
67
|
+
/** Default impl: real process, real env. */
|
|
68
|
+
export declare const defaultImpl: ClipboardImpl;
|
|
69
|
+
/**
|
|
70
|
+
* Pick the write ladder for a platform. The ladder is tried SERIALLY in
|
|
71
|
+
* order — the helper short-circuits on the first rung that exits cleanly
|
|
72
|
+
* and treats a missing binary (`ENOENT`) as "try the next rung". This
|
|
73
|
+
* mirrors the read-side ladder and avoids two problems the old parallel
|
|
74
|
+
* shape had on a real Wayland + XWayland desktop:
|
|
75
|
+
* 1. `wl-copy` (Wayland) and `xclip` (X11) write the same `CLIPBOARD`
|
|
76
|
+
* selection through different protocols; spawning them concurrently
|
|
77
|
+
* races the two ends and the loser's payload can win.
|
|
78
|
+
* 2. `wl-copy` forks a background helper that lingers after the
|
|
79
|
+
* `execFile` resolves; running it in parallel with every other
|
|
80
|
+
* rung accumulated helpers with each Ctrl+Shift+C.
|
|
81
|
+
*/
|
|
82
|
+
export declare function writeCommandsForPlatform(env: ClipboardEnv): readonly {
|
|
83
|
+
cmd: string;
|
|
84
|
+
args: readonly string[];
|
|
85
|
+
}[];
|
|
86
|
+
/** Read-ladder resolver: returns the commands to try in order. */
|
|
87
|
+
export declare function readCommandsForPlatform(env: ClipboardEnv): readonly {
|
|
88
|
+
cmd: string;
|
|
89
|
+
args: readonly string[];
|
|
90
|
+
}[];
|
|
91
|
+
/**
|
|
92
|
+
* Copy `text` to the system clipboard.
|
|
93
|
+
*
|
|
94
|
+
* Returns `true` when either the OSC 52 path succeeded AND we wrote a small
|
|
95
|
+
* enough buffer, OR any local command exited cleanly. Returns `false`
|
|
96
|
+
* when every path failed. Errors are swallowed; the helper must never
|
|
97
|
+
* throw from a key handler.
|
|
98
|
+
*/
|
|
99
|
+
export declare function writeClipboard(text: string, impl?: ClipboardImpl): Promise<boolean>;
|
|
100
|
+
/**
|
|
101
|
+
* Read the system clipboard and return its text. The payload is the raw
|
|
102
|
+
* stdout of whichever rung landed first — the ask-user side sanitizes
|
|
103
|
+
* it through `sanitizePastedText` before the editor buffer sees it, so
|
|
104
|
+
* a future caller of this helper gets a faithful copy of what the OS
|
|
105
|
+
* gave us. Returns `null` when no read path succeeds or the platform
|
|
106
|
+
* has been cached as "unavailable" (no command found on the ladder).
|
|
107
|
+
*/
|
|
108
|
+
export declare function readClipboard(impl?: ClipboardImpl): Promise<string | null>;
|
|
109
|
+
/**
|
|
110
|
+
* Test seam: drop the cached "platform has no read command" state. Tests
|
|
111
|
+
* inject a different `ClipboardImpl` per case, so the cache can be left
|
|
112
|
+
* full from a previous run; the seam keeps the test order independent.
|
|
113
|
+
*/
|
|
114
|
+
export declare function __resetClipboardUnavailableForTest(): void;
|
package/lib/clipboard.js
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clipboard helper for the ask-user "Type something." sentinel editor.
|
|
3
|
+
*
|
|
4
|
+
* The editor only ever needs to push free-text into the system clipboard
|
|
5
|
+
* (Ctrl+Shift+C) and read it back (right-click → paste). Terminal-side
|
|
6
|
+
* paste is already handled by the bracketed-paste path in `handleCustomInput`
|
|
7
|
+
* (see ask-user.ts); this module is the bridge to a real OS clipboard.
|
|
8
|
+
*
|
|
9
|
+
* Strategy:
|
|
10
|
+
* - `writeClipboard` writes the OSC 52 "set primary clipboard" sequence so a
|
|
11
|
+
* host terminal that follows the convention picks the text up without
|
|
12
|
+
* touching the OS. The sequence is capped at 64 KiB to keep the terminal
|
|
13
|
+
* from being spammed with a multi-MB escape run for a pathological paste;
|
|
14
|
+
* a >64 KiB OSC 52 is skipped and the local command alone is relied on.
|
|
15
|
+
* In parallel, the best-matching local command for the platform is tried
|
|
16
|
+
* (`pbcopy` / `wl-copy` / `xclip` / `xsel` / `clip`). Whichever path lands
|
|
17
|
+
* first resolves `true`; the local failure is swallowed (the OSC 52 path
|
|
18
|
+
* may still succeed, the next call may not, we never want a thrown error
|
|
19
|
+
* to interrupt the user's keystroke).
|
|
20
|
+
* - `readClipboard` is a fall-back ladder. WAYLAND-aware first
|
|
21
|
+
* (`wl-paste` when `WAYLAND_DISPLAY` is set), then the X11 ladder
|
|
22
|
+
* (`xclip` → `xsel`), then macOS `pbpaste`, then Windows PowerShell.
|
|
23
|
+
* The same command is not retried within a single process: when a binary
|
|
24
|
+
* is missing (`ENOENT`) the platform is cached as "unavailable" and the
|
|
25
|
+
* read is short-circuited to `null` for the remainder of the run.
|
|
26
|
+
*
|
|
27
|
+
* Every command runs through `execFile` (no shell, parameter-array argv,
|
|
28
|
+
* bounded timeout, large-enough buffer). The text path itself is raw —
|
|
29
|
+
* the ask-user side (which is the only in-tree caller) sanitizes the
|
|
30
|
+
* payload through `sanitizePastedText` before it lands in the editor
|
|
31
|
+
* buffer. Splitting the sanitize to the call site means a future caller
|
|
32
|
+
* of `readClipboard` (e.g. a feature that streams the clipboard into
|
|
33
|
+
* the chat composer) gets a faithful copy of what the OS gave us, and
|
|
34
|
+
* the editor gets its one sanitization pass.
|
|
35
|
+
*
|
|
36
|
+
* Everything is injectable through `ClipboardImpl` so the module is fully
|
|
37
|
+
* unit-testable without spawning real clipboard processes.
|
|
38
|
+
*/
|
|
39
|
+
import { execFile as execFileCb } from 'node:child_process';
|
|
40
|
+
import { Buffer } from 'node:buffer';
|
|
41
|
+
/** Max UTF-8 byte length we accept into the OSC 52 write sequence. */
|
|
42
|
+
const OSC52_MAX_BYTES = 65536;
|
|
43
|
+
/**
|
|
44
|
+
* Default `execFile` wrapper built on the real `node:child_process`. The
|
|
45
|
+
* callback form gives us stdin for write commands (pbcopy / wl-copy / xclip
|
|
46
|
+
* / xsel / clip all consume the text on stdin); a `null` payload skips the
|
|
47
|
+
* stdin pipe entirely (read commands) and returns the collected stdout.
|
|
48
|
+
*/
|
|
49
|
+
function makeDefaultExecFile() {
|
|
50
|
+
return (file, args, options, stdinPayload) => new Promise((resolve, reject) => {
|
|
51
|
+
const child = execFileCb(file, [...args], options, (error, stdout) => {
|
|
52
|
+
if (error === null)
|
|
53
|
+
resolve({ stdout });
|
|
54
|
+
else
|
|
55
|
+
reject(error);
|
|
56
|
+
});
|
|
57
|
+
if (stdinPayload !== null) {
|
|
58
|
+
// A child that exists but exits before draining stdin (e.g. xclip
|
|
59
|
+
// printing "Can't open display" on Linux without an X server) raises
|
|
60
|
+
// an uncaught EPIPE on the stream; swallow it here so the helper
|
|
61
|
+
// keeps its best-effort "never throw" contract.
|
|
62
|
+
child.stdin?.on('error', () => { });
|
|
63
|
+
child.stdin?.end(stdinPayload);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/** Default impl: real process, real env. */
|
|
68
|
+
export const defaultImpl = {
|
|
69
|
+
execFile: makeDefaultExecFile(),
|
|
70
|
+
write: chunk => { process.stdout.write(chunk); },
|
|
71
|
+
env: {
|
|
72
|
+
platform: process.platform,
|
|
73
|
+
waylandDisplay: process.env.WAYLAND_DISPLAY,
|
|
74
|
+
display: process.env.DISPLAY,
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Cache of platforms whose read-binary ladder is fully missing: we still
|
|
79
|
+
* retry the write path every call (a copy without a way to read it back is
|
|
80
|
+
* still useful), but a read that would just re-spawn a missing ENOENT
|
|
81
|
+
* short-circuits to `null` so the right-click path stays snappy.
|
|
82
|
+
*/
|
|
83
|
+
const readUnavailable = new Set();
|
|
84
|
+
/**
|
|
85
|
+
* Pick the write ladder for a platform. The ladder is tried SERIALLY in
|
|
86
|
+
* order — the helper short-circuits on the first rung that exits cleanly
|
|
87
|
+
* and treats a missing binary (`ENOENT`) as "try the next rung". This
|
|
88
|
+
* mirrors the read-side ladder and avoids two problems the old parallel
|
|
89
|
+
* shape had on a real Wayland + XWayland desktop:
|
|
90
|
+
* 1. `wl-copy` (Wayland) and `xclip` (X11) write the same `CLIPBOARD`
|
|
91
|
+
* selection through different protocols; spawning them concurrently
|
|
92
|
+
* races the two ends and the loser's payload can win.
|
|
93
|
+
* 2. `wl-copy` forks a background helper that lingers after the
|
|
94
|
+
* `execFile` resolves; running it in parallel with every other
|
|
95
|
+
* rung accumulated helpers with each Ctrl+Shift+C.
|
|
96
|
+
*/
|
|
97
|
+
export function writeCommandsForPlatform(env) {
|
|
98
|
+
if (env.platform === 'darwin') {
|
|
99
|
+
return [{ cmd: 'pbcopy', args: [] }];
|
|
100
|
+
}
|
|
101
|
+
if (env.platform === 'win32') {
|
|
102
|
+
return [{ cmd: 'clip', args: [] }];
|
|
103
|
+
}
|
|
104
|
+
// Linux and friends: ONE rung per session, picked from the env. WAYLAND
|
|
105
|
+
// wins over X11 outright (a Wayland session that also exposes $DISPLAY
|
|
106
|
+
// for XWayland apps still has its native clipboard on `wl-copy`; the
|
|
107
|
+
// X11 rung would be the XWayland clipboard, a different selection).
|
|
108
|
+
// X11 falls back from xclip to xsel when xclip is not installed.
|
|
109
|
+
if (env.waylandDisplay !== undefined && env.waylandDisplay !== '') {
|
|
110
|
+
return [{ cmd: 'wl-copy', args: [] }];
|
|
111
|
+
}
|
|
112
|
+
if (env.display !== undefined && env.display !== '') {
|
|
113
|
+
return [
|
|
114
|
+
{ cmd: 'xclip', args: ['-selection', 'clipboard'] },
|
|
115
|
+
{ cmd: 'xsel', args: ['--clipboard', '--input'] },
|
|
116
|
+
];
|
|
117
|
+
}
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
/** Read-ladder resolver: returns the commands to try in order. */
|
|
121
|
+
export function readCommandsForPlatform(env) {
|
|
122
|
+
if (env.platform === 'darwin') {
|
|
123
|
+
return [{ cmd: 'pbpaste', args: [] }];
|
|
124
|
+
}
|
|
125
|
+
if (env.platform === 'win32') {
|
|
126
|
+
return [{ cmd: 'powershell.exe', args: ['-NoProfile', '-command', 'Get-Clipboard'] }];
|
|
127
|
+
}
|
|
128
|
+
const ladder = [];
|
|
129
|
+
if (env.waylandDisplay !== undefined && env.waylandDisplay !== '') {
|
|
130
|
+
ladder.push({ cmd: 'wl-paste', args: ['--no-newline'] });
|
|
131
|
+
}
|
|
132
|
+
if (env.display !== undefined && env.display !== '') {
|
|
133
|
+
ladder.push({ cmd: 'xclip', args: ['-o', '-selection', 'clipboard'] });
|
|
134
|
+
}
|
|
135
|
+
ladder.push({ cmd: 'xsel', args: ['--clipboard', '--output'] });
|
|
136
|
+
return ladder;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Copy `text` to the system clipboard.
|
|
140
|
+
*
|
|
141
|
+
* Returns `true` when either the OSC 52 path succeeded AND we wrote a small
|
|
142
|
+
* enough buffer, OR any local command exited cleanly. Returns `false`
|
|
143
|
+
* when every path failed. Errors are swallowed; the helper must never
|
|
144
|
+
* throw from a key handler.
|
|
145
|
+
*/
|
|
146
|
+
export async function writeClipboard(text, impl = defaultImpl) {
|
|
147
|
+
let osc52Ok = false;
|
|
148
|
+
// Cap is on UTF-8 byte length, not on the JS string length (which is
|
|
149
|
+
// a UTF-16 code-unit count): 65 536 CJK characters are ~192 KiB in
|
|
150
|
+
// UTF-8, and the terminal would still be spammed with a multi-MB
|
|
151
|
+
// escape run even though `text.length` looked in-budget. The base64
|
|
152
|
+
// step then doubles that again, so `Buffer.byteLength` is the right
|
|
153
|
+
// pre-encode gate.
|
|
154
|
+
if (Buffer.byteLength(text, 'utf8') <= OSC52_MAX_BYTES) {
|
|
155
|
+
const payload = Buffer.from(text, 'utf8').toString('base64');
|
|
156
|
+
try {
|
|
157
|
+
impl.write(`\x1b]52;c;${payload}\x07`);
|
|
158
|
+
osc52Ok = true;
|
|
159
|
+
}
|
|
160
|
+
catch { /* terminal may have closed — give up on this path */ }
|
|
161
|
+
}
|
|
162
|
+
const ladder = writeCommandsForPlatform(impl.env);
|
|
163
|
+
const localOk = await runWriteLadder(ladder, text, impl);
|
|
164
|
+
return osc52Ok || localOk;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Read the system clipboard and return its text. The payload is the raw
|
|
168
|
+
* stdout of whichever rung landed first — the ask-user side sanitizes
|
|
169
|
+
* it through `sanitizePastedText` before the editor buffer sees it, so
|
|
170
|
+
* a future caller of this helper gets a faithful copy of what the OS
|
|
171
|
+
* gave us. Returns `null` when no read path succeeds or the platform
|
|
172
|
+
* has been cached as "unavailable" (no command found on the ladder).
|
|
173
|
+
*/
|
|
174
|
+
export async function readClipboard(impl = defaultImpl) {
|
|
175
|
+
if (readUnavailable.has(impl.env.platform))
|
|
176
|
+
return null;
|
|
177
|
+
const ladder = readCommandsForPlatform(impl.env);
|
|
178
|
+
for (const { cmd, args } of ladder) {
|
|
179
|
+
try {
|
|
180
|
+
const { stdout } = await impl.execFile(cmd, args, {
|
|
181
|
+
timeout: 5000,
|
|
182
|
+
maxBuffer: 1 << 20,
|
|
183
|
+
windowsHide: true,
|
|
184
|
+
}, null);
|
|
185
|
+
return stdout;
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
const code = error?.code;
|
|
189
|
+
if (code === 'ENOENT') {
|
|
190
|
+
// Binary missing on this ladder rung; try the next.
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
// Any other failure (nonzero exit, timeout, signal) — give up.
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// The whole ladder was missing — cache the platform and bail.
|
|
198
|
+
readUnavailable.add(impl.env.platform);
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Run the write ladder SERIALLY against the same text payload. Resolve
|
|
203
|
+
* `true` as soon as one rung exits cleanly; treat a missing binary
|
|
204
|
+
* (`ENOENT`) as "try the next rung" and any other failure as
|
|
205
|
+
* terminal. The serial order keeps Wayland and X11 from racing on the
|
|
206
|
+
* same `CLIPBOARD` selection, and avoids the wl-copy helper-process
|
|
207
|
+
* leak that parallel `Promise.all` caused.
|
|
208
|
+
*/
|
|
209
|
+
async function runWriteLadder(ladder, text, impl) {
|
|
210
|
+
for (const { cmd, args } of ladder) {
|
|
211
|
+
try {
|
|
212
|
+
await impl.execFile(cmd, args, {
|
|
213
|
+
timeout: 5000,
|
|
214
|
+
maxBuffer: 1 << 20,
|
|
215
|
+
windowsHide: true,
|
|
216
|
+
}, text);
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
const code = error?.code;
|
|
221
|
+
if (code === 'ENOENT')
|
|
222
|
+
continue; // binary missing on this rung; try the next.
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Test seam: drop the cached "platform has no read command" state. Tests
|
|
230
|
+
* inject a different `ClipboardImpl` per case, so the cache can be left
|
|
231
|
+
* full from a previous run; the seam keeps the test order independent.
|
|
232
|
+
*/
|
|
233
|
+
export function __resetClipboardUnavailableForTest() {
|
|
234
|
+
readUnavailable.clear();
|
|
235
|
+
}
|
|
236
|
+
//# sourceMappingURL=clipboard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clipboard.js","sourceRoot":"","sources":["../src/clipboard.ts"],"names":[],"mappings":"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"}
|
package/lib/index.js
CHANGED
|
@@ -35,7 +35,11 @@ import { reloadPlugin } from "./reload.js";
|
|
|
35
35
|
import { runSessionRetentionOnce } from "./retention.js";
|
|
36
36
|
import { setNoticeSink } from "./notice-bridge.js";
|
|
37
37
|
import { collectStartupSummary, formatResumeCommand, parseResumeArg, resolveProfileName, } from "./startup-info.js";
|
|
38
|
-
import { inspectPersistedSession, pickPersistedSession, showSessionInfo } from "./sessions.js";
|
|
38
|
+
import { inspectPersistedSession, pickPersistedSession, sessionLogRoot, showSessionInfo } from "./sessions.js";
|
|
39
|
+
import { isCorruptLogError, locateSessionLog, repairFailureNotice, repairSessionLog, } from "./log-repair.js";
|
|
40
|
+
import { openRepairConfirmDialog } from "./repair-dialog.js";
|
|
41
|
+
import { WriterLockedError } from "./writer-lock.js";
|
|
42
|
+
import { emitNotice } from "./notice-bridge.js";
|
|
39
43
|
import { applySubagentPolicy } from "./subagent-policy.js";
|
|
40
44
|
import { openSubagentViewer } from "./subagent-viewer.js";
|
|
41
45
|
import { commandUsagePath, CommandUsageTracker } from "./usage.js";
|
|
@@ -601,7 +605,26 @@ export function apply(ctx) {
|
|
|
601
605
|
liveWidgets.renderAgents(agents);
|
|
602
606
|
},
|
|
603
607
|
};
|
|
604
|
-
const
|
|
608
|
+
const bridgeCallbacksWithTakeover = {
|
|
609
|
+
...bridgeCallbacks,
|
|
610
|
+
onRemotePromotable: async (idRaw) => {
|
|
611
|
+
// Queued follow-ups won the writer-lock race at an idle boundary:
|
|
612
|
+
// take over EXACTLY like a manual /resume, then flush the queue.
|
|
613
|
+
const resumed = await bridge.resume(SessionId(idRaw));
|
|
614
|
+
refreshPermissionPreset();
|
|
615
|
+
renderer.clear();
|
|
616
|
+
liveWidgets.clear();
|
|
617
|
+
const session = resumed.agent.session;
|
|
618
|
+
const adopted = 'adopted' in resumed && resumed.adopted === true;
|
|
619
|
+
bridge.replay(adopted ? session.events : session.events.filter(event => event.seq < session.firstLiveSeq));
|
|
620
|
+
for (const text of bridge.takePendingRemoteFollowups()) {
|
|
621
|
+
await bridge.prompt(text);
|
|
622
|
+
}
|
|
623
|
+
emitNotice('Write lock acquired — follow-ups sent.');
|
|
624
|
+
ui.requestRender();
|
|
625
|
+
},
|
|
626
|
+
};
|
|
627
|
+
const bridge = new DshSessionBridge(ctx, bridgeCallbacksWithTakeover);
|
|
605
628
|
bridgeRef = bridge;
|
|
606
629
|
// Subagent fine-grained control, all in-process (see subagent-policy.ts):
|
|
607
630
|
// a tools.guard denies spawn-tool calls once `maxAgents` children run
|
|
@@ -963,6 +986,112 @@ export function apply(ctx) {
|
|
|
963
986
|
}
|
|
964
987
|
if (picked.kind === 'cancelled')
|
|
965
988
|
return { kind: 'success', text: 'Resume cancelled.' };
|
|
989
|
+
// Narrowed shared handle for the closures below (const narrowing
|
|
990
|
+
// propagates into async closures; `picked`'s does not).
|
|
991
|
+
const target = picked;
|
|
992
|
+
// The selected row's resume path: swap the live agent for the target
|
|
993
|
+
// and rebuild transcript + stats from the stored events. Shared by the
|
|
994
|
+
// direct hit and the post-repair re-entry so both behave identically
|
|
995
|
+
// (including the WriterLockedError read-only fallback).
|
|
996
|
+
const resumeAndReplay = async () => {
|
|
997
|
+
let resumed;
|
|
998
|
+
try {
|
|
999
|
+
resumed = await bridge.resume(target.id);
|
|
1000
|
+
}
|
|
1001
|
+
catch (error) {
|
|
1002
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1003
|
+
if (error instanceof WriterLockedError) {
|
|
1004
|
+
// Single-writer guard fired: another process drives this session.
|
|
1005
|
+
// Rather than a dead end, degrade to a READ-ONLY view synced from
|
|
1006
|
+
// its persisted log — final replies arrive (poll-delayed, without
|
|
1007
|
+
// streaming detail); input is refused until /resume or /new.
|
|
1008
|
+
try {
|
|
1009
|
+
await bridge.watchRemote(target.id);
|
|
1010
|
+
refreshPermissionPreset();
|
|
1011
|
+
renderer.clear();
|
|
1012
|
+
liveWidgets.clear();
|
|
1013
|
+
ui.requestRender();
|
|
1014
|
+
return {
|
|
1015
|
+
kind: 'success',
|
|
1016
|
+
text: `Watching ${clipToWidth(String(target.id), 8)} (read-only · driven by pid ${error.holder.pid}). /resume or /new to switch.`,
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
catch (watchError) {
|
|
1020
|
+
return {
|
|
1021
|
+
kind: 'error',
|
|
1022
|
+
text: `Cannot watch ${clipToWidth(String(target.id), 8)} read-only: ${watchError instanceof Error ? watchError.message : String(watchError)}`,
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
return {
|
|
1027
|
+
kind: 'error',
|
|
1028
|
+
text: `Resume failed: ${message} — the previous session was closed; the next prompt starts a new one.`,
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
// Seed the badge cache for the resumed session (its pin event may have
|
|
1032
|
+
// been emitted before the bridge's session-id filter re-bound).
|
|
1033
|
+
refreshPermissionPreset();
|
|
1034
|
+
// Clear BEFORE replay: the renderer's local-echo dedupe must not see
|
|
1035
|
+
// replayed user messages next to a stale prompt echo. The live widget
|
|
1036
|
+
// drops the previous session's todos too (its agents already went via
|
|
1037
|
+
// the bridge's onLive([]) on resume reset).
|
|
1038
|
+
renderer.clear();
|
|
1039
|
+
liveWidgets.clear();
|
|
1040
|
+
// Replay seed history only: events at or above firstLiveSeq were
|
|
1041
|
+
// published in-process and arrive again through the session/event
|
|
1042
|
+
// subscription (replaying them would double-count stats and echo).
|
|
1043
|
+
// Seeds entered through construction never published — replaying them
|
|
1044
|
+
// exactly once covers the stored log with zero overlap, zero gap.
|
|
1045
|
+
const session = resumed.agent.session;
|
|
1046
|
+
// Adopted live sessions (attach arm): EVERY event in the log was
|
|
1047
|
+
// published before this surface started tracking it — the firehose
|
|
1048
|
+
// dropped all of them, so replay unfiltered or the transcript misses
|
|
1049
|
+
// everything the other surface did. Cold resumes keep the firstLiveSeq
|
|
1050
|
+
// filter (seeded history replays once; live events re-arrive).
|
|
1051
|
+
const adopted = 'adopted' in resumed && resumed.adopted === true;
|
|
1052
|
+
bridge.replay(adopted ? session.events : session.events.filter(event => event.seq < session.firstLiveSeq));
|
|
1053
|
+
ui.requestRender();
|
|
1054
|
+
return {
|
|
1055
|
+
kind: 'success',
|
|
1056
|
+
text: `Resumed ${clipToWidth(String(target.id), 8)} · ${session.events.length} events.`,
|
|
1057
|
+
};
|
|
1058
|
+
};
|
|
1059
|
+
// Corrupt-log branch: the repair rewrites user data on disk, so it is
|
|
1060
|
+
// gated behind an explicit confirmation dialog — it never runs
|
|
1061
|
+
// silently, and Cancel falls back to the plain failure text.
|
|
1062
|
+
const offerCorruptedLogRepair = async (message) => {
|
|
1063
|
+
// Locate BEFORE asking (feishu-surface order): a log that cannot be
|
|
1064
|
+
// grounded on disk gives the dialog nothing to confirm.
|
|
1065
|
+
const persistence = ctx.get('sessionPersistence');
|
|
1066
|
+
const logPath = await locateSessionLog(persistence, String(target.id), sessionLogRoot());
|
|
1067
|
+
if (logPath === undefined) {
|
|
1068
|
+
return {
|
|
1069
|
+
kind: 'error',
|
|
1070
|
+
text: `repair failed: cannot locate the log of ${clipToWidth(String(target.id), 8)} on disk — log untouched`,
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
if (await openRepairConfirmDialog(ui.tui, ui.theme, refocusEditor) !== 'repair') {
|
|
1074
|
+
return {
|
|
1075
|
+
kind: 'error',
|
|
1076
|
+
text: `Cannot resume ${clipToWidth(String(target.id), 8)}: ${message}`,
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
// repairSessionLog maps its own failures to results; the catch is a
|
|
1080
|
+
// defensive floor so the dispatch never sees an exception.
|
|
1081
|
+
let notice;
|
|
1082
|
+
try {
|
|
1083
|
+
notice = repairFailureNotice(await repairSessionLog(logPath));
|
|
1084
|
+
}
|
|
1085
|
+
catch (error) {
|
|
1086
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1087
|
+
notice = `repair failed: ${detail} — log untouched`;
|
|
1088
|
+
}
|
|
1089
|
+
if (notice !== undefined)
|
|
1090
|
+
return { kind: 'error', text: notice };
|
|
1091
|
+
// A verified-clean log now sits under the canonical name — re-enter
|
|
1092
|
+
// the selected row's resume path.
|
|
1093
|
+
return resumeAndReplay();
|
|
1094
|
+
};
|
|
966
1095
|
// Validate the target log before tearing down the current agent: a
|
|
967
1096
|
// corrupt log must leave the live session untouched.
|
|
968
1097
|
try {
|
|
@@ -970,46 +1099,12 @@ export function apply(ctx) {
|
|
|
970
1099
|
}
|
|
971
1100
|
catch (error) {
|
|
972
1101
|
const message = error instanceof Error ? error.message : String(error);
|
|
1102
|
+
if (isCorruptLogError(message)) {
|
|
1103
|
+
return offerCorruptedLogRepair(message);
|
|
1104
|
+
}
|
|
973
1105
|
return { kind: 'error', text: `Cannot resume ${clipToWidth(String(picked.id), 8)}: ${message}` };
|
|
974
1106
|
}
|
|
975
|
-
|
|
976
|
-
try {
|
|
977
|
-
resumed = await bridge.resume(picked.id);
|
|
978
|
-
}
|
|
979
|
-
catch (error) {
|
|
980
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
981
|
-
return {
|
|
982
|
-
kind: 'error',
|
|
983
|
-
text: `Resume failed: ${message} — the previous session was closed; the next prompt starts a new one.`,
|
|
984
|
-
};
|
|
985
|
-
}
|
|
986
|
-
// Seed the badge cache for the resumed session (its pin event may have
|
|
987
|
-
// been emitted before the bridge's session-id filter re-bound).
|
|
988
|
-
refreshPermissionPreset();
|
|
989
|
-
// Clear BEFORE replay: the renderer's local-echo dedupe must not see
|
|
990
|
-
// replayed user messages next to a stale prompt echo. The live widget
|
|
991
|
-
// drops the previous session's todos too (its agents already went via
|
|
992
|
-
// the bridge's onLive([]) on resume reset).
|
|
993
|
-
renderer.clear();
|
|
994
|
-
liveWidgets.clear();
|
|
995
|
-
// Replay seed history only: events at or above firstLiveSeq were
|
|
996
|
-
// published in-process and arrive again through the session/event
|
|
997
|
-
// subscription (replaying them would double-count stats and echo).
|
|
998
|
-
// Seeds entered through construction never published — replaying them
|
|
999
|
-
// exactly once covers the stored log with zero overlap, zero gap.
|
|
1000
|
-
const session = resumed.agent.session;
|
|
1001
|
-
// Adopted live sessions (attach arm): EVERY event in the log was
|
|
1002
|
-
// published before this surface started tracking it — the firehose
|
|
1003
|
-
// dropped all of them, so replay unfiltered or the transcript misses
|
|
1004
|
-
// everything the other surface did. Cold resumes keep the firstLiveSeq
|
|
1005
|
-
// filter (seeded history replays once; live events re-arrive).
|
|
1006
|
-
const adopted = 'adopted' in resumed && resumed.adopted === true;
|
|
1007
|
-
bridge.replay(adopted ? session.events : session.events.filter(event => event.seq < session.firstLiveSeq));
|
|
1008
|
-
ui.requestRender();
|
|
1009
|
-
return {
|
|
1010
|
-
kind: 'success',
|
|
1011
|
-
text: `Resumed ${clipToWidth(String(picked.id), 8)} · ${session.events.length} events.`,
|
|
1012
|
-
};
|
|
1107
|
+
return resumeAndReplay();
|
|
1013
1108
|
};
|
|
1014
1109
|
commands.registerLocal('resume', resumeHandler);
|
|
1015
1110
|
ctx.effect(() => ctx.commands.register({
|
|
@@ -1527,7 +1622,7 @@ export function apply(ctx) {
|
|
|
1527
1622
|
// runs, the dialog decides steer vs follow-up — Esc cancels and the
|
|
1528
1623
|
// draft goes back into the editor untouched. Idle → direct send (the
|
|
1529
1624
|
// two primitives are equivalent there: both wake a fresh turn).
|
|
1530
|
-
if (decideSubmitPath(bridge.isRunning()) === 'dialog') {
|
|
1625
|
+
if (decideSubmitPath(bridge.isRunning() && !bridge.isReadOnlyView()) === 'dialog') {
|
|
1531
1626
|
const route = await openSubmitRouteDialog(ui.tui, ui.theme, text, refocusEditor);
|
|
1532
1627
|
if (route === undefined) {
|
|
1533
1628
|
// Review S1: restore the RAW submitted text — restoring the trimmed
|
|
@@ -1546,8 +1641,11 @@ export function apply(ctx) {
|
|
|
1546
1641
|
// editor for text that never ran.
|
|
1547
1642
|
liveWidgets.setLastRequest(line);
|
|
1548
1643
|
renderer.renderPromptEcho(line);
|
|
1644
|
+
const wasWatching = bridge.isReadOnlyView();
|
|
1549
1645
|
try {
|
|
1550
1646
|
await bridge.prompt(line);
|
|
1647
|
+
if (wasWatching)
|
|
1648
|
+
emitNotice('Queued follow-up — sends automatically once the write lock frees up.');
|
|
1551
1649
|
}
|
|
1552
1650
|
catch (error) {
|
|
1553
1651
|
// Buffered notice: the failure line is the only on-screen record and
|