agent-sh 0.12.21 → 0.12.23
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/dist/agent/agent-loop.d.ts +1 -0
- package/dist/agent/agent-loop.js +25 -5
- package/dist/agent/conversation-state.d.ts +3 -2
- package/dist/agent/conversation-state.js +27 -14
- package/dist/core.d.ts +3 -1
- package/dist/core.js +20 -24
- package/dist/event-bus.d.ts +9 -1
- package/dist/event-bus.js +9 -0
- package/dist/executor.js +18 -4
- package/dist/extensions/agent-backend.js +49 -3
- package/dist/extensions/slash-commands.js +0 -24
- package/dist/index.js +8 -33
- package/dist/shell/shell.d.ts +26 -7
- package/dist/shell/shell.js +133 -74
- package/dist/utils/floating-panel.d.ts +1 -2
- package/dist/utils/floating-panel.js +35 -36
- package/dist/utils/markdown.js +2 -2
- package/examples/extensions/overlay-agent.ts +51 -43
- package/examples/extensions/pi-bridge/README.md +12 -19
- package/examples/extensions/pi-bridge/index.ts +307 -35
- package/package.json +1 -1
package/dist/shell/shell.d.ts
CHANGED
|
@@ -4,16 +4,25 @@ export interface ShellHandlers {
|
|
|
4
4
|
define: (name: string, fn: (...args: any[]) => any) => void;
|
|
5
5
|
call: (name: string, ...args: any[]) => any;
|
|
6
6
|
}
|
|
7
|
+
/**
|
|
8
|
+
* A claim on the shell's stdout-mute state. Acquire from shell.acquire*,
|
|
9
|
+
* pair with release() in a try/finally. Token-shape forces symmetry —
|
|
10
|
+
* the only way to influence the gate is to hold and release a scope.
|
|
11
|
+
*/
|
|
12
|
+
export interface ShellScope {
|
|
13
|
+
readonly reason: string;
|
|
14
|
+
release(): void;
|
|
15
|
+
}
|
|
7
16
|
export declare class Shell implements InputContext {
|
|
8
17
|
private ptyProcess;
|
|
9
18
|
private bus;
|
|
10
19
|
private handlers;
|
|
11
20
|
private inputHandler;
|
|
12
21
|
private outputParser;
|
|
13
|
-
private
|
|
14
|
-
private
|
|
15
|
-
private
|
|
16
|
-
private
|
|
22
|
+
private hardMuteScopes;
|
|
23
|
+
private softMuteScopes;
|
|
24
|
+
private unmuteScopes;
|
|
25
|
+
private pendingEchoSkips;
|
|
17
26
|
private agentActive;
|
|
18
27
|
private isZsh;
|
|
19
28
|
private tmpDir?;
|
|
@@ -30,6 +39,15 @@ export declare class Shell implements InputContext {
|
|
|
30
39
|
cwd: string;
|
|
31
40
|
instanceId: string;
|
|
32
41
|
});
|
|
42
|
+
/** Compositing-layer claim — overrides any unmute. */
|
|
43
|
+
acquireHardMute(reason: string): ShellScope;
|
|
44
|
+
/** Agent-turn / exec-style mute — overridable by unmute. */
|
|
45
|
+
acquireMute(reason: string): ShellScope;
|
|
46
|
+
/** Force visible while held; overrides soft mutes only. */
|
|
47
|
+
acquireUnmute(reason: string): ShellScope;
|
|
48
|
+
/** Swallow the next \n-terminated chunk from PTY (one per call). */
|
|
49
|
+
skipNextLine(): void;
|
|
50
|
+
private isHostMuted;
|
|
33
51
|
isForegroundBusy(): boolean;
|
|
34
52
|
getCwd(): string;
|
|
35
53
|
isAgentActive(): boolean;
|
|
@@ -52,9 +70,10 @@ export declare class Shell implements InputContext {
|
|
|
52
70
|
private setupOutput;
|
|
53
71
|
private setupInput;
|
|
54
72
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
73
|
+
* shell:on-processing-done splits into unconditional state cleanup
|
|
74
|
+
* (release agent-turn scope) and an advisable redraw (freshPrompt).
|
|
75
|
+
* RemoteSession suppresses the redraw, never the cleanup, so soft-mute
|
|
76
|
+
* can't leak past the end of a turn even when overlays are involved.
|
|
58
77
|
*/
|
|
59
78
|
private setupAgentLifecycle;
|
|
60
79
|
/** Temp directory used for shell config and sockets. */
|
package/dist/shell/shell.js
CHANGED
|
@@ -5,17 +5,19 @@ import * as pty from "node-pty";
|
|
|
5
5
|
import { InputHandler } from "./input-handler.js";
|
|
6
6
|
import { OutputParser } from "./output-parser.js";
|
|
7
7
|
import { getSettings } from "../settings.js";
|
|
8
|
-
import { RefCounter } from "../utils/ref-counter.js";
|
|
9
8
|
export class Shell {
|
|
10
9
|
ptyProcess;
|
|
11
10
|
bus;
|
|
12
11
|
handlers;
|
|
13
12
|
inputHandler;
|
|
14
13
|
outputParser;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
// hardMute is unconditional (overlay compositing); softMute is overridable
|
|
15
|
+
// by unmute (terminal_keys, permission UI). Gate: hard wins; otherwise
|
|
16
|
+
// muted iff softMute held without an unmute.
|
|
17
|
+
hardMuteScopes = new Set();
|
|
18
|
+
softMuteScopes = new Set();
|
|
19
|
+
unmuteScopes = new Set();
|
|
20
|
+
pendingEchoSkips = 0;
|
|
19
21
|
agentActive = false;
|
|
20
22
|
isZsh = false;
|
|
21
23
|
tmpDir;
|
|
@@ -186,12 +188,75 @@ export class Shell {
|
|
|
186
188
|
this.bus.on("shell:pty-resize", ({ cols, rows }) => {
|
|
187
189
|
this.ptyProcess.resize(cols, rows);
|
|
188
190
|
});
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
this.bus.on("shell:stdout-
|
|
194
|
-
|
|
191
|
+
// Compat shims for the bus-event API. shell:stdout-hold maps to hard
|
|
192
|
+
// mute so terminal_keys' stdout-show can't paint through the overlay.
|
|
193
|
+
let holdRefcount = 0;
|
|
194
|
+
let holdScope = null;
|
|
195
|
+
this.bus.on("shell:stdout-hold", () => {
|
|
196
|
+
if (holdRefcount === 0)
|
|
197
|
+
holdScope = this.acquireHardMute("bus:stdout-hold");
|
|
198
|
+
holdRefcount++;
|
|
199
|
+
});
|
|
200
|
+
this.bus.on("shell:stdout-release", () => {
|
|
201
|
+
if (holdRefcount === 0)
|
|
202
|
+
return;
|
|
203
|
+
holdRefcount--;
|
|
204
|
+
if (holdRefcount === 0) {
|
|
205
|
+
holdScope?.release();
|
|
206
|
+
holdScope = null;
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
let showRefcount = 0;
|
|
210
|
+
let showScope = null;
|
|
211
|
+
this.bus.on("shell:stdout-show", () => {
|
|
212
|
+
if (showRefcount === 0)
|
|
213
|
+
showScope = this.acquireUnmute("bus:stdout-show");
|
|
214
|
+
showRefcount++;
|
|
215
|
+
});
|
|
216
|
+
this.bus.on("shell:stdout-hide", () => {
|
|
217
|
+
if (showRefcount === 0)
|
|
218
|
+
return;
|
|
219
|
+
showRefcount--;
|
|
220
|
+
if (showRefcount === 0) {
|
|
221
|
+
showScope?.release();
|
|
222
|
+
showScope = null;
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
// ── Scope-based gating ─────────────────────────────────────
|
|
227
|
+
/** Compositing-layer claim — overrides any unmute. */
|
|
228
|
+
acquireHardMute(reason) {
|
|
229
|
+
const scope = {
|
|
230
|
+
reason,
|
|
231
|
+
release: () => { this.hardMuteScopes.delete(scope); },
|
|
232
|
+
};
|
|
233
|
+
this.hardMuteScopes.add(scope);
|
|
234
|
+
return scope;
|
|
235
|
+
}
|
|
236
|
+
/** Agent-turn / exec-style mute — overridable by unmute. */
|
|
237
|
+
acquireMute(reason) {
|
|
238
|
+
const scope = {
|
|
239
|
+
reason,
|
|
240
|
+
release: () => { this.softMuteScopes.delete(scope); },
|
|
241
|
+
};
|
|
242
|
+
this.softMuteScopes.add(scope);
|
|
243
|
+
return scope;
|
|
244
|
+
}
|
|
245
|
+
/** Force visible while held; overrides soft mutes only. */
|
|
246
|
+
acquireUnmute(reason) {
|
|
247
|
+
const scope = {
|
|
248
|
+
reason,
|
|
249
|
+
release: () => { this.unmuteScopes.delete(scope); },
|
|
250
|
+
};
|
|
251
|
+
this.unmuteScopes.add(scope);
|
|
252
|
+
return scope;
|
|
253
|
+
}
|
|
254
|
+
/** Swallow the next \n-terminated chunk from PTY (one per call). */
|
|
255
|
+
skipNextLine() { this.pendingEchoSkips++; }
|
|
256
|
+
isHostMuted() {
|
|
257
|
+
if (this.hardMuteScopes.size > 0)
|
|
258
|
+
return true;
|
|
259
|
+
return this.softMuteScopes.size > 0 && this.unmuteScopes.size === 0;
|
|
195
260
|
}
|
|
196
261
|
// ── InputContext implementation (delegates to OutputParser) ──
|
|
197
262
|
isForegroundBusy() {
|
|
@@ -211,12 +276,9 @@ export class Shell {
|
|
|
211
276
|
* zsh (ZLE widget) and bash (readline redraw-current-line) bind to repaint.
|
|
212
277
|
*/
|
|
213
278
|
redrawPrompt() {
|
|
214
|
-
// Stale echoSkip/paused from handleProcessingDone re-entering a mode
|
|
215
|
-
// would swallow the redraw and freeze the terminal visually.
|
|
216
|
-
this.echoSkip = false;
|
|
217
|
-
this.paused = false;
|
|
218
279
|
const result = this.bus.emitPipe("shell:redraw-prompt", {
|
|
219
280
|
cwd: this.outputParser.getCwd(),
|
|
281
|
+
kind: "redraw",
|
|
220
282
|
handled: false,
|
|
221
283
|
});
|
|
222
284
|
if (!result.handled) {
|
|
@@ -234,6 +296,7 @@ export class Shell {
|
|
|
234
296
|
freshPrompt() {
|
|
235
297
|
const result = this.bus.emitPipe("shell:redraw-prompt", {
|
|
236
298
|
cwd: this.outputParser.getCwd(),
|
|
299
|
+
kind: "fresh",
|
|
237
300
|
handled: false,
|
|
238
301
|
});
|
|
239
302
|
if (!result.handled) {
|
|
@@ -250,16 +313,13 @@ export class Shell {
|
|
|
250
313
|
this.ptyProcess.onData((data) => {
|
|
251
314
|
this.bus.emit("shell:pty-data", { raw: data });
|
|
252
315
|
this.outputParser.processData(data);
|
|
253
|
-
if (this.
|
|
254
|
-
return;
|
|
255
|
-
if (this.paused && !this.stdoutShow.active)
|
|
316
|
+
if (this.isHostMuted())
|
|
256
317
|
return;
|
|
257
|
-
|
|
258
|
-
if (this.echoSkip) {
|
|
318
|
+
if (this.pendingEchoSkips > 0) {
|
|
259
319
|
const nlIdx = data.indexOf("\n");
|
|
260
320
|
if (nlIdx === -1)
|
|
261
321
|
return;
|
|
262
|
-
this.
|
|
322
|
+
this.pendingEchoSkips--;
|
|
263
323
|
const rest = data.slice(nlIdx + 1);
|
|
264
324
|
if (rest)
|
|
265
325
|
process.stdout.write(rest);
|
|
@@ -275,81 +335,80 @@ export class Shell {
|
|
|
275
335
|
});
|
|
276
336
|
}
|
|
277
337
|
/**
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
338
|
+
* shell:on-processing-done splits into unconditional state cleanup
|
|
339
|
+
* (release agent-turn scope) and an advisable redraw (freshPrompt).
|
|
340
|
+
* RemoteSession suppresses the redraw, never the cleanup, so soft-mute
|
|
341
|
+
* can't leak past the end of a turn even when overlays are involved.
|
|
281
342
|
*/
|
|
282
343
|
setupAgentLifecycle() {
|
|
283
|
-
|
|
284
|
-
// then redraw the prompt when done. Extensions advise these handlers
|
|
285
|
-
// to change behavior (e.g. tmux split keeps the shell interactive).
|
|
344
|
+
let agentTurnScope = null;
|
|
286
345
|
this.handlers.define("shell:on-processing-start", () => {
|
|
287
346
|
this.agentActive = true;
|
|
288
|
-
|
|
347
|
+
agentTurnScope = this.acquireMute("agent-turn");
|
|
289
348
|
});
|
|
290
|
-
this.handlers.define("shell:on-processing-
|
|
291
|
-
this.agentActive = false;
|
|
292
|
-
// If handleProcessingDone re-entered a mode, leave stdout paused so
|
|
293
|
-
// stale PTY output doesn't overwrite the mode prompt (exitMode →
|
|
294
|
-
// redrawPrompt will unpause). Setting echoSkip here would swallow
|
|
295
|
-
// that PTY output since no \n was sent.
|
|
349
|
+
this.handlers.define("shell:on-processing-redraw", () => {
|
|
296
350
|
if (!this.inputHandler.handleProcessingDone()) {
|
|
297
|
-
this.
|
|
298
|
-
|
|
299
|
-
this.echoSkip = true;
|
|
300
|
-
}
|
|
351
|
+
if (this.freshPrompt())
|
|
352
|
+
this.skipNextLine();
|
|
301
353
|
}
|
|
302
354
|
});
|
|
355
|
+
this.handlers.define("shell:on-processing-done", () => {
|
|
356
|
+
this.agentActive = false;
|
|
357
|
+
agentTurnScope?.release();
|
|
358
|
+
agentTurnScope = null;
|
|
359
|
+
this.handlers.call("shell:on-processing-redraw");
|
|
360
|
+
});
|
|
303
361
|
this.bus.on("agent:processing-start", () => {
|
|
304
362
|
this.handlers.call("shell:on-processing-start");
|
|
305
363
|
});
|
|
306
364
|
this.bus.on("agent:processing-done", () => {
|
|
307
365
|
this.handlers.call("shell:on-processing-done");
|
|
308
366
|
});
|
|
309
|
-
// Permission
|
|
310
|
-
//
|
|
367
|
+
// Permission UI is briefly visible during the prompt; an unmute scope
|
|
368
|
+
// overrides whatever mute is currently held, then releases cleanly.
|
|
369
|
+
// Doesn't touch agent-turn state, so suppressed handlers can't leak.
|
|
370
|
+
let permissionVisible = null;
|
|
311
371
|
this.bus.on("permission:request", () => {
|
|
312
|
-
|
|
372
|
+
permissionVisible?.release();
|
|
373
|
+
permissionVisible = this.acquireUnmute("permission-ui");
|
|
313
374
|
});
|
|
314
375
|
this.bus.onPipeAsync("permission:request", async (payload) => {
|
|
315
|
-
|
|
376
|
+
permissionVisible?.release();
|
|
377
|
+
permissionVisible = null;
|
|
316
378
|
return payload;
|
|
317
379
|
});
|
|
318
|
-
// Shell exec: write a command to the live PTY and capture its output.
|
|
319
|
-
// stdout is paused during agent processing, so PTY output flows through
|
|
320
|
-
// OutputParser (for OSC detection) but never reaches the terminal.
|
|
321
380
|
this.bus.onPipeAsync("shell:exec-request", async (payload) => {
|
|
322
|
-
|
|
323
|
-
this.
|
|
381
|
+
const visible = this.acquireUnmute("exec-request");
|
|
382
|
+
this.skipNextLine();
|
|
324
383
|
process.stdout.write("\n");
|
|
325
384
|
this.bus.emit("shell:agent-exec-start", {});
|
|
326
|
-
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
385
|
+
try {
|
|
386
|
+
const output = await new Promise((resolve, reject) => {
|
|
387
|
+
const timeout = setTimeout(() => {
|
|
388
|
+
this.bus.off("shell:command-done", handler);
|
|
389
|
+
this.ptyProcess.write("\x03");
|
|
390
|
+
reject(new Error("Shell exec timed out after 30s"));
|
|
391
|
+
}, 30_000);
|
|
392
|
+
const handler = (e) => {
|
|
393
|
+
clearTimeout(timeout);
|
|
394
|
+
this.bus.off("shell:command-done", handler);
|
|
395
|
+
resolve({ output: e.output, cwd: e.cwd, exitCode: e.exitCode });
|
|
396
|
+
};
|
|
397
|
+
this.bus.on("shell:command-done", handler);
|
|
398
|
+
this.outputParser.onCommandEntered(payload.command, this.outputParser.getCwd());
|
|
399
|
+
// Collapse literal newlines to spaces so the PTY receives a single-line
|
|
400
|
+
// command. Multi-line commands (e.g. git commit -m "...\n...") would
|
|
401
|
+
// cause the shell to execute prematurely, producing garbled output from
|
|
402
|
+
// syntax highlighting plugins (zsh syntax highlighting, etc).
|
|
403
|
+
const oneLine = payload.command.replace(/\n/g, " ");
|
|
404
|
+
this.ptyProcess.write(oneLine + "\r");
|
|
405
|
+
});
|
|
406
|
+
return { ...payload, output: output.output, cwd: output.cwd, exitCode: output.exitCode, done: true };
|
|
407
|
+
}
|
|
408
|
+
finally {
|
|
409
|
+
visible.release();
|
|
410
|
+
this.bus.emit("shell:agent-exec-done", {});
|
|
411
|
+
}
|
|
353
412
|
});
|
|
354
413
|
}
|
|
355
414
|
// ── Public API (used by index.ts) ──
|
|
@@ -172,7 +172,6 @@ export declare class FloatingPanel {
|
|
|
172
172
|
private prevFrame;
|
|
173
173
|
private suppressNextRedraw;
|
|
174
174
|
private autoDismissTimer;
|
|
175
|
-
private ptyBuffer;
|
|
176
175
|
private usedAltScreen;
|
|
177
176
|
private wrapCache;
|
|
178
177
|
private wrapCacheWidth;
|
|
@@ -204,6 +203,7 @@ export declare class FloatingPanel {
|
|
|
204
203
|
appendText(text: string): void;
|
|
205
204
|
appendLine(line: string): void;
|
|
206
205
|
updateLastLine(fn: (line: string) => string): void;
|
|
206
|
+
popLastLine(): void;
|
|
207
207
|
clearContent(): void;
|
|
208
208
|
setTitle(title: string): void;
|
|
209
209
|
setFooter(footer: string): void;
|
|
@@ -222,7 +222,6 @@ export declare class FloatingPanel {
|
|
|
222
222
|
private buildFrame;
|
|
223
223
|
private scheduleRender;
|
|
224
224
|
private render;
|
|
225
|
-
/** Full screen teardown: exit alt screen, release stdout, force redraw. */
|
|
226
225
|
private teardownScreen;
|
|
227
226
|
/** Start rendering TerminalBuffer directly (no overlay box). */
|
|
228
227
|
private startPassthrough;
|
|
@@ -118,7 +118,6 @@ export class FloatingPanel {
|
|
|
118
118
|
prevFrame = [];
|
|
119
119
|
suppressNextRedraw = false;
|
|
120
120
|
autoDismissTimer = null;
|
|
121
|
-
ptyBuffer = ""; // PTY output accumulated while overlay is open
|
|
122
121
|
usedAltScreen = false; // whether we entered our own alt screen
|
|
123
122
|
wrapCache = new Map(); // line → wrapped lines (invalidated on width change)
|
|
124
123
|
wrapCacheWidth = 0;
|
|
@@ -287,20 +286,14 @@ export class FloatingPanel {
|
|
|
287
286
|
}
|
|
288
287
|
// ── Bus event wiring ───────────────────────────────────────
|
|
289
288
|
wireEvents() {
|
|
290
|
-
// Buffer PTY output while overlay is visible (alt screen discards it).
|
|
291
|
-
// Don't buffer when hidden — PTY flows to terminal directly via stdout-show.
|
|
292
|
-
this.bus.on("shell:pty-data", ({ raw }) => {
|
|
293
|
-
if (this._visible)
|
|
294
|
-
this.ptyBuffer += raw;
|
|
295
|
-
});
|
|
296
289
|
this.bus.onPipe("input:intercept", (payload) => this.handleIntercept(payload));
|
|
297
290
|
this.bus.onPipe("shell:redraw-prompt", (payload) => {
|
|
298
291
|
if (this._visible || this._passthrough) {
|
|
299
292
|
return { ...payload, handled: true };
|
|
300
293
|
}
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
if (this.suppressNextRedraw) {
|
|
294
|
+
// Suppress only freshPrompt's \n — an in-place redraw must not
|
|
295
|
+
// consume the slot, or unrelated mode-exit redraws go missing.
|
|
296
|
+
if (this.suppressNextRedraw && payload.kind === "fresh") {
|
|
304
297
|
this.suppressNextRedraw = false;
|
|
305
298
|
return { ...payload, handled: true };
|
|
306
299
|
}
|
|
@@ -374,7 +367,6 @@ export class FloatingPanel {
|
|
|
374
367
|
// so the background program's screen stays correct without
|
|
375
368
|
// handing rendering control back to ncurses.
|
|
376
369
|
this._passthrough = true;
|
|
377
|
-
this.ptyBuffer = "";
|
|
378
370
|
this.startPassthrough();
|
|
379
371
|
}
|
|
380
372
|
else {
|
|
@@ -435,7 +427,6 @@ export class FloatingPanel {
|
|
|
435
427
|
/** Common screen enter logic shared by open() and show(). */
|
|
436
428
|
enterScreen() {
|
|
437
429
|
this._visible = true;
|
|
438
|
-
this.ptyBuffer = "";
|
|
439
430
|
this.bus.emit("shell:stdout-hold", {});
|
|
440
431
|
this.usedAltScreen = !(this.buffer?.altScreen);
|
|
441
432
|
if (this.usedAltScreen) {
|
|
@@ -471,6 +462,15 @@ export class FloatingPanel {
|
|
|
471
462
|
}
|
|
472
463
|
this.scheduleRender();
|
|
473
464
|
}
|
|
465
|
+
popLastLine() {
|
|
466
|
+
if (this.currentPartialLine) {
|
|
467
|
+
this.currentPartialLine = "";
|
|
468
|
+
}
|
|
469
|
+
else if (this.contentLines.length > 0) {
|
|
470
|
+
this.contentLines.pop();
|
|
471
|
+
}
|
|
472
|
+
this.scheduleRender();
|
|
473
|
+
}
|
|
474
474
|
clearContent() {
|
|
475
475
|
this.contentLines = [];
|
|
476
476
|
this.currentPartialLine = "";
|
|
@@ -751,39 +751,38 @@ export class FloatingPanel {
|
|
|
751
751
|
this.prevFrame = frame;
|
|
752
752
|
}
|
|
753
753
|
// ── Screen helpers ────────────────────────────────────────
|
|
754
|
-
/** Full screen teardown: exit alt screen, release stdout, force redraw. */
|
|
755
754
|
teardownScreen() {
|
|
756
755
|
this.resizeUnsub?.();
|
|
757
756
|
this.resizeUnsub = null;
|
|
758
757
|
this.suppressNextRedraw = true;
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
// Without this, commands run by the agent (e.g. user_shell ls)
|
|
768
|
-
// would vanish — the alt screen exit restores the saved screen
|
|
769
|
-
// from before the overlay opened, losing any shell output produced
|
|
770
|
-
// during the session.
|
|
771
|
-
if (this.ptyBuffer) {
|
|
772
|
-
this.surface.write(this.ptyBuffer);
|
|
773
|
-
}
|
|
774
|
-
this.ptyBuffer = "";
|
|
775
|
-
this.bus.emit("shell:stdout-release", {});
|
|
776
|
-
if (stillInAltScreen || programExited) {
|
|
777
|
-
// Either a TUI app is still running and needs SIGWINCH to repaint,
|
|
778
|
-
// or the overlaid program exited (e.g. agent quit vim) and we
|
|
779
|
-
// discarded its stale buffer — SIGWINCH makes the shell redraw
|
|
780
|
-
// its prompt cleanly.
|
|
758
|
+
this.buffer?.flush();
|
|
759
|
+
const programInAlt = !!this.buffer?.altScreen;
|
|
760
|
+
if (!this.usedAltScreen && programInAlt) {
|
|
761
|
+
// Program still in its own alt-screen — SIGWINCH so it redraws
|
|
762
|
+
// and re-asserts its modes; replaying from the mirror would
|
|
763
|
+
// freeze modes serialize() doesn't track (modifyOtherKeys, kitty
|
|
764
|
+
// kbd) and leave ctrl-c arriving as \x1b[27;5;99~.
|
|
765
|
+
this.bus.emit("shell:stdout-release", {});
|
|
781
766
|
const cols = this.surface.columns;
|
|
782
767
|
const rows = this.surface.rows;
|
|
783
768
|
this.bus.emit("shell:pty-resize", { cols, rows: rows - 1 });
|
|
784
769
|
setTimeout(() => {
|
|
785
770
|
this.bus.emit("shell:pty-resize", { cols, rows });
|
|
786
771
|
}, 50);
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
this.surface.write("\x1b[?1049l");
|
|
775
|
+
if (!this.usedAltScreen) {
|
|
776
|
+
// Program exited mid-overlay; its reset bytes were eaten by
|
|
777
|
+
// stdout-hold. Reset modes serialize() doesn't track or the
|
|
778
|
+
// host stays in vim's modifyOtherKeys mode.
|
|
779
|
+
this.surface.write("\x1b[>4;0m\x1b[<u\x1b[?2004l\x1b[?1004l" +
|
|
780
|
+
"\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l");
|
|
781
|
+
}
|
|
782
|
+
this.bus.emit("shell:stdout-release", {});
|
|
783
|
+
const serialized = this.buffer?.serialize();
|
|
784
|
+
if (serialized) {
|
|
785
|
+
this.surface.write(`${SYNC_START}\x1b[2J\x1b[H${serialized}${SYNC_END}`);
|
|
787
786
|
}
|
|
788
787
|
}
|
|
789
788
|
// ── Passthrough rendering ─────────────────────────────────
|
|
@@ -808,7 +807,7 @@ export class FloatingPanel {
|
|
|
808
807
|
const serialized = this.buffer.serialize();
|
|
809
808
|
if (serialized && serialized !== this.prevSerialized) {
|
|
810
809
|
this.prevSerialized = serialized;
|
|
811
|
-
this.surface.write(`${SYNC_START}\x1b[H${serialized}${SYNC_END}`);
|
|
810
|
+
this.surface.write(`${SYNC_START}\x1b[2J\x1b[H${serialized}${SYNC_END}`);
|
|
812
811
|
}
|
|
813
812
|
}
|
|
814
813
|
resolveSize(spec, available) {
|
package/dist/utils/markdown.js
CHANGED
|
@@ -364,6 +364,8 @@ export class MarkdownRenderer {
|
|
|
364
364
|
return this.renderInline(line);
|
|
365
365
|
}
|
|
366
366
|
renderInline(text) {
|
|
367
|
+
// Links first — later subs inject `\x1b[…m` whose `[` would be eaten here.
|
|
368
|
+
text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, `$1 ${p.muted}${p.underline}($2)${p.reset}`);
|
|
367
369
|
// Inline code
|
|
368
370
|
text = text.replace(/`([^`]+)`/g, `${p.accent}$1${p.reset}`);
|
|
369
371
|
// Bold + italic
|
|
@@ -376,8 +378,6 @@ export class MarkdownRenderer {
|
|
|
376
378
|
text = text.replace(/(?<!\w)_(.+?)_(?!\w)/g, `${p.italic}$1${p.reset}`);
|
|
377
379
|
// Strikethrough
|
|
378
380
|
text = text.replace(/~~(.+?)~~/g, `${p.dim}$1${p.reset}`);
|
|
379
|
-
// Links
|
|
380
|
-
text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, `$1 ${p.muted}${p.underline}($2)${p.reset}`);
|
|
381
381
|
return text;
|
|
382
382
|
}
|
|
383
383
|
/**
|