agent-sh 0.3.0 → 0.4.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 +28 -11
- package/dist/acp-client.d.ts +6 -1
- package/dist/acp-client.js +68 -24
- package/dist/core.js +12 -2
- package/dist/event-bus.d.ts +26 -0
- package/dist/event-bus.js +10 -0
- package/dist/extensions/tui-renderer.d.ts +1 -1
- package/dist/extensions/tui-renderer.js +325 -165
- package/dist/index.js +44 -16
- package/dist/input-handler.d.ts +17 -8
- package/dist/input-handler.js +79 -39
- package/dist/settings.d.ts +11 -0
- package/dist/settings.js +19 -1
- package/dist/shell.js +3 -1
- package/dist/types.d.ts +28 -0
- package/dist/utils/box-frame.js +2 -1
- package/dist/utils/diff-renderer.js +1 -1
- package/dist/utils/frame-renderer.d.ts +26 -0
- package/dist/utils/frame-renderer.js +76 -0
- package/dist/utils/handler-registry.d.ts +41 -0
- package/dist/utils/handler-registry.js +52 -0
- package/dist/utils/line-editor.js +4 -0
- package/dist/utils/markdown.d.ts +15 -6
- package/dist/utils/markdown.js +106 -67
- package/dist/utils/output-writer.d.ts +22 -0
- package/dist/utils/output-writer.js +29 -0
- package/dist/utils/stream-transform.d.ts +70 -0
- package/dist/utils/stream-transform.js +229 -0
- package/dist/utils/tool-display.d.ts +9 -8
- package/dist/utils/tool-display.js +26 -31
- package/examples/extensions/latex-images.ts +142 -0
- package/package.json +10 -2
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
+
import * as path from "node:path";
|
|
3
4
|
import { Shell } from "./shell.js";
|
|
4
5
|
import { createCore } from "./core.js";
|
|
5
6
|
import { palette as p } from "./utils/palette.js";
|
|
@@ -10,16 +11,23 @@ import shellRecall from "./extensions/shell-recall.js";
|
|
|
10
11
|
import shellExec from "./extensions/shell-exec.js";
|
|
11
12
|
import { loadExtensions } from "./extension-loader.js";
|
|
12
13
|
/**
|
|
13
|
-
* Capture the user's full shell environment
|
|
14
|
+
* Capture the user's full shell environment.
|
|
14
15
|
* This picks up env vars exported in .zshrc/.bashrc that the
|
|
15
|
-
* Node.js process doesn't have.
|
|
16
|
+
* Node.js process doesn't have (e.g. when launched from an IDE).
|
|
16
17
|
*
|
|
17
|
-
* Uses -l (login shell)
|
|
18
|
+
* Uses -l (login shell) to get .zprofile/.bash_profile vars, then
|
|
19
|
+
* explicitly sources the interactive rc file (.zshrc/.bashrc) which
|
|
20
|
+
* -l alone doesn't load (that requires -i, which blocks on TTY).
|
|
18
21
|
*/
|
|
19
22
|
async function captureShellEnvAsync(shell) {
|
|
20
23
|
return new Promise((resolve) => {
|
|
21
24
|
try {
|
|
22
|
-
const
|
|
25
|
+
const shellName = path.basename(shell);
|
|
26
|
+
const isZsh = shellName.includes("zsh");
|
|
27
|
+
const sourceRc = isZsh
|
|
28
|
+
? 'source ~/.zshrc 2>/dev/null;'
|
|
29
|
+
: '[ -f ~/.bashrc ] && source ~/.bashrc 2>/dev/null;';
|
|
30
|
+
const child = spawn(shell, ["-l", "-c", `${sourceRc} env -0`], {
|
|
23
31
|
stdio: ["ignore", "pipe", "ignore"],
|
|
24
32
|
timeout: 5000,
|
|
25
33
|
});
|
|
@@ -154,7 +162,7 @@ function formatAgentInfo(agentInfo, model, thoughtLevel) {
|
|
|
154
162
|
const label = thoughtLevel.replace(/^Thinking:\s*/i, "");
|
|
155
163
|
infoStr += ` ${p.dim}[${label}]${p.reset}`;
|
|
156
164
|
}
|
|
157
|
-
return
|
|
165
|
+
return infoStr;
|
|
158
166
|
}
|
|
159
167
|
async function main() {
|
|
160
168
|
// Set up signal handlers before any terminal operations.
|
|
@@ -163,29 +171,26 @@ async function main() {
|
|
|
163
171
|
// Also ignore SIGTTIN which can occur when reading from terminal while backgrounded.
|
|
164
172
|
process.on("SIGTTIN", () => { });
|
|
165
173
|
const config = parseArgs(process.argv.slice(2));
|
|
166
|
-
//
|
|
167
|
-
//
|
|
174
|
+
// Capture user's full shell environment (from .zshrc/.bashrc etc.)
|
|
175
|
+
// This must complete before spawning the agent so it sees all env vars.
|
|
168
176
|
const baseEnv = {};
|
|
169
177
|
for (const [k, v] of Object.entries(process.env)) {
|
|
170
178
|
if (v !== undefined)
|
|
171
179
|
baseEnv[k] = v;
|
|
172
180
|
}
|
|
173
181
|
config.shellEnv = baseEnv;
|
|
174
|
-
// Asynchronously capture full shell environment without blocking startup
|
|
175
182
|
const shellPath = config.shell || process.env.SHELL || "/bin/bash";
|
|
176
|
-
|
|
183
|
+
try {
|
|
184
|
+
const shellEnv = await captureShellEnvAsync(shellPath);
|
|
177
185
|
if (Object.keys(shellEnv).length > 0) {
|
|
178
|
-
|
|
179
|
-
config.shellEnv = merged;
|
|
186
|
+
config.shellEnv = mergeShellEnv(config.shellEnv, shellEnv);
|
|
180
187
|
if (process.env.DEBUG) {
|
|
181
|
-
console.error('[agent-sh] Shell environment
|
|
188
|
+
console.error('[agent-sh] Shell environment captured');
|
|
182
189
|
}
|
|
183
190
|
}
|
|
184
|
-
}
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
185
193
|
// Ignore errors, we already have process.env as fallback
|
|
186
|
-
});
|
|
187
|
-
if (process.env.DEBUG) {
|
|
188
|
-
console.error('[agent-sh] Using current process environment (async enrichment pending)');
|
|
189
194
|
}
|
|
190
195
|
// ── Core (frontend-agnostic) ──────────────────────────────────
|
|
191
196
|
const core = createCore(config);
|
|
@@ -232,6 +237,29 @@ async function main() {
|
|
|
232
237
|
if (process.env.DEBUG) {
|
|
233
238
|
console.error('[agent-sh] Shell created');
|
|
234
239
|
}
|
|
240
|
+
// ── Input modes ──────────────────────────────────────────────
|
|
241
|
+
bus.emit("input-mode:register", {
|
|
242
|
+
id: "query",
|
|
243
|
+
trigger: "?",
|
|
244
|
+
label: "query",
|
|
245
|
+
promptIcon: "❯",
|
|
246
|
+
indicator: "❓",
|
|
247
|
+
onSubmit(query, b) {
|
|
248
|
+
b.emit("agent:submit", { query, modeLabel: "Query", modeInstruction: "[mode: query]" });
|
|
249
|
+
},
|
|
250
|
+
returnToSelf: true,
|
|
251
|
+
});
|
|
252
|
+
bus.emit("input-mode:register", {
|
|
253
|
+
id: "execute",
|
|
254
|
+
trigger: ">",
|
|
255
|
+
label: "execute",
|
|
256
|
+
promptIcon: "⟩",
|
|
257
|
+
indicator: "●",
|
|
258
|
+
onSubmit(query, b) {
|
|
259
|
+
b.emit("agent:submit", { query, modeLabel: "Execute", modeInstruction: "[mode: execute]" });
|
|
260
|
+
},
|
|
261
|
+
returnToSelf: false,
|
|
262
|
+
});
|
|
235
263
|
// ── Extensions ────────────────────────────────────────────────
|
|
236
264
|
if (process.env.DEBUG) {
|
|
237
265
|
console.error('[agent-sh] Setting up extensions...');
|
package/dist/input-handler.d.ts
CHANGED
|
@@ -16,7 +16,10 @@ export interface InputContext {
|
|
|
16
16
|
export declare class InputHandler {
|
|
17
17
|
private ctx;
|
|
18
18
|
private lineBuffer;
|
|
19
|
-
private
|
|
19
|
+
private activeMode;
|
|
20
|
+
private pendingReturnMode;
|
|
21
|
+
private modes;
|
|
22
|
+
private modesById;
|
|
20
23
|
private editor;
|
|
21
24
|
private autocompleteActive;
|
|
22
25
|
private autocompleteIndex;
|
|
@@ -37,22 +40,28 @@ export declare class InputHandler {
|
|
|
37
40
|
model?: string;
|
|
38
41
|
};
|
|
39
42
|
});
|
|
43
|
+
private registerMode;
|
|
40
44
|
private loadHistory;
|
|
41
45
|
private saveHistory;
|
|
42
|
-
/** Write the
|
|
43
|
-
private
|
|
46
|
+
/** Write the mode prompt line with cursor at the correct position. */
|
|
47
|
+
private writeModePromptLine;
|
|
44
48
|
handleInput(data: string): void;
|
|
45
|
-
private
|
|
46
|
-
private
|
|
49
|
+
private enterMode;
|
|
50
|
+
private exitMode;
|
|
47
51
|
/** Move to the start of the prompt area and clear everything below. */
|
|
48
52
|
private clearPromptArea;
|
|
49
53
|
printPrompt(): void;
|
|
50
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Called when agent processing completes. Returns true if the input
|
|
56
|
+
* handler re-entered a mode (so caller should skip shell prompt).
|
|
57
|
+
*/
|
|
58
|
+
handleProcessingDone(): boolean;
|
|
59
|
+
private renderModeInput;
|
|
51
60
|
private updateAutocomplete;
|
|
52
61
|
private renderAutocomplete;
|
|
53
62
|
private applyAutocomplete;
|
|
54
63
|
private dismissAutocomplete;
|
|
55
64
|
private clearAutocompleteLines;
|
|
56
|
-
private
|
|
57
|
-
private
|
|
65
|
+
private handleModeInput;
|
|
66
|
+
private processModeActions;
|
|
58
67
|
}
|
package/dist/input-handler.js
CHANGED
|
@@ -8,7 +8,10 @@ const HISTORY_FILE = path.join(CONFIG_DIR, "history");
|
|
|
8
8
|
export class InputHandler {
|
|
9
9
|
ctx;
|
|
10
10
|
lineBuffer = "";
|
|
11
|
-
|
|
11
|
+
activeMode = null;
|
|
12
|
+
pendingReturnMode = null; // mode id to return to after processing
|
|
13
|
+
modes = new Map(); // keyed by trigger char
|
|
14
|
+
modesById = new Map(); // keyed by id
|
|
12
15
|
editor = new LineEditor();
|
|
13
16
|
autocompleteActive = false;
|
|
14
17
|
autocompleteIndex = 0;
|
|
@@ -28,9 +31,23 @@ export class InputHandler {
|
|
|
28
31
|
this.loadHistory();
|
|
29
32
|
// Re-render prompt when config changes (e.g. thinking level cycled)
|
|
30
33
|
this.bus.on("config:changed", () => {
|
|
31
|
-
if (this.
|
|
32
|
-
this.
|
|
34
|
+
if (this.activeMode)
|
|
35
|
+
this.writeModePromptLine();
|
|
33
36
|
});
|
|
37
|
+
// Listen for mode registrations from extensions
|
|
38
|
+
this.bus.on("input-mode:register", (config) => {
|
|
39
|
+
this.registerMode(config);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
registerMode(config) {
|
|
43
|
+
if (this.modes.has(config.trigger)) {
|
|
44
|
+
this.bus.emit("ui:error", {
|
|
45
|
+
message: `Input mode "${config.id}" cannot register trigger "${config.trigger}" — already taken by "${this.modes.get(config.trigger).id}"`,
|
|
46
|
+
});
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
this.modes.set(config.trigger, config);
|
|
50
|
+
this.modesById.set(config.id, config);
|
|
34
51
|
}
|
|
35
52
|
loadHistory() {
|
|
36
53
|
try {
|
|
@@ -52,8 +69,8 @@ export class InputHandler {
|
|
|
52
69
|
// Non-critical — ignore write failures
|
|
53
70
|
}
|
|
54
71
|
}
|
|
55
|
-
/** Write the
|
|
56
|
-
|
|
72
|
+
/** Write the mode prompt line with cursor at the correct position. */
|
|
73
|
+
writeModePromptLine(showBuffer = true) {
|
|
57
74
|
const termW = process.stdout.columns || 80;
|
|
58
75
|
// Move cursor to the start of the prompt area (first line of wrapped content)
|
|
59
76
|
if (this.promptWrappedLines > 0) {
|
|
@@ -62,9 +79,13 @@ export class InputHandler {
|
|
|
62
79
|
// Clear from here to end of screen — removes current + all wrapped lines below
|
|
63
80
|
process.stdout.write("\r\x1b[J");
|
|
64
81
|
const agentInfo = this.onShowAgentInfo();
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
|
|
82
|
+
const indicator = this.activeMode?.indicator ?? "●";
|
|
83
|
+
const infoPrefix = agentInfo.info
|
|
84
|
+
? `${agentInfo.info} ${p.success}${indicator}${p.reset} `
|
|
85
|
+
: `${p.success}${indicator}${p.reset} `;
|
|
86
|
+
const icon = this.activeMode?.promptIcon ?? "❯";
|
|
87
|
+
const promptPrefix = infoPrefix + p.warning + p.bold + icon + " " + p.reset;
|
|
88
|
+
const promptVisLen = visibleLen(infoPrefix) + visibleLen(icon) + 1; // icon + space
|
|
68
89
|
if (!showBuffer || !this.editor.buffer.includes("\n")) {
|
|
69
90
|
// Single-line: simple rendering
|
|
70
91
|
const bufferText = showBuffer ? p.accent + this.editor.buffer + p.reset : "";
|
|
@@ -127,7 +148,7 @@ export class InputHandler {
|
|
|
127
148
|
return;
|
|
128
149
|
}
|
|
129
150
|
// Intercept control chars for TUI (Ctrl+T, Ctrl+O) — don't pass to PTY
|
|
130
|
-
if (data.length === 1 && data.charCodeAt(0) < 32 && !this.
|
|
151
|
+
if (data.length === 1 && data.charCodeAt(0) < 32 && !this.activeMode) {
|
|
131
152
|
const code = data.charCodeAt(0);
|
|
132
153
|
// Keys consumed by TUI extensions
|
|
133
154
|
if (code === 0x14 || code === 0x0f) { // Ctrl+T, Ctrl+O
|
|
@@ -139,9 +160,9 @@ export class InputHandler {
|
|
|
139
160
|
this.bus.emit("input:keypress", { key: data });
|
|
140
161
|
}
|
|
141
162
|
}
|
|
142
|
-
// If in
|
|
143
|
-
if (this.
|
|
144
|
-
this.
|
|
163
|
+
// If in an input mode (typing a query)
|
|
164
|
+
if (this.activeMode) {
|
|
165
|
+
this.handleModeInput(data);
|
|
145
166
|
return;
|
|
146
167
|
}
|
|
147
168
|
for (let i = 0; i < data.length; i++) {
|
|
@@ -171,10 +192,11 @@ export class InputHandler {
|
|
|
171
192
|
this.ctx.writeToPty(ch);
|
|
172
193
|
}
|
|
173
194
|
else {
|
|
174
|
-
// Check if
|
|
195
|
+
// Check if trigger char at start of empty line → enter that mode
|
|
175
196
|
// But not if a foreground process (ssh, vim, etc.) is running
|
|
176
|
-
|
|
177
|
-
|
|
197
|
+
const mode = this.modes.get(ch);
|
|
198
|
+
if (this.lineBuffer === "" && mode && !this.ctx.isForegroundBusy()) {
|
|
199
|
+
this.enterMode(mode);
|
|
178
200
|
return; // don't process remaining chars
|
|
179
201
|
}
|
|
180
202
|
this.lineBuffer += ch;
|
|
@@ -182,17 +204,17 @@ export class InputHandler {
|
|
|
182
204
|
}
|
|
183
205
|
}
|
|
184
206
|
}
|
|
185
|
-
|
|
186
|
-
this.
|
|
207
|
+
enterMode(mode) {
|
|
208
|
+
this.activeMode = mode;
|
|
187
209
|
this.editor.clear();
|
|
188
210
|
// Enable kitty keyboard protocol (progressive enhancement flag 1)
|
|
189
211
|
// so Shift+Enter sends \x1b[13;2u instead of plain \r
|
|
190
212
|
process.stdout.write("\x1b[>1u");
|
|
191
|
-
this.
|
|
213
|
+
this.writeModePromptLine(false);
|
|
192
214
|
}
|
|
193
|
-
|
|
215
|
+
exitMode() {
|
|
194
216
|
this.dismissAutocomplete();
|
|
195
|
-
this.
|
|
217
|
+
this.activeMode = null;
|
|
196
218
|
this.editor.clear();
|
|
197
219
|
// Disable kitty keyboard protocol
|
|
198
220
|
process.stdout.write("\x1b[<u");
|
|
@@ -210,9 +232,24 @@ export class InputHandler {
|
|
|
210
232
|
printPrompt() {
|
|
211
233
|
this.ctx.redrawPrompt();
|
|
212
234
|
}
|
|
213
|
-
|
|
235
|
+
/**
|
|
236
|
+
* Called when agent processing completes. Returns true if the input
|
|
237
|
+
* handler re-entered a mode (so caller should skip shell prompt).
|
|
238
|
+
*/
|
|
239
|
+
handleProcessingDone() {
|
|
240
|
+
if (this.pendingReturnMode) {
|
|
241
|
+
const mode = this.modesById.get(this.pendingReturnMode);
|
|
242
|
+
this.pendingReturnMode = null;
|
|
243
|
+
if (mode) {
|
|
244
|
+
this.enterMode(mode);
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
renderModeInput() {
|
|
214
251
|
this.clearAutocompleteLines();
|
|
215
|
-
this.
|
|
252
|
+
this.writeModePromptLine();
|
|
216
253
|
this.updateAutocomplete();
|
|
217
254
|
}
|
|
218
255
|
updateAutocomplete() {
|
|
@@ -254,7 +291,8 @@ export class InputHandler {
|
|
|
254
291
|
}
|
|
255
292
|
const agentInfo = this.onShowAgentInfo();
|
|
256
293
|
const infoLength = visibleLen(agentInfo.info);
|
|
257
|
-
const
|
|
294
|
+
const icon = this.activeMode?.promptIcon ?? "❯";
|
|
295
|
+
const col = infoLength + visibleLen(icon) + 1 + this.editor.cursor;
|
|
258
296
|
process.stdout.write(`\r\x1b[${col}C`);
|
|
259
297
|
}
|
|
260
298
|
applyAutocomplete() {
|
|
@@ -279,7 +317,7 @@ export class InputHandler {
|
|
|
279
317
|
this.autocompleteActive = false;
|
|
280
318
|
this.autocompleteItems = [];
|
|
281
319
|
this.autocompleteIndex = 0;
|
|
282
|
-
this.
|
|
320
|
+
this.writeModePromptLine();
|
|
283
321
|
if (isFileAc)
|
|
284
322
|
this.updateAutocomplete();
|
|
285
323
|
}
|
|
@@ -299,7 +337,7 @@ export class InputHandler {
|
|
|
299
337
|
process.stdout.write("\x1b8"); // restore cursor
|
|
300
338
|
this.autocompleteLines = 0;
|
|
301
339
|
}
|
|
302
|
-
|
|
340
|
+
handleModeInput(data) {
|
|
303
341
|
// Clear any pending escape timer — new data arrived
|
|
304
342
|
if (this.escapeTimer) {
|
|
305
343
|
clearTimeout(this.escapeTimer);
|
|
@@ -313,18 +351,18 @@ export class InputHandler {
|
|
|
313
351
|
this.escapeTimer = null;
|
|
314
352
|
const flushed = this.editor.flushPendingEscape();
|
|
315
353
|
if (flushed.length > 0)
|
|
316
|
-
this.
|
|
354
|
+
this.processModeActions(flushed);
|
|
317
355
|
}, 50);
|
|
318
356
|
}
|
|
319
|
-
this.
|
|
357
|
+
this.processModeActions(actions);
|
|
320
358
|
}
|
|
321
|
-
|
|
359
|
+
processModeActions(actions) {
|
|
322
360
|
for (const act of actions) {
|
|
323
361
|
switch (act.action) {
|
|
324
362
|
case "changed":
|
|
325
363
|
this.historyIndex = -1;
|
|
326
364
|
this.autocompleteIndex = 0;
|
|
327
|
-
this.
|
|
365
|
+
this.renderModeInput();
|
|
328
366
|
break;
|
|
329
367
|
case "submit": {
|
|
330
368
|
if (this.autocompleteActive) {
|
|
@@ -343,7 +381,8 @@ export class InputHandler {
|
|
|
343
381
|
this.clearAutocompleteLines();
|
|
344
382
|
this.clearPromptArea();
|
|
345
383
|
process.stdout.write("\x1b[<u"); // disable kitty keyboard protocol
|
|
346
|
-
|
|
384
|
+
const currentMode = this.activeMode;
|
|
385
|
+
this.activeMode = null;
|
|
347
386
|
this.editor.clear();
|
|
348
387
|
this.dismissAutocomplete();
|
|
349
388
|
if (query && query.startsWith("/")) {
|
|
@@ -354,25 +393,26 @@ export class InputHandler {
|
|
|
354
393
|
this.ctx.redrawPrompt();
|
|
355
394
|
}
|
|
356
395
|
else if (query) {
|
|
357
|
-
this.
|
|
396
|
+
this.pendingReturnMode = currentMode.returnToSelf ? currentMode.id : null;
|
|
397
|
+
currentMode.onSubmit(query, this.bus);
|
|
358
398
|
}
|
|
359
399
|
else {
|
|
360
|
-
this.
|
|
400
|
+
this.exitMode();
|
|
361
401
|
}
|
|
362
402
|
return;
|
|
363
403
|
}
|
|
364
404
|
case "cancel":
|
|
365
405
|
if (this.autocompleteActive) {
|
|
366
406
|
this.dismissAutocomplete();
|
|
367
|
-
this.
|
|
407
|
+
this.writeModePromptLine();
|
|
368
408
|
}
|
|
369
409
|
else {
|
|
370
|
-
this.
|
|
410
|
+
this.exitMode();
|
|
371
411
|
}
|
|
372
412
|
return;
|
|
373
413
|
case "delete-empty":
|
|
374
414
|
this.dismissAutocomplete();
|
|
375
|
-
this.
|
|
415
|
+
this.exitMode();
|
|
376
416
|
return;
|
|
377
417
|
case "tab":
|
|
378
418
|
if (this.autocompleteActive) {
|
|
@@ -389,7 +429,7 @@ export class InputHandler {
|
|
|
389
429
|
? this.autocompleteItems.length - 1
|
|
390
430
|
: this.autocompleteIndex - 1;
|
|
391
431
|
this.clearAutocompleteLines();
|
|
392
|
-
this.
|
|
432
|
+
this.writeModePromptLine();
|
|
393
433
|
this.renderAutocomplete();
|
|
394
434
|
}
|
|
395
435
|
else if (this.history.length > 0) {
|
|
@@ -402,7 +442,7 @@ export class InputHandler {
|
|
|
402
442
|
}
|
|
403
443
|
this.editor.buffer = this.history[this.historyIndex];
|
|
404
444
|
this.editor.cursor = this.editor.buffer.length;
|
|
405
|
-
this.
|
|
445
|
+
this.renderModeInput();
|
|
406
446
|
}
|
|
407
447
|
break;
|
|
408
448
|
case "arrow-down":
|
|
@@ -412,7 +452,7 @@ export class InputHandler {
|
|
|
412
452
|
? 0
|
|
413
453
|
: this.autocompleteIndex + 1;
|
|
414
454
|
this.clearAutocompleteLines();
|
|
415
|
-
this.
|
|
455
|
+
this.writeModePromptLine();
|
|
416
456
|
this.renderAutocomplete();
|
|
417
457
|
}
|
|
418
458
|
else if (this.historyIndex !== -1) {
|
|
@@ -425,7 +465,7 @@ export class InputHandler {
|
|
|
425
465
|
this.editor.buffer = this.savedBuffer;
|
|
426
466
|
}
|
|
427
467
|
this.editor.cursor = this.editor.buffer.length;
|
|
428
|
-
this.
|
|
468
|
+
this.renderModeInput();
|
|
429
469
|
}
|
|
430
470
|
break;
|
|
431
471
|
}
|
package/dist/settings.d.ts
CHANGED
|
@@ -28,6 +28,17 @@ export interface Settings {
|
|
|
28
28
|
declare const DEFAULTS: Required<Settings>;
|
|
29
29
|
/** Load settings from disk (cached after first call). */
|
|
30
30
|
export declare function getSettings(): Settings & typeof DEFAULTS;
|
|
31
|
+
/**
|
|
32
|
+
* Get settings for an extension, namespaced under its key in settings.json.
|
|
33
|
+
*
|
|
34
|
+
* Example settings.json:
|
|
35
|
+
* { "latex-images": { "dpi": 600, "fgColor": "ffffff" } }
|
|
36
|
+
*
|
|
37
|
+
* Usage in extension:
|
|
38
|
+
* const config = getExtensionSettings("latex-images", { dpi: 300, fgColor: "d4d4d4" });
|
|
39
|
+
* // config.dpi === 600 (overridden), config.fgColor === "ffffff" (overridden)
|
|
40
|
+
*/
|
|
41
|
+
export declare function getExtensionSettings<T extends Record<string, unknown>>(namespace: string, defaults: T): T;
|
|
31
42
|
/** Reset cached settings (for testing or after external edit). */
|
|
32
43
|
export declare function reloadSettings(): void;
|
|
33
44
|
export {};
|
package/dist/settings.js
CHANGED
|
@@ -18,7 +18,7 @@ const DEFAULTS = {
|
|
|
18
18
|
shellHeadLines: 5,
|
|
19
19
|
shellTailLines: 5,
|
|
20
20
|
recallExpandMaxLines: 100,
|
|
21
|
-
maxCommandOutputLines:
|
|
21
|
+
maxCommandOutputLines: 3,
|
|
22
22
|
readOutputMaxLines: 0,
|
|
23
23
|
diffMaxLines: 20,
|
|
24
24
|
enableMcp: true,
|
|
@@ -37,6 +37,24 @@ export function getSettings() {
|
|
|
37
37
|
}
|
|
38
38
|
return { ...DEFAULTS, ...cached };
|
|
39
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Get settings for an extension, namespaced under its key in settings.json.
|
|
42
|
+
*
|
|
43
|
+
* Example settings.json:
|
|
44
|
+
* { "latex-images": { "dpi": 600, "fgColor": "ffffff" } }
|
|
45
|
+
*
|
|
46
|
+
* Usage in extension:
|
|
47
|
+
* const config = getExtensionSettings("latex-images", { dpi: 300, fgColor: "d4d4d4" });
|
|
48
|
+
* // config.dpi === 600 (overridden), config.fgColor === "ffffff" (overridden)
|
|
49
|
+
*/
|
|
50
|
+
export function getExtensionSettings(namespace, defaults) {
|
|
51
|
+
const all = getSettings();
|
|
52
|
+
const ext = all[namespace];
|
|
53
|
+
if (ext && typeof ext === "object" && !Array.isArray(ext)) {
|
|
54
|
+
return { ...defaults, ...ext };
|
|
55
|
+
}
|
|
56
|
+
return defaults;
|
|
57
|
+
}
|
|
40
58
|
/** Reset cached settings (for testing or after external edit). */
|
|
41
59
|
export function reloadSettings() {
|
|
42
60
|
cached = null;
|
package/dist/shell.js
CHANGED
|
@@ -239,7 +239,9 @@ export class Shell {
|
|
|
239
239
|
this.paused = false;
|
|
240
240
|
this.agentActive = false;
|
|
241
241
|
this.echoSkip = true;
|
|
242
|
-
this.
|
|
242
|
+
if (!this.inputHandler.handleProcessingDone()) {
|
|
243
|
+
this.freshPrompt();
|
|
244
|
+
}
|
|
243
245
|
});
|
|
244
246
|
// Permission prompts need stdout unpaused so the interactive UI renders,
|
|
245
247
|
// then re-paused after the decision.
|
package/dist/types.d.ts
CHANGED
|
@@ -2,6 +2,9 @@ import type { EventBus } from "./event-bus.js";
|
|
|
2
2
|
import type { ContextManager } from "./context-manager.js";
|
|
3
3
|
import type { AcpClient } from "./acp-client.js";
|
|
4
4
|
import type { ColorPalette } from "./utils/palette.js";
|
|
5
|
+
import type { BlockTransformOptions, FencedBlockTransformOptions } from "./utils/stream-transform.js";
|
|
6
|
+
export type { ContentBlock } from "./event-bus.js";
|
|
7
|
+
export type { BlockTransformOptions, FencedBlockTransformOptions } from "./utils/stream-transform.js";
|
|
5
8
|
export interface AgentShellConfig {
|
|
6
9
|
agentCommand: string;
|
|
7
10
|
agentArgs: string[];
|
|
@@ -24,6 +27,31 @@ export interface ExtensionContext {
|
|
|
24
27
|
quit: () => void;
|
|
25
28
|
/** Override color palette slots for theming. */
|
|
26
29
|
setPalette: (overrides: Partial<ColorPalette>) => void;
|
|
30
|
+
/** Register a delimiter-based content transform (e.g. $$...$$ → image). */
|
|
31
|
+
createBlockTransform: (opts: BlockTransformOptions) => void;
|
|
32
|
+
/** Register a fenced block transform (e.g. ```lang...``` → code-block). */
|
|
33
|
+
createFencedBlockTransform: (opts: FencedBlockTransformOptions) => void;
|
|
34
|
+
/** Read extension-namespaced settings from ~/.agent-sh/settings.json. */
|
|
35
|
+
getExtensionSettings: <T extends Record<string, unknown>>(namespace: string, defaults: T) => T;
|
|
36
|
+
/** Register a named handler. */
|
|
37
|
+
define: (name: string, fn: (...args: any[]) => any) => void;
|
|
38
|
+
/** Wrap a named handler. Receives `next` (original) + args. */
|
|
39
|
+
advise: (name: string, wrapper: (next: (...args: any[]) => any, ...args: any[]) => any) => void;
|
|
40
|
+
/** Call a named handler. */
|
|
41
|
+
call: (name: string, ...args: any[]) => any;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Configuration for a registered input mode.
|
|
45
|
+
* Extensions emit "input-mode:register" with this shape to add new modes.
|
|
46
|
+
*/
|
|
47
|
+
export interface InputModeConfig {
|
|
48
|
+
id: string;
|
|
49
|
+
trigger: string;
|
|
50
|
+
label: string;
|
|
51
|
+
promptIcon: string;
|
|
52
|
+
indicator: string;
|
|
53
|
+
onSubmit(query: string, bus: EventBus): void;
|
|
54
|
+
returnToSelf: boolean;
|
|
27
55
|
}
|
|
28
56
|
export interface TerminalSession {
|
|
29
57
|
id: string;
|
package/dist/utils/box-frame.js
CHANGED
|
@@ -22,7 +22,8 @@ const BORDERS = {
|
|
|
22
22
|
* @returns Array of terminal-ready lines with borders
|
|
23
23
|
*/
|
|
24
24
|
export function renderBoxFrame(content, opts) {
|
|
25
|
-
const { width, borderColor = p.dim } = opts;
|
|
25
|
+
const { width: rawWidth, borderColor = p.dim } = opts;
|
|
26
|
+
const width = Math.max(6, rawWidth);
|
|
26
27
|
const style = opts.style ?? "rounded";
|
|
27
28
|
const b = BORDERS[style];
|
|
28
29
|
const bc = borderColor;
|
|
@@ -320,7 +320,7 @@ function renderSplit(diff, opts) {
|
|
|
320
320
|
const lang = useSyntax ? detectLanguage(opts.filePath) : undefined;
|
|
321
321
|
const totalWidth = opts.width;
|
|
322
322
|
// 3 chars for " │ " separator
|
|
323
|
-
const colWidth = Math.floor((totalWidth - 3) / 2);
|
|
323
|
+
const colWidth = Math.max(1, Math.floor((totalWidth - 3) / 2));
|
|
324
324
|
// Compute max line number width
|
|
325
325
|
let maxNo = 0;
|
|
326
326
|
for (const hunk of diff.hunks) {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Differential frame renderer.
|
|
3
|
+
*
|
|
4
|
+
* Accepts a frame (string[]) and writes only the lines that changed
|
|
5
|
+
* compared to the previous frame. Designed for scrolling content
|
|
6
|
+
* (not full-screen ownership like pi-tui).
|
|
7
|
+
*
|
|
8
|
+
* Fast paths:
|
|
9
|
+
* 1. First render → write everything
|
|
10
|
+
* 2. Append-only → write only new lines
|
|
11
|
+
* 3. Last line changed → \r overwrite (for spinner / partial streaming)
|
|
12
|
+
* 4. General diff → cursor-up, rewrite changed region, cursor-down
|
|
13
|
+
*/
|
|
14
|
+
import type { OutputWriter } from "./output-writer.js";
|
|
15
|
+
export declare class FrameRenderer {
|
|
16
|
+
private writer;
|
|
17
|
+
private prevLines;
|
|
18
|
+
constructor(writer: OutputWriter);
|
|
19
|
+
/**
|
|
20
|
+
* Render a new frame, writing only the diff to the output.
|
|
21
|
+
* Each line in `lines` should NOT include a trailing newline.
|
|
22
|
+
*/
|
|
23
|
+
update(lines: string[]): void;
|
|
24
|
+
/** Reset state — next update will be treated as a first render. */
|
|
25
|
+
reset(): void;
|
|
26
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export class FrameRenderer {
|
|
2
|
+
writer;
|
|
3
|
+
prevLines = [];
|
|
4
|
+
constructor(writer) {
|
|
5
|
+
this.writer = writer;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Render a new frame, writing only the diff to the output.
|
|
9
|
+
* Each line in `lines` should NOT include a trailing newline.
|
|
10
|
+
*/
|
|
11
|
+
update(lines) {
|
|
12
|
+
const prev = this.prevLines;
|
|
13
|
+
if (prev.length === 0) {
|
|
14
|
+
// Fast path 1: first render
|
|
15
|
+
for (const line of lines) {
|
|
16
|
+
this.writer.write(line + "\n");
|
|
17
|
+
}
|
|
18
|
+
this.prevLines = lines.slice();
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
// Find first and last changed indices
|
|
22
|
+
const minLen = Math.min(prev.length, lines.length);
|
|
23
|
+
let firstChanged = -1;
|
|
24
|
+
let lastChanged = -1;
|
|
25
|
+
for (let i = 0; i < minLen; i++) {
|
|
26
|
+
if (prev[i] !== lines[i]) {
|
|
27
|
+
if (firstChanged === -1)
|
|
28
|
+
firstChanged = i;
|
|
29
|
+
lastChanged = i;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// Check for appended or removed lines
|
|
33
|
+
const appended = lines.length > prev.length;
|
|
34
|
+
const truncated = lines.length < prev.length;
|
|
35
|
+
if (firstChanged === -1 && !appended && !truncated) {
|
|
36
|
+
// No changes at all
|
|
37
|
+
this.prevLines = lines.slice();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (firstChanged === -1 && appended) {
|
|
41
|
+
// Fast path 2: only new lines appended, existing unchanged
|
|
42
|
+
for (let i = prev.length; i < lines.length; i++) {
|
|
43
|
+
this.writer.write(lines[i] + "\n");
|
|
44
|
+
}
|
|
45
|
+
this.prevLines = lines.slice();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
// General diff: move cursor up to first changed line, rewrite
|
|
49
|
+
const linesFromBottom = prev.length - (firstChanged === -1 ? prev.length : firstChanged);
|
|
50
|
+
if (linesFromBottom > 0) {
|
|
51
|
+
this.writer.write(`\x1b[${linesFromBottom}A`); // cursor up
|
|
52
|
+
}
|
|
53
|
+
this.writer.write("\r"); // start of line
|
|
54
|
+
// Rewrite from firstChanged to end of new frame
|
|
55
|
+
const start = firstChanged === -1 ? prev.length : firstChanged;
|
|
56
|
+
for (let i = start; i < lines.length; i++) {
|
|
57
|
+
this.writer.write(`\x1b[2K${lines[i]}\n`); // clear line + write + newline
|
|
58
|
+
}
|
|
59
|
+
// If new frame is shorter, clear remaining old lines
|
|
60
|
+
if (truncated) {
|
|
61
|
+
for (let i = lines.length; i < prev.length; i++) {
|
|
62
|
+
this.writer.write("\x1b[2K\n");
|
|
63
|
+
}
|
|
64
|
+
// Move cursor back up to end of new content
|
|
65
|
+
const extra = prev.length - lines.length;
|
|
66
|
+
if (extra > 0) {
|
|
67
|
+
this.writer.write(`\x1b[${extra}A`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
this.prevLines = lines.slice();
|
|
71
|
+
}
|
|
72
|
+
/** Reset state — next update will be treated as a first render. */
|
|
73
|
+
reset() {
|
|
74
|
+
this.prevLines = [];
|
|
75
|
+
}
|
|
76
|
+
}
|