agent-sh 0.9.0 → 0.10.1
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 +25 -30
- package/dist/agent/agent-loop.d.ts +43 -6
- package/dist/agent/agent-loop.js +817 -157
- package/dist/agent/conversation-state.d.ts +72 -21
- package/dist/agent/conversation-state.js +364 -151
- package/dist/agent/history-file.d.ts +13 -4
- package/dist/agent/history-file.js +110 -36
- package/dist/agent/nuclear-form.d.ts +28 -3
- package/dist/agent/nuclear-form.js +84 -3
- package/dist/agent/skills.d.ts +2 -4
- package/dist/agent/skills.js +10 -4
- package/dist/agent/subagent.d.ts +23 -0
- package/dist/agent/subagent.js +53 -11
- package/dist/agent/system-prompt.d.ts +34 -1
- package/dist/agent/system-prompt.js +96 -47
- package/dist/agent/token-budget.d.ts +10 -13
- package/dist/agent/token-budget.js +6 -46
- package/dist/agent/tool-protocol.d.ts +23 -1
- package/dist/agent/tool-protocol.js +169 -4
- package/dist/agent/tools/bash.js +3 -3
- package/dist/agent/tools/edit-file.js +9 -6
- package/dist/agent/tools/glob.js +4 -2
- package/dist/agent/tools/grep.js +27 -3
- package/dist/agent/tools/ls.js +5 -6
- package/dist/agent/types.d.ts +1 -2
- package/dist/context-manager.d.ts +16 -19
- package/dist/context-manager.js +48 -152
- package/dist/core.js +27 -6
- package/dist/event-bus.d.ts +59 -3
- package/dist/executor.d.ts +4 -3
- package/dist/executor.js +18 -15
- package/dist/extension-loader.js +75 -17
- package/dist/extensions/agent-backend.d.ts +8 -7
- package/dist/extensions/agent-backend.js +72 -50
- package/dist/extensions/index.js +0 -2
- package/dist/extensions/slash-commands.js +14 -9
- package/dist/extensions/tui-renderer.js +67 -80
- package/dist/index.js +25 -6
- package/dist/settings.d.ts +39 -16
- package/dist/settings.js +51 -11
- package/dist/shell/input-handler.d.ts +2 -1
- package/dist/shell/input-handler.js +84 -76
- package/dist/shell/shell.js +19 -2
- package/dist/types.d.ts +15 -0
- package/dist/utils/ansi.d.ts +7 -0
- package/dist/utils/ansi.js +69 -8
- package/dist/utils/box-frame.js +8 -2
- package/dist/utils/compositor.d.ts +5 -0
- package/dist/utils/compositor.js +31 -3
- package/dist/utils/diff-renderer.d.ts +9 -0
- package/dist/utils/diff-renderer.js +221 -143
- package/dist/utils/diff.d.ts +21 -2
- package/dist/utils/diff.js +165 -89
- package/dist/utils/handler-registry.d.ts +5 -0
- package/dist/utils/handler-registry.js +6 -0
- package/dist/utils/line-editor.d.ts +11 -1
- package/dist/utils/line-editor.js +44 -5
- package/dist/utils/markdown.js +23 -8
- package/dist/utils/package-version.d.ts +1 -0
- package/dist/utils/package-version.js +10 -0
- package/dist/utils/shell-output-spill.d.ts +2 -0
- package/dist/utils/shell-output-spill.js +81 -0
- package/dist/utils/tool-display.d.ts +1 -1
- package/dist/utils/tool-display.js +4 -4
- package/examples/extensions/ash-acp-bridge/src/index.ts +4 -1
- package/examples/extensions/ash-mcp-bridge/index.ts +13 -3
- package/examples/extensions/claude-code-bridge/README.md +14 -0
- package/examples/extensions/claude-code-bridge/index.ts +204 -145
- package/examples/extensions/claude-code-bridge/package.json +1 -0
- package/examples/extensions/interactive-prompts.ts +39 -25
- package/examples/extensions/overlay-agent.ts +3 -3
- package/examples/extensions/peer-mesh.ts +115 -0
- package/examples/extensions/pi-bridge/README.md +16 -0
- package/examples/extensions/pi-bridge/index.ts +9 -155
- package/examples/extensions/questionnaire.ts +16 -5
- package/examples/extensions/subagents.ts +19 -4
- package/examples/extensions/terminal-buffer.ts +163 -0
- package/examples/extensions/user-shell.ts +136 -0
- package/examples/extensions/web-access.ts +8 -0
- package/package.json +36 -2
- package/dist/agent/tools/display.d.ts +0 -13
- package/dist/agent/tools/display.js +0 -70
- package/dist/agent/tools/user-shell.d.ts +0 -13
- package/dist/agent/tools/user-shell.js +0 -87
- package/dist/extensions/shell-recall.d.ts +0 -9
- package/dist/extensions/shell-recall.js +0 -8
- package/dist/extensions/terminal-buffer.d.ts +0 -14
- package/dist/extensions/terminal-buffer.js +0 -134
|
@@ -4,7 +4,7 @@ import { visibleLen } from "../utils/ansi.js";
|
|
|
4
4
|
import { palette as p } from "../utils/palette.js";
|
|
5
5
|
import { LineEditor } from "../utils/line-editor.js";
|
|
6
6
|
import { CONFIG_DIR, getSettings } from "../settings.js";
|
|
7
|
-
const HISTORY_FILE = path.join(CONFIG_DIR, "history");
|
|
7
|
+
const HISTORY_FILE = path.join(CONFIG_DIR, "input-history");
|
|
8
8
|
export class InputHandler {
|
|
9
9
|
ctx;
|
|
10
10
|
lineBuffer = "";
|
|
@@ -20,7 +20,8 @@ export class InputHandler {
|
|
|
20
20
|
history = [];
|
|
21
21
|
historyIndex = -1; // -1 = not browsing history
|
|
22
22
|
savedBuffer = ""; // buffer saved when entering history
|
|
23
|
-
|
|
23
|
+
cursorRowsBelow = 0; // rows from prompt top to cursor row
|
|
24
|
+
cursorTermCol = 1; // 1-indexed terminal column of cursor
|
|
24
25
|
escapeTimer = null;
|
|
25
26
|
bus;
|
|
26
27
|
onShowAgentInfo;
|
|
@@ -72,9 +73,10 @@ export class InputHandler {
|
|
|
72
73
|
/** Write the mode prompt line with cursor at the correct position. */
|
|
73
74
|
writeModePromptLine(showBuffer = true) {
|
|
74
75
|
const termW = process.stdout.columns || 80;
|
|
75
|
-
// Move cursor to the start of the prompt area
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
// Move cursor to the start of the prompt area.
|
|
77
|
+
// We know exactly how many rows below the top the cursor currently sits.
|
|
78
|
+
if (this.cursorRowsBelow > 0) {
|
|
79
|
+
process.stdout.write(`\x1b[${this.cursorRowsBelow}A`);
|
|
78
80
|
}
|
|
79
81
|
// Clear from here to end of screen — removes current + all wrapped lines below
|
|
80
82
|
process.stdout.write("\r\x1b[J");
|
|
@@ -88,37 +90,36 @@ export class InputHandler {
|
|
|
88
90
|
const promptVisLen = visibleLen(infoPrefix) + visibleLen(icon) + 1; // icon + space
|
|
89
91
|
const display = showBuffer ? this.editor.displayText : "";
|
|
90
92
|
const dCursor = showBuffer ? this.editor.displayCursor : 0;
|
|
91
|
-
if (!showBuffer
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
93
|
+
if (!showBuffer) {
|
|
94
|
+
// No buffer — just write the prompt prefix, cursor stays at end
|
|
95
|
+
process.stdout.write(promptPrefix);
|
|
96
|
+
const N = promptVisLen;
|
|
97
|
+
this.cursorRowsBelow = N > 0 ? Math.ceil(N / termW) - 1 : 0;
|
|
98
|
+
this.cursorTermCol = N === 0 ? 1 : (N % termW === 0 ? termW : (N % termW) + 1);
|
|
99
|
+
}
|
|
100
|
+
else if (!display.includes("\n")) {
|
|
101
|
+
// Single-line: write up to cursor, save, write rest, restore.
|
|
102
|
+
// The terminal handles all wrapping — no manual row/col math needed.
|
|
103
|
+
const before = display.slice(0, dCursor);
|
|
104
|
+
const after = display.slice(dCursor);
|
|
105
|
+
process.stdout.write(promptPrefix + p.accent + before + p.reset +
|
|
106
|
+
"\x1b7" + // DECSC — save cursor position
|
|
107
|
+
p.accent + after + p.reset +
|
|
108
|
+
"\x1b8" // DECRC — restore cursor position
|
|
109
|
+
);
|
|
110
|
+
// cursorRowsBelow is distance from cursor (restored by DECRC, sitting at
|
|
111
|
+
// the cursor col) back up to the prompt's top row. Next redraw uses it
|
|
112
|
+
// with \x1b[${n}A then \x1b[J — moving past the top scrolls the screen.
|
|
113
|
+
const cursorVisCol = promptVisLen + visibleLen(before);
|
|
114
|
+
this.cursorRowsBelow = cursorVisCol > 0 ? Math.ceil(cursorVisCol / termW) - 1 : 0;
|
|
115
|
+
this.cursorTermCol = cursorVisCol === 0 ? 1 : (cursorVisCol % termW === 0 ? termW : (cursorVisCol % termW) + 1);
|
|
103
116
|
}
|
|
104
117
|
else {
|
|
105
|
-
// Multi-line: render each line with continuation indent
|
|
118
|
+
// Multi-line: render each line with continuation indent.
|
|
119
|
+
// Same save/restore strategy — cursor position is never computed.
|
|
106
120
|
const lines = display.split("\n");
|
|
107
121
|
const indent = " ".repeat(promptVisLen);
|
|
108
|
-
|
|
109
|
-
for (let li = 0; li < lines.length; li++) {
|
|
110
|
-
const prefix = li === 0 ? promptPrefix : indent;
|
|
111
|
-
const prefixVisLen = li === 0 ? promptVisLen : promptVisLen;
|
|
112
|
-
const lineText = lines[li];
|
|
113
|
-
process.stdout.write(prefix + p.accent + lineText + p.reset);
|
|
114
|
-
if (li < lines.length - 1)
|
|
115
|
-
process.stdout.write("\n");
|
|
116
|
-
// Count terminal lines this logical line occupies
|
|
117
|
-
const lineVisLen = prefixVisLen + lineText.length;
|
|
118
|
-
totalTermLines += lineVisLen > 0 ? Math.ceil(lineVisLen / termW) : 1;
|
|
119
|
-
}
|
|
120
|
-
this.promptWrappedLines = totalTermLines - 1;
|
|
121
|
-
// Position cursor: find which display line and column the cursor is on
|
|
122
|
+
// Locate cursor: which logical line and offset within it.
|
|
122
123
|
let charsRemaining = dCursor;
|
|
123
124
|
let cursorLine = 0;
|
|
124
125
|
for (let li = 0; li < lines.length; li++) {
|
|
@@ -129,31 +130,37 @@ export class InputHandler {
|
|
|
129
130
|
charsRemaining -= lines[li].length + 1; // +1 for \n
|
|
130
131
|
cursorLine = li + 1;
|
|
131
132
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
133
|
+
let output = "";
|
|
134
|
+
let cursorRowFromTop = 0;
|
|
135
|
+
let rowsSoFar = 0;
|
|
136
|
+
for (let li = 0; li < lines.length; li++) {
|
|
137
|
+
const prefix = li === 0 ? promptPrefix : indent;
|
|
138
|
+
const lineText = lines[li];
|
|
139
|
+
const lineVisLen = promptVisLen + visibleLen(lineText);
|
|
140
|
+
const lineTermRows = lineVisLen > 0 ? Math.ceil(lineVisLen / termW) : 1;
|
|
141
|
+
if (li === cursorLine) {
|
|
142
|
+
// Split this line at the cursor.
|
|
143
|
+
const before = lineText.slice(0, charsRemaining);
|
|
144
|
+
const after = lineText.slice(charsRemaining);
|
|
145
|
+
output += prefix + p.accent + before + p.reset;
|
|
146
|
+
output += "\x1b7"; // DECSC — save cursor position
|
|
147
|
+
output += p.accent + after + p.reset;
|
|
148
|
+
const beforeVisCol = promptVisLen + visibleLen(before);
|
|
149
|
+
cursorRowFromTop = rowsSoFar + (beforeVisCol > 0 ? Math.ceil(beforeVisCol / termW) - 1 : 0);
|
|
150
|
+
this.cursorTermCol = beforeVisCol === 0 ? 1 : (beforeVisCol % termW === 0 ? termW : (beforeVisCol % termW) + 1);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
output += prefix + p.accent + lineText + p.reset;
|
|
154
|
+
}
|
|
155
|
+
if (li < lines.length - 1)
|
|
156
|
+
output += "\n";
|
|
157
|
+
rowsSoFar += lineTermRows;
|
|
156
158
|
}
|
|
159
|
+
process.stdout.write(output + "\x1b8"); // DECRC — restore cursor position
|
|
160
|
+
// Distance from cursor (where DECRC lands) back to the top row. Next
|
|
161
|
+
// redraw moves up by this and clears to end-of-screen — \x1b[J handles
|
|
162
|
+
// everything below, including rows after the cursor's logical line.
|
|
163
|
+
this.cursorRowsBelow = cursorRowFromTop;
|
|
157
164
|
}
|
|
158
165
|
}
|
|
159
166
|
handleInput(data) {
|
|
@@ -281,15 +288,17 @@ export class InputHandler {
|
|
|
281
288
|
// Disable kitty keyboard protocol and bracket paste mode
|
|
282
289
|
process.stdout.write("\x1b[<u\x1b[?2004l");
|
|
283
290
|
this.clearPromptArea();
|
|
291
|
+
this.cursorRowsBelow = 0;
|
|
292
|
+
this.cursorTermCol = 1;
|
|
284
293
|
this.printPrompt();
|
|
285
294
|
}
|
|
286
295
|
/** Move to the start of the prompt area and clear everything below. */
|
|
287
296
|
clearPromptArea() {
|
|
288
|
-
if (this.
|
|
289
|
-
process.stdout.write(`\x1b[${this.
|
|
297
|
+
if (this.cursorRowsBelow > 0) {
|
|
298
|
+
process.stdout.write(`\x1b[${this.cursorRowsBelow}A`);
|
|
290
299
|
}
|
|
291
300
|
process.stdout.write("\r\x1b[J");
|
|
292
|
-
this.
|
|
301
|
+
this.cursorRowsBelow = 0;
|
|
293
302
|
}
|
|
294
303
|
printPrompt() {
|
|
295
304
|
this.ctx.redrawPrompt();
|
|
@@ -363,16 +372,10 @@ export class InputHandler {
|
|
|
363
372
|
if (this.autocompleteLines > 0) {
|
|
364
373
|
process.stdout.write(`\x1b[${this.autocompleteLines}A`);
|
|
365
374
|
}
|
|
366
|
-
//
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
? `${agentInfo.info} ${indicator} `
|
|
371
|
-
: `${indicator} `;
|
|
372
|
-
const icon = this.activeMode?.promptIcon ?? "❯";
|
|
373
|
-
const promptVisLen = visibleLen(infoPrefix) + visibleLen(icon) + 1;
|
|
374
|
-
const col = promptVisLen + this.editor.displayCursor;
|
|
375
|
-
process.stdout.write(`\r\x1b[${col}C`);
|
|
375
|
+
// Restore cursor column — use explicit column set instead of DECRC
|
|
376
|
+
// because writing \n above may have scrolled the terminal, which
|
|
377
|
+
// invalidates the absolute position saved by DECSC.
|
|
378
|
+
process.stdout.write(`\x1b[${this.cursorTermCol}G`);
|
|
376
379
|
}
|
|
377
380
|
applyAutocomplete() {
|
|
378
381
|
if (!this.autocompleteActive || this.autocompleteItems.length === 0)
|
|
@@ -407,11 +410,12 @@ export class InputHandler {
|
|
|
407
410
|
clearAutocompleteLines() {
|
|
408
411
|
if (this.autocompleteLines <= 0)
|
|
409
412
|
return;
|
|
410
|
-
|
|
413
|
+
// Use CSI B (cursor down, bounded) instead of \n to avoid scroll
|
|
411
414
|
for (let i = 0; i < this.autocompleteLines; i++) {
|
|
412
|
-
process.stdout.write("\
|
|
415
|
+
process.stdout.write("\x1b[B\x1b[2K"); // move down, clear line
|
|
413
416
|
}
|
|
414
|
-
|
|
417
|
+
// Move back up and restore column with relative movement (scroll-safe)
|
|
418
|
+
process.stdout.write(`\x1b[${this.autocompleteLines}A\x1b[${this.cursorTermCol}G`);
|
|
415
419
|
this.autocompleteLines = 0;
|
|
416
420
|
}
|
|
417
421
|
handleModeInput(data) {
|
|
@@ -475,13 +479,20 @@ export class InputHandler {
|
|
|
475
479
|
const currentMode = this.activeMode;
|
|
476
480
|
this.activeMode = null;
|
|
477
481
|
this.editor.clear();
|
|
482
|
+
this.cursorRowsBelow = 0;
|
|
483
|
+
this.cursorTermCol = 1;
|
|
478
484
|
this.dismissAutocomplete();
|
|
479
485
|
if (query && query.startsWith("/")) {
|
|
480
486
|
const spaceIdx = query.indexOf(" ");
|
|
481
487
|
const name = spaceIdx === -1 ? query : query.slice(0, spaceIdx);
|
|
482
488
|
const args = spaceIdx === -1 ? "" : query.slice(spaceIdx + 1).trim();
|
|
483
489
|
this.bus.emit("command:execute", { name, args });
|
|
484
|
-
|
|
490
|
+
if (currentMode.returnToSelf) {
|
|
491
|
+
this.enterMode(currentMode);
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
this.ctx.freshPrompt();
|
|
495
|
+
}
|
|
485
496
|
}
|
|
486
497
|
else if (query) {
|
|
487
498
|
this.pendingReturnMode = currentMode.returnToSelf ? currentMode.id : null;
|
|
@@ -510,9 +521,6 @@ export class InputHandler {
|
|
|
510
521
|
this.applyAutocomplete();
|
|
511
522
|
}
|
|
512
523
|
break;
|
|
513
|
-
case "shift+tab":
|
|
514
|
-
this.bus.emit("config:cycle", {});
|
|
515
|
-
break;
|
|
516
524
|
case "arrow-up":
|
|
517
525
|
if (this.autocompleteActive) {
|
|
518
526
|
this.autocompleteIndex =
|
package/dist/shell/shell.js
CHANGED
|
@@ -197,6 +197,11 @@ export class Shell {
|
|
|
197
197
|
* For bash, falls back to sending \n for a fresh prompt cycle.
|
|
198
198
|
*/
|
|
199
199
|
redrawPrompt() {
|
|
200
|
+
// A stale echoSkip or paused flag (left over from handleProcessingDone
|
|
201
|
+
// re-entering a mode) would swallow the redrawn prompt and make the
|
|
202
|
+
// terminal appear frozen. Reset both before emitting.
|
|
203
|
+
this.echoSkip = false;
|
|
204
|
+
this.paused = false;
|
|
200
205
|
const result = this.bus.emitPipe("shell:redraw-prompt", {
|
|
201
206
|
cwd: this.outputParser.getCwd(),
|
|
202
207
|
handled: false,
|
|
@@ -277,9 +282,13 @@ export class Shell {
|
|
|
277
282
|
this.paused = true;
|
|
278
283
|
});
|
|
279
284
|
this.handlers.define("shell:on-processing-done", () => {
|
|
280
|
-
this.paused = false;
|
|
281
285
|
this.agentActive = false;
|
|
286
|
+
// If handleProcessingDone re-entered a mode, leave stdout paused so
|
|
287
|
+
// stale PTY output doesn't overwrite the mode prompt (exitMode →
|
|
288
|
+
// redrawPrompt will unpause). Setting echoSkip here would swallow
|
|
289
|
+
// that PTY output since no \n was sent.
|
|
282
290
|
if (!this.inputHandler.handleProcessingDone()) {
|
|
291
|
+
this.paused = false;
|
|
283
292
|
if (this.freshPrompt()) {
|
|
284
293
|
this.echoSkip = true;
|
|
285
294
|
}
|
|
@@ -317,11 +326,19 @@ export class Shell {
|
|
|
317
326
|
const handler = (e) => {
|
|
318
327
|
clearTimeout(timeout);
|
|
319
328
|
this.bus.off("shell:command-done", handler);
|
|
329
|
+
// Re-pause stdout so the prompt text following the marker doesn't
|
|
330
|
+
// leak to the terminal while the agent is still processing.
|
|
331
|
+
this.paused = true;
|
|
320
332
|
resolve({ output: e.output, cwd: e.cwd, exitCode: e.exitCode });
|
|
321
333
|
};
|
|
322
334
|
this.bus.on("shell:command-done", handler);
|
|
323
335
|
this.outputParser.onCommandEntered(payload.command, this.outputParser.getCwd());
|
|
324
|
-
|
|
336
|
+
// Collapse literal newlines to spaces so the PTY receives a single-line
|
|
337
|
+
// command. Multi-line commands (e.g. git commit -m "...\n...") would
|
|
338
|
+
// cause the shell to execute prematurely, producing garbled output from
|
|
339
|
+
// syntax highlighting plugins (zsh syntax highlighting, etc).
|
|
340
|
+
const oneLine = payload.command.replace(/\n/g, " ");
|
|
341
|
+
this.ptyProcess.write(oneLine + "\r");
|
|
325
342
|
});
|
|
326
343
|
this.paused = true;
|
|
327
344
|
this.echoSkip = false;
|
package/dist/types.d.ts
CHANGED
|
@@ -80,6 +80,12 @@ export interface ExtensionContext {
|
|
|
80
80
|
createFencedBlockTransform: (opts: FencedBlockTransformOptions) => void;
|
|
81
81
|
/** Read extension-namespaced settings from ~/.agent-sh/settings.json. */
|
|
82
82
|
getExtensionSettings: <T extends Record<string, unknown>>(namespace: string, defaults: T) => T;
|
|
83
|
+
/**
|
|
84
|
+
* Get (and lazily create) a per-extension storage directory under
|
|
85
|
+
* ~/.agent-sh/<namespace>/. Returns the absolute path. Lets extensions
|
|
86
|
+
* persist state without each one re-deriving the location.
|
|
87
|
+
*/
|
|
88
|
+
getStoragePath: (namespace: string) => string;
|
|
83
89
|
/** Register a slash command available in any input mode. */
|
|
84
90
|
registerCommand: (name: string, description: string, handler: (args: string) => Promise<void> | void) => void;
|
|
85
91
|
/** Register a tool for the built-in agent. No-op when using bridge backends. */
|
|
@@ -92,12 +98,18 @@ export interface ExtensionContext {
|
|
|
92
98
|
registerInstruction: (name: string, text: string) => void;
|
|
93
99
|
/** Remove a named instruction block from the system prompt. */
|
|
94
100
|
removeInstruction: (name: string) => void;
|
|
101
|
+
/** Register a skill (on-demand reference material) for the agent. */
|
|
102
|
+
registerSkill: (name: string, description: string, filePath: string) => void;
|
|
103
|
+
/** Remove a registered skill by name. */
|
|
104
|
+
removeSkill: (name: string) => void;
|
|
95
105
|
/** Register a named handler. */
|
|
96
106
|
define: (name: string, fn: (...args: any[]) => any) => void;
|
|
97
107
|
/** Wrap a named handler. Receives `next` (original) + args. Returns an unadvise function. */
|
|
98
108
|
advise: (name: string, wrapper: (next: (...args: any[]) => any, ...args: any[]) => any) => () => void;
|
|
99
109
|
/** Call a named handler. */
|
|
100
110
|
call: (name: string, ...args: any[]) => any;
|
|
111
|
+
/** Names of all registered handlers — for diagnostic / introspection use. */
|
|
112
|
+
list: () => string[];
|
|
101
113
|
/**
|
|
102
114
|
* Shared headless terminal buffer mirroring PTY output.
|
|
103
115
|
* Lazily created on first access. Returns null if @xterm/headless is not installed.
|
|
@@ -146,12 +158,15 @@ export type Exchange = {
|
|
|
146
158
|
timestamp: number;
|
|
147
159
|
cwd: string;
|
|
148
160
|
command: string;
|
|
161
|
+
/** In-context representation: full text if short, head+tail+path stub if spilled. */
|
|
149
162
|
output: string;
|
|
150
163
|
exitCode: number | null;
|
|
151
164
|
outputLines: number;
|
|
152
165
|
outputBytes: number;
|
|
153
166
|
/** Who initiated this command: "user" (typed) or "agent" (via user_shell). */
|
|
154
167
|
source: "user" | "agent";
|
|
168
|
+
/** Path to the tempfile holding the full captured output, if spilled. */
|
|
169
|
+
spillPath?: string;
|
|
155
170
|
} | {
|
|
156
171
|
type: "agent_query";
|
|
157
172
|
id: number;
|
package/dist/utils/ansi.d.ts
CHANGED
|
@@ -6,6 +6,13 @@ export declare const RED = "\u001B[31m";
|
|
|
6
6
|
export declare const GRAY = "\u001B[90m";
|
|
7
7
|
export declare const BOLD = "\u001B[1m";
|
|
8
8
|
export declare const RESET = "\u001B[0m";
|
|
9
|
+
/**
|
|
10
|
+
* Check if a Unicode code point is a wide character (CJK, fullwidth, emoji, etc.)
|
|
11
|
+
* Returns 2 for wide chars, 1 for normal chars, 0 for combining chars.
|
|
12
|
+
*
|
|
13
|
+
* Based on East Asian Width and Unicode categories.
|
|
14
|
+
*/
|
|
15
|
+
export declare function charWidth(codePoint: number): number;
|
|
9
16
|
/**
|
|
10
17
|
* Measure visible string length in terminal columns.
|
|
11
18
|
* Excludes SGR (color/style) sequences and accounts for CJK double-width chars.
|
package/dist/utils/ansi.js
CHANGED
|
@@ -10,9 +10,54 @@ export const RESET = "\x1b[0m";
|
|
|
10
10
|
// ── ANSI utility functions ───────────────────────────────────
|
|
11
11
|
/**
|
|
12
12
|
* Check if a Unicode code point is a wide character (CJK, fullwidth, emoji, etc.)
|
|
13
|
-
* Returns 2 for wide chars, 1 for normal chars.
|
|
13
|
+
* Returns 2 for wide chars, 1 for normal chars, 0 for combining chars.
|
|
14
|
+
*
|
|
15
|
+
* Based on East Asian Width and Unicode categories.
|
|
14
16
|
*/
|
|
15
|
-
function charWidth(codePoint) {
|
|
17
|
+
export function charWidth(codePoint) {
|
|
18
|
+
// Combining characters (zero width)
|
|
19
|
+
if (codePoint >= 0x0300 && codePoint <= 0x036f)
|
|
20
|
+
return 0; // Combining Diacritical Marks
|
|
21
|
+
if (codePoint >= 0x1ab0 && codePoint <= 0x1aff)
|
|
22
|
+
return 0; // Combining Diacritical Marks Extended
|
|
23
|
+
if (codePoint >= 0x1dc0 && codePoint <= 0x1dff)
|
|
24
|
+
return 0; // Combining Diacritical Marks Supplement
|
|
25
|
+
if (codePoint >= 0x20d0 && codePoint <= 0x20ff)
|
|
26
|
+
return 0; // Combining Diacritical Marks for Symbols
|
|
27
|
+
if (codePoint >= 0xfe20 && codePoint <= 0xfe2f)
|
|
28
|
+
return 0; // Combining Half Marks
|
|
29
|
+
if (codePoint >= 0xfe00 && codePoint <= 0xfe0f)
|
|
30
|
+
return 0; // Variation Selectors
|
|
31
|
+
if (codePoint >= 0xe0100 && codePoint <= 0xe01ef)
|
|
32
|
+
return 0; // Variation Selectors Supplement
|
|
33
|
+
// Emoji and symbols that render as wide (2 columns)
|
|
34
|
+
// Emoji presentation sequences and keycap
|
|
35
|
+
if (codePoint === 0x20e3)
|
|
36
|
+
return 2; // Combining Enclosing Keycap
|
|
37
|
+
// Emoji blocks
|
|
38
|
+
if (codePoint >= 0x1f600 && codePoint <= 0x1f64f)
|
|
39
|
+
return 2; // Emoticons
|
|
40
|
+
if (codePoint >= 0x1f300 && codePoint <= 0x1f5ff)
|
|
41
|
+
return 2; // Misc Symbols and Pictographs
|
|
42
|
+
if (codePoint >= 0x1f680 && codePoint <= 0x1f6ff)
|
|
43
|
+
return 2; // Transport and Map
|
|
44
|
+
if (codePoint >= 0x1f700 && codePoint <= 0x1f77f)
|
|
45
|
+
return 2; // Alchemical Symbols
|
|
46
|
+
if (codePoint >= 0x1f780 && codePoint <= 0x1f7ff)
|
|
47
|
+
return 2; // Geometric Shapes Extended
|
|
48
|
+
if (codePoint >= 0x1f800 && codePoint <= 0x1f8ff)
|
|
49
|
+
return 2; // Supplemental Arrows-C
|
|
50
|
+
if (codePoint >= 0x1f900 && codePoint <= 0x1f9ff)
|
|
51
|
+
return 2; // Supplemental Symbols and Pictographs
|
|
52
|
+
if (codePoint >= 0x1fa00 && codePoint <= 0x1faff)
|
|
53
|
+
return 2; // Chess Symbols, Symbols and Pictographs Extended-A
|
|
54
|
+
// NOTE: 0x2300-0x23ff (Misc Technical), 0x2600-0x26ff (Misc Symbols),
|
|
55
|
+
// and 0x2700-0x27bf (Dingbats) are intentionally NOT width 2 — these ranges
|
|
56
|
+
// contain mostly "Ambiguous" width characters that render as 1 column in
|
|
57
|
+
// non-CJK terminal locales (e.g. ❯, ⌘, ★, ♦).
|
|
58
|
+
// Regional indicator symbols (flag emoji components)
|
|
59
|
+
if (codePoint >= 0x1f1e6 && codePoint <= 0x1f1ff)
|
|
60
|
+
return 2;
|
|
16
61
|
// CJK Unified Ideographs
|
|
17
62
|
if (codePoint >= 0x4e00 && codePoint <= 0x9fff)
|
|
18
63
|
return 2;
|
|
@@ -28,7 +73,6 @@ function charWidth(codePoint) {
|
|
|
28
73
|
// Fullwidth ASCII variants
|
|
29
74
|
if (codePoint >= 0xff01 && codePoint <= 0xff5e)
|
|
30
75
|
return 2;
|
|
31
|
-
// Halfwidth Katakana (actually narrow, skip)
|
|
32
76
|
// Fullwidth bracket forms
|
|
33
77
|
if (codePoint >= 0xff5f && codePoint <= 0xff60)
|
|
34
78
|
return 2;
|
|
@@ -76,18 +120,35 @@ export function visibleLen(str) {
|
|
|
76
120
|
*/
|
|
77
121
|
export function truncateToWidth(str, maxWidth) {
|
|
78
122
|
const clean = str.replace(/\x1b\[[^m]*m/g, "");
|
|
123
|
+
if (maxWidth <= 0)
|
|
124
|
+
return "";
|
|
125
|
+
// First check if the entire string fits
|
|
126
|
+
let fullWidth = 0;
|
|
127
|
+
for (const char of clean) {
|
|
128
|
+
fullWidth += charWidth(char.codePointAt(0) ?? 0);
|
|
129
|
+
}
|
|
130
|
+
if (fullWidth <= maxWidth)
|
|
131
|
+
return clean;
|
|
132
|
+
// String doesn't fit — truncate with "…"
|
|
133
|
+
// At maxWidth=1 the ellipsis alone fills the budget.
|
|
134
|
+
if (maxWidth === 1)
|
|
135
|
+
return "…";
|
|
136
|
+
// Reserve 1 column for "…", so target content width is maxWidth - 1
|
|
137
|
+
const target = maxWidth - 1;
|
|
79
138
|
let width = 0;
|
|
80
139
|
let i = 0;
|
|
81
140
|
for (const char of clean) {
|
|
82
141
|
const cw = charWidth(char.codePointAt(0) ?? 0);
|
|
83
|
-
if (width + cw >
|
|
84
|
-
|
|
85
|
-
return clean.slice(0, i) + "…";
|
|
86
|
-
}
|
|
142
|
+
if (width + cw > target)
|
|
143
|
+
break;
|
|
87
144
|
width += cw;
|
|
88
145
|
i += char.length;
|
|
89
146
|
}
|
|
90
|
-
|
|
147
|
+
// If nothing fit (first char is wider than target), just show the ellipsis
|
|
148
|
+
// rather than emit a character that would overflow the budget.
|
|
149
|
+
if (i === 0)
|
|
150
|
+
return "…";
|
|
151
|
+
return clean.slice(0, i) + "…";
|
|
91
152
|
}
|
|
92
153
|
/**
|
|
93
154
|
* Pad a string with spaces to fill `targetWidth` visible columns.
|
package/dist/utils/box-frame.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* never writes to stdout. Supports multiple border styles and
|
|
6
6
|
* optional title/footer sections with dividers.
|
|
7
7
|
*/
|
|
8
|
-
import { visibleLen } from "./ansi.js";
|
|
8
|
+
import { visibleLen, truncateToWidth } from "./ansi.js";
|
|
9
9
|
import { palette as p } from "./palette.js";
|
|
10
10
|
const BORDERS = {
|
|
11
11
|
rounded: { tl: "╭", tr: "╮", bl: "╰", br: "╯", h: "─", v: "│", ml: "├", mr: "┤" },
|
|
@@ -63,6 +63,12 @@ export function renderBoxFrame(content, opts) {
|
|
|
63
63
|
}
|
|
64
64
|
// ── Helpers ──────────────────────────────────────────────────────
|
|
65
65
|
function boxLine(text, innerW, v, bc) {
|
|
66
|
-
const
|
|
66
|
+
const textWidth = visibleLen(text);
|
|
67
|
+
if (textWidth > innerW) {
|
|
68
|
+
// Content is too wide — truncate to fit exactly
|
|
69
|
+
const truncated = truncateToWidth(text, innerW);
|
|
70
|
+
return `${bc}${v}${p.reset} ${truncated} ${bc}${v}${p.reset}`;
|
|
71
|
+
}
|
|
72
|
+
const pad = innerW - textWidth;
|
|
67
73
|
return `${bc}${v}${p.reset} ${text}${" ".repeat(pad)} ${bc}${v}${p.reset}`;
|
|
68
74
|
}
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
* compositor.redirect("agent:diff", diffPanelSurface);
|
|
25
25
|
* // "agent:text", "agent:tool" etc. still go to stdout
|
|
26
26
|
*/
|
|
27
|
+
import type { EventBus } from "../event-bus.js";
|
|
27
28
|
/**
|
|
28
29
|
* A surface accepts rendered output. Stdout is a surface.
|
|
29
30
|
* A floating panel's content area is a surface. A test buffer is a surface.
|
|
@@ -56,7 +57,11 @@ export declare class StdoutSurface implements RenderSurface {
|
|
|
56
57
|
export declare class DefaultCompositor implements Compositor {
|
|
57
58
|
private defaults;
|
|
58
59
|
private overrides;
|
|
60
|
+
private readonly bus?;
|
|
61
|
+
constructor(bus?: EventBus);
|
|
59
62
|
surface(stream: string): RenderSurface;
|
|
60
63
|
redirect(stream: string, target: RenderSurface): () => void;
|
|
61
64
|
setDefault(stream: string, target: RenderSurface): void;
|
|
65
|
+
/** Wrap a surface so writes emit `compositor:write` before delegating. */
|
|
66
|
+
private wrap;
|
|
62
67
|
}
|
package/dist/utils/compositor.js
CHANGED
|
@@ -50,6 +50,10 @@ export class StdoutSurface {
|
|
|
50
50
|
export class DefaultCompositor {
|
|
51
51
|
defaults = new Map();
|
|
52
52
|
overrides = new Map();
|
|
53
|
+
bus;
|
|
54
|
+
constructor(bus) {
|
|
55
|
+
this.bus = bus;
|
|
56
|
+
}
|
|
53
57
|
surface(stream) {
|
|
54
58
|
const stack = this.overrides.get(stream);
|
|
55
59
|
if (stack && stack.length > 0)
|
|
@@ -63,12 +67,13 @@ export class DefaultCompositor {
|
|
|
63
67
|
return nullSurface;
|
|
64
68
|
}
|
|
65
69
|
redirect(stream, target) {
|
|
70
|
+
const wrapped = this.wrap(stream, target);
|
|
66
71
|
let stack = this.overrides.get(stream);
|
|
67
72
|
if (!stack) {
|
|
68
73
|
stack = [];
|
|
69
74
|
this.overrides.set(stream, stack);
|
|
70
75
|
}
|
|
71
|
-
stack.push(
|
|
76
|
+
stack.push(wrapped);
|
|
72
77
|
let restored = false;
|
|
73
78
|
return () => {
|
|
74
79
|
if (restored)
|
|
@@ -77,12 +82,35 @@ export class DefaultCompositor {
|
|
|
77
82
|
const s = this.overrides.get(stream);
|
|
78
83
|
if (!s)
|
|
79
84
|
return;
|
|
80
|
-
const idx = s.indexOf(
|
|
85
|
+
const idx = s.indexOf(wrapped);
|
|
81
86
|
if (idx !== -1)
|
|
82
87
|
s.splice(idx, 1);
|
|
83
88
|
};
|
|
84
89
|
}
|
|
85
90
|
setDefault(stream, target) {
|
|
86
|
-
this.defaults.set(stream, target);
|
|
91
|
+
this.defaults.set(stream, this.wrap(stream, target));
|
|
92
|
+
}
|
|
93
|
+
/** Wrap a surface so writes emit `compositor:write` before delegating. */
|
|
94
|
+
wrap(stream, target) {
|
|
95
|
+
const bus = this.bus;
|
|
96
|
+
if (!bus)
|
|
97
|
+
return target;
|
|
98
|
+
return {
|
|
99
|
+
write: (text) => {
|
|
100
|
+
try {
|
|
101
|
+
bus.emit("compositor:write", { stream, text });
|
|
102
|
+
}
|
|
103
|
+
catch { }
|
|
104
|
+
target.write(text);
|
|
105
|
+
},
|
|
106
|
+
writeLine: (line) => {
|
|
107
|
+
try {
|
|
108
|
+
bus.emit("compositor:write", { stream, text: line + "\n" });
|
|
109
|
+
}
|
|
110
|
+
catch { }
|
|
111
|
+
target.writeLine(line);
|
|
112
|
+
},
|
|
113
|
+
get columns() { return target.columns; },
|
|
114
|
+
};
|
|
87
115
|
}
|
|
88
116
|
}
|
|
@@ -18,3 +18,12 @@ export interface DiffRenderOptions {
|
|
|
18
18
|
export declare function selectMode(width: number): DiffDisplayMode;
|
|
19
19
|
/** Render a diff result as an array of ANSI-formatted terminal lines. */
|
|
20
20
|
export declare function renderDiff(diff: DiffResult, opts: DiffRenderOptions): string[];
|
|
21
|
+
/**
|
|
22
|
+
* Async variant of renderDiff that yields to the event loop between hunks.
|
|
23
|
+
* Use when rendering in a context where a spinner or other UI needs to stay
|
|
24
|
+
* responsive (e.g. showing a large diff during a permission prompt).
|
|
25
|
+
*
|
|
26
|
+
* @param onLines - Callback invoked with each batch of rendered lines as they
|
|
27
|
+
* are produced. Allows progressive/streaming display.
|
|
28
|
+
*/
|
|
29
|
+
export declare function renderDiffAsync(diff: DiffResult, opts: DiffRenderOptions, onLines: (lines: string[]) => void): Promise<void>;
|