agent-sh 0.2.0 → 0.3.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 +21 -0
- package/dist/acp-client.d.ts +24 -0
- package/dist/acp-client.js +155 -33
- package/dist/context-manager.d.ts +5 -3
- package/dist/context-manager.js +62 -31
- package/dist/core.js +10 -0
- package/dist/event-bus.d.ts +26 -0
- package/dist/event-bus.js +10 -0
- package/dist/extension-loader.js +3 -14
- package/dist/extensions/shell-exec.js +27 -22
- package/dist/extensions/tui-renderer.d.ts +1 -1
- package/dist/extensions/tui-renderer.js +369 -126
- package/dist/index.js +184 -37
- package/dist/input-handler.d.ts +10 -0
- package/dist/input-handler.js +169 -10
- package/dist/mcp-server.js +37 -8
- package/dist/settings.d.ts +44 -0
- package/dist/settings.js +61 -0
- package/dist/shell.d.ts +1 -0
- package/dist/shell.js +44 -4
- package/dist/types.d.ts +17 -0
- package/dist/utils/ansi.d.ts +4 -1
- package/dist/utils/ansi.js +60 -2
- 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.d.ts +21 -1
- package/dist/utils/line-editor.js +193 -99
- 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 +11 -8
- package/dist/utils/tool-display.js +69 -46
- package/examples/extensions/latex-images.ts +142 -0
- package/examples/pi-agent-sh.ts +166 -0
- package/package.json +10 -2
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
3
|
import { Shell } from "./shell.js";
|
|
4
4
|
import { createCore } from "./core.js";
|
|
5
5
|
import { palette as p } from "./utils/palette.js";
|
|
@@ -10,35 +10,63 @@ import shellRecall from "./extensions/shell-recall.js";
|
|
|
10
10
|
import shellExec from "./extensions/shell-exec.js";
|
|
11
11
|
import { loadExtensions } from "./extension-loader.js";
|
|
12
12
|
/**
|
|
13
|
-
* Capture the user's full shell environment
|
|
14
|
-
*
|
|
15
|
-
* Node.js process
|
|
16
|
-
*
|
|
13
|
+
* Capture the user's full shell environment asynchronously.
|
|
14
|
+
* This picks up env vars exported in .zshrc/.bashrc that the
|
|
15
|
+
* Node.js process doesn't have.
|
|
16
|
+
*
|
|
17
|
+
* Uses -l (login shell) instead of -i to avoid TTY blocking issues.
|
|
17
18
|
*/
|
|
18
|
-
function
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
19
|
+
async function captureShellEnvAsync(shell) {
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
try {
|
|
22
|
+
const child = spawn(shell, ["-l", "-c", "env -0"], {
|
|
23
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
24
|
+
timeout: 5000,
|
|
25
|
+
});
|
|
26
|
+
let output = "";
|
|
27
|
+
child.stdout?.on("data", (data) => {
|
|
28
|
+
output += data.toString("utf-8");
|
|
29
|
+
});
|
|
30
|
+
child.on("close", (code) => {
|
|
31
|
+
if (code !== 0 || !output) {
|
|
32
|
+
resolve({}); // Return empty to trigger fallback
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const env = {};
|
|
36
|
+
for (const entry of output.split("\0")) {
|
|
37
|
+
const eq = entry.indexOf("=");
|
|
38
|
+
if (eq > 0)
|
|
39
|
+
env[entry.slice(0, eq)] = entry.slice(eq + 1);
|
|
40
|
+
}
|
|
41
|
+
resolve(env);
|
|
42
|
+
});
|
|
43
|
+
child.on("error", () => {
|
|
44
|
+
resolve({}); // Return empty to trigger fallback
|
|
45
|
+
});
|
|
46
|
+
// Safety timeout
|
|
47
|
+
setTimeout(() => {
|
|
48
|
+
child.kill("SIGTERM");
|
|
49
|
+
resolve({});
|
|
50
|
+
}, 5000);
|
|
30
51
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
52
|
+
catch {
|
|
53
|
+
resolve({});
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Merge captured shell env into base env, only adding keys that don't exist.
|
|
59
|
+
* This preserves any runtime modifications while adding missing shell vars.
|
|
60
|
+
*/
|
|
61
|
+
function mergeShellEnv(baseEnv, shellEnv) {
|
|
62
|
+
const merged = { ...baseEnv };
|
|
63
|
+
for (const [key, value] of Object.entries(shellEnv)) {
|
|
64
|
+
// Only add if key doesn't exist or is empty in base env
|
|
65
|
+
if (!(key in merged) || !merged[key]) {
|
|
66
|
+
merged[key] = value;
|
|
39
67
|
}
|
|
40
|
-
return env;
|
|
41
68
|
}
|
|
69
|
+
return merged;
|
|
42
70
|
}
|
|
43
71
|
function parseArgs(argv) {
|
|
44
72
|
// Priority: CLI args > Environment variables > Config file > Defaults
|
|
@@ -111,7 +139,7 @@ Inside the shell:
|
|
|
111
139
|
}
|
|
112
140
|
return { agentCommand, agentArgs, shell, model, extensions };
|
|
113
141
|
}
|
|
114
|
-
function formatAgentInfo(agentInfo, model) {
|
|
142
|
+
function formatAgentInfo(agentInfo, model, thoughtLevel) {
|
|
115
143
|
const name = agentInfo.name.replace(/-acp$/, "").replace(/-/g, " ");
|
|
116
144
|
let infoStr = `${p.dim}${name}${p.reset}`;
|
|
117
145
|
if (model) {
|
|
@@ -121,17 +149,51 @@ function formatAgentInfo(agentInfo, model) {
|
|
|
121
149
|
.replace(/^google\//i, "");
|
|
122
150
|
infoStr += ` ${p.dim}(${cleanModel})${p.reset}`;
|
|
123
151
|
}
|
|
152
|
+
if (thoughtLevel) {
|
|
153
|
+
// Clean up verbose mode names like "Thinking: medium" → "medium"
|
|
154
|
+
const label = thoughtLevel.replace(/^Thinking:\s*/i, "");
|
|
155
|
+
infoStr += ` ${p.dim}[${label}]${p.reset}`;
|
|
156
|
+
}
|
|
124
157
|
return `${infoStr} ${p.success}●${p.reset}`;
|
|
125
158
|
}
|
|
126
159
|
async function main() {
|
|
160
|
+
// Set up signal handlers before any terminal operations.
|
|
161
|
+
// Ignore SIGTTOU to prevent suspension when modifying terminal settings.
|
|
162
|
+
process.on("SIGTTOU", () => { });
|
|
163
|
+
// Also ignore SIGTTIN which can occur when reading from terminal while backgrounded.
|
|
164
|
+
process.on("SIGTTIN", () => { });
|
|
127
165
|
const config = parseArgs(process.argv.slice(2));
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
|
|
166
|
+
// Start with current process environment (fast, non-blocking)
|
|
167
|
+
// We'll enrich it with shell env asynchronously in the background
|
|
168
|
+
const baseEnv = {};
|
|
169
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
170
|
+
if (v !== undefined)
|
|
171
|
+
baseEnv[k] = v;
|
|
172
|
+
}
|
|
173
|
+
config.shellEnv = baseEnv;
|
|
174
|
+
// Asynchronously capture full shell environment without blocking startup
|
|
175
|
+
const shellPath = config.shell || process.env.SHELL || "/bin/bash";
|
|
176
|
+
captureShellEnvAsync(shellPath).then((shellEnv) => {
|
|
177
|
+
if (Object.keys(shellEnv).length > 0) {
|
|
178
|
+
const merged = mergeShellEnv(config.shellEnv, shellEnv);
|
|
179
|
+
config.shellEnv = merged;
|
|
180
|
+
if (process.env.DEBUG) {
|
|
181
|
+
console.error('[agent-sh] Shell environment enriched asynchronously');
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}).catch(() => {
|
|
185
|
+
// 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
|
+
}
|
|
131
190
|
// ── Core (frontend-agnostic) ──────────────────────────────────
|
|
132
191
|
const core = createCore(config);
|
|
133
192
|
const { bus, client } = core;
|
|
134
193
|
// ── Interactive frontend ──────────────────────────────────────
|
|
194
|
+
if (process.env.DEBUG) {
|
|
195
|
+
console.error('[agent-sh] Setting up interactive frontend...');
|
|
196
|
+
}
|
|
135
197
|
process.stdout.write(`\x1b]0;agent-sh\x07`);
|
|
136
198
|
const cols = process.stdout.columns || 80;
|
|
137
199
|
const rows = process.stdout.rows || 24;
|
|
@@ -143,6 +205,12 @@ async function main() {
|
|
|
143
205
|
}
|
|
144
206
|
process.exit(0);
|
|
145
207
|
};
|
|
208
|
+
if (process.env.DEBUG) {
|
|
209
|
+
console.error('[agent-sh] Creating Shell...');
|
|
210
|
+
}
|
|
211
|
+
// Small delay on macOS to ensure we're fully in the foreground process group
|
|
212
|
+
// before spawning the PTY. This prevents SIGTTOU suspension.
|
|
213
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
146
214
|
const shell = new Shell({
|
|
147
215
|
bus,
|
|
148
216
|
cols,
|
|
@@ -154,32 +222,86 @@ async function main() {
|
|
|
154
222
|
const agentInfo = client.getAgentInfo();
|
|
155
223
|
const model = client.getModel();
|
|
156
224
|
if (agentInfo) {
|
|
157
|
-
|
|
225
|
+
const mode = client.getCurrentMode();
|
|
226
|
+
return { info: formatAgentInfo(agentInfo, model, mode?.name ?? null) };
|
|
158
227
|
}
|
|
159
228
|
}
|
|
160
229
|
return { info: "" };
|
|
161
230
|
},
|
|
162
231
|
});
|
|
232
|
+
if (process.env.DEBUG) {
|
|
233
|
+
console.error('[agent-sh] Shell created');
|
|
234
|
+
}
|
|
163
235
|
// ── Extensions ────────────────────────────────────────────────
|
|
236
|
+
if (process.env.DEBUG) {
|
|
237
|
+
console.error('[agent-sh] Setting up extensions...');
|
|
238
|
+
}
|
|
164
239
|
const extCtx = core.extensionContext({ quit: cleanup });
|
|
165
240
|
tuiRenderer(extCtx);
|
|
166
241
|
slashCommands(extCtx);
|
|
167
242
|
fileAutocomplete(extCtx);
|
|
168
243
|
shellRecall(extCtx);
|
|
169
|
-
// Shell-exec: start the Unix socket bridge so
|
|
170
|
-
// route
|
|
244
|
+
// Shell-exec: start the Unix domain socket bridge so agent extensions
|
|
245
|
+
// and MCP servers can route tool calls to the PTY via the EventBus.
|
|
171
246
|
const tmpDir = shell.getTmpDir();
|
|
172
247
|
if (tmpDir) {
|
|
248
|
+
if (process.env.DEBUG) {
|
|
249
|
+
console.error('[agent-sh] Starting shell-exec socket server...');
|
|
250
|
+
}
|
|
173
251
|
shellExec(extCtx, { socketPath: `${tmpDir}/shell.sock` });
|
|
174
252
|
}
|
|
175
|
-
|
|
253
|
+
// Load extensions with timeout to prevent blocking startup
|
|
254
|
+
if (process.env.DEBUG) {
|
|
255
|
+
console.error('[agent-sh] Loading extensions...');
|
|
256
|
+
}
|
|
257
|
+
const loadExtensionsTimeoutMs = 10000; // 10 seconds
|
|
258
|
+
await Promise.race([
|
|
259
|
+
loadExtensions(extCtx, config.extensions),
|
|
260
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`Extension loading timeout after ${loadExtensionsTimeoutMs}ms`)), loadExtensionsTimeoutMs)),
|
|
261
|
+
]).catch((err) => {
|
|
262
|
+
console.error(`Warning: ${err.message}`);
|
|
263
|
+
});
|
|
264
|
+
if (process.env.DEBUG) {
|
|
265
|
+
console.error('[agent-sh] Extensions loaded');
|
|
266
|
+
}
|
|
176
267
|
// ── Agent connection (async — don't block shell startup) ──────
|
|
177
|
-
|
|
268
|
+
const agentStartTimeoutMs = 35000; // 35 seconds (slightly longer than internal timeouts)
|
|
269
|
+
Promise.race([
|
|
270
|
+
core.start(),
|
|
271
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`Agent connection timeout`)), agentStartTimeoutMs)),
|
|
272
|
+
]).catch((err) => {
|
|
178
273
|
console.error(`Failed to connect to ${config.agentCommand}:`, err);
|
|
179
274
|
});
|
|
180
275
|
// ── Terminal lifecycle ────────────────────────────────────────
|
|
181
276
|
process.on("SIGTERM", cleanup);
|
|
182
277
|
process.on("SIGHUP", cleanup);
|
|
278
|
+
// Handle terminal stop/resume signals properly
|
|
279
|
+
process.on("SIGTSTP", () => {
|
|
280
|
+
// Handle Ctrl+Z - suspend the entire process group
|
|
281
|
+
// Restore terminal state before suspending
|
|
282
|
+
if (process.stdin.isTTY) {
|
|
283
|
+
try {
|
|
284
|
+
process.stdin.setRawMode(false);
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
// Ignore
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// Re-send SIGSTOP to actually suspend
|
|
291
|
+
process.kill(process.pid, "SIGSTOP");
|
|
292
|
+
});
|
|
293
|
+
process.on("SIGCONT", () => {
|
|
294
|
+
// Re-acquire terminal when brought back to foreground
|
|
295
|
+
if (process.stdin.isTTY) {
|
|
296
|
+
try {
|
|
297
|
+
// Ensure we reacquire controlling terminal
|
|
298
|
+
process.stdin.setRawMode(true);
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
// May fail if stdin is not a TTY
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
});
|
|
183
305
|
process.stdout.on("resize", () => {
|
|
184
306
|
shell.resize(process.stdout.columns || 80, process.stdout.rows || 24);
|
|
185
307
|
});
|
|
@@ -190,10 +312,35 @@ async function main() {
|
|
|
190
312
|
}
|
|
191
313
|
process.exit(e.exitCode);
|
|
192
314
|
});
|
|
193
|
-
|
|
194
|
-
|
|
315
|
+
// Set up stdin - resume after all event listeners are in place
|
|
316
|
+
if (process.env.DEBUG) {
|
|
317
|
+
console.error('[agent-sh] Resuming stdin...');
|
|
195
318
|
}
|
|
196
319
|
process.stdin.resume();
|
|
320
|
+
// Set raw mode after resume to avoid SIGTTOU issues
|
|
321
|
+
if (process.stdin.isTTY) {
|
|
322
|
+
if (process.env.DEBUG) {
|
|
323
|
+
console.error('[agent-sh] Setting raw mode...');
|
|
324
|
+
}
|
|
325
|
+
// Use setImmediate to ensure we're in the next tick
|
|
326
|
+
setImmediate(() => {
|
|
327
|
+
try {
|
|
328
|
+
process.stdin.setRawMode(true);
|
|
329
|
+
if (process.env.DEBUG) {
|
|
330
|
+
console.error('[agent-sh] Raw mode enabled');
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
catch (err) {
|
|
334
|
+
if (process.env.DEBUG) {
|
|
335
|
+
console.error(`[agent-sh] Failed to set raw mode: ${err}`);
|
|
336
|
+
}
|
|
337
|
+
// May fail if process is in background; SIGTTOU handler prevents suspension
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
if (process.env.DEBUG) {
|
|
342
|
+
console.error('[agent-sh] Startup complete');
|
|
343
|
+
}
|
|
197
344
|
}
|
|
198
345
|
main().catch((err) => {
|
|
199
346
|
console.error("Fatal:", err);
|
package/dist/input-handler.d.ts
CHANGED
|
@@ -22,6 +22,11 @@ export declare class InputHandler {
|
|
|
22
22
|
private autocompleteIndex;
|
|
23
23
|
private autocompleteItems;
|
|
24
24
|
private autocompleteLines;
|
|
25
|
+
private history;
|
|
26
|
+
private historyIndex;
|
|
27
|
+
private savedBuffer;
|
|
28
|
+
private promptWrappedLines;
|
|
29
|
+
private escapeTimer;
|
|
25
30
|
private bus;
|
|
26
31
|
private onShowAgentInfo;
|
|
27
32
|
constructor(opts: {
|
|
@@ -32,11 +37,15 @@ export declare class InputHandler {
|
|
|
32
37
|
model?: string;
|
|
33
38
|
};
|
|
34
39
|
});
|
|
40
|
+
private loadHistory;
|
|
41
|
+
private saveHistory;
|
|
35
42
|
/** Write the agent prompt line with cursor at the correct position. */
|
|
36
43
|
private writeAgentPromptLine;
|
|
37
44
|
handleInput(data: string): void;
|
|
38
45
|
private enterAgentInputMode;
|
|
39
46
|
private exitAgentInputMode;
|
|
47
|
+
/** Move to the start of the prompt area and clear everything below. */
|
|
48
|
+
private clearPromptArea;
|
|
40
49
|
printPrompt(): void;
|
|
41
50
|
private renderAgentInput;
|
|
42
51
|
private updateAutocomplete;
|
|
@@ -45,4 +54,5 @@ export declare class InputHandler {
|
|
|
45
54
|
private dismissAutocomplete;
|
|
46
55
|
private clearAutocompleteLines;
|
|
47
56
|
private handleAgentInput;
|
|
57
|
+
private processAgentActions;
|
|
48
58
|
}
|
package/dist/input-handler.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
1
3
|
import { visibleLen } from "./utils/ansi.js";
|
|
2
4
|
import { palette as p } from "./utils/palette.js";
|
|
3
5
|
import { LineEditor } from "./utils/line-editor.js";
|
|
6
|
+
import { CONFIG_DIR, getSettings } from "./settings.js";
|
|
7
|
+
const HISTORY_FILE = path.join(CONFIG_DIR, "history");
|
|
4
8
|
export class InputHandler {
|
|
5
9
|
ctx;
|
|
6
10
|
lineBuffer = "";
|
|
@@ -10,24 +14,105 @@ export class InputHandler {
|
|
|
10
14
|
autocompleteIndex = 0;
|
|
11
15
|
autocompleteItems = [];
|
|
12
16
|
autocompleteLines = 0;
|
|
17
|
+
history = [];
|
|
18
|
+
historyIndex = -1; // -1 = not browsing history
|
|
19
|
+
savedBuffer = ""; // buffer saved when entering history
|
|
20
|
+
promptWrappedLines = 0; // extra lines from terminal wrapping
|
|
21
|
+
escapeTimer = null;
|
|
13
22
|
bus;
|
|
14
23
|
onShowAgentInfo;
|
|
15
24
|
constructor(opts) {
|
|
16
25
|
this.ctx = opts.ctx;
|
|
17
26
|
this.bus = opts.bus;
|
|
18
27
|
this.onShowAgentInfo = opts.onShowAgentInfo;
|
|
28
|
+
this.loadHistory();
|
|
29
|
+
// Re-render prompt when config changes (e.g. thinking level cycled)
|
|
30
|
+
this.bus.on("config:changed", () => {
|
|
31
|
+
if (this.agentInputMode)
|
|
32
|
+
this.writeAgentPromptLine();
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
loadHistory() {
|
|
36
|
+
try {
|
|
37
|
+
const data = fs.readFileSync(HISTORY_FILE, "utf-8");
|
|
38
|
+
this.history = data.split("\n").filter(Boolean);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// No history file yet
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
saveHistory() {
|
|
45
|
+
try {
|
|
46
|
+
const { historySize } = getSettings();
|
|
47
|
+
fs.mkdirSync(path.dirname(HISTORY_FILE), { recursive: true });
|
|
48
|
+
const lines = this.history.slice(-historySize);
|
|
49
|
+
fs.writeFileSync(HISTORY_FILE, lines.join("\n") + "\n");
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// Non-critical — ignore write failures
|
|
53
|
+
}
|
|
19
54
|
}
|
|
20
55
|
/** Write the agent prompt line with cursor at the correct position. */
|
|
21
56
|
writeAgentPromptLine(showBuffer = true) {
|
|
57
|
+
const termW = process.stdout.columns || 80;
|
|
58
|
+
// Move cursor to the start of the prompt area (first line of wrapped content)
|
|
59
|
+
if (this.promptWrappedLines > 0) {
|
|
60
|
+
process.stdout.write(`\x1b[${this.promptWrappedLines}A`);
|
|
61
|
+
}
|
|
62
|
+
// Clear from here to end of screen — removes current + all wrapped lines below
|
|
63
|
+
process.stdout.write("\r\x1b[J");
|
|
22
64
|
const agentInfo = this.onShowAgentInfo();
|
|
23
65
|
const infoPrefix = agentInfo.info ? `${agentInfo.info} ` : "";
|
|
24
66
|
const promptPrefix = infoPrefix + p.warning + p.bold + "❯ " + p.reset;
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
67
|
+
const promptVisLen = visibleLen(infoPrefix) + 2; // "❯ "
|
|
68
|
+
if (!showBuffer || !this.editor.buffer.includes("\n")) {
|
|
69
|
+
// Single-line: simple rendering
|
|
70
|
+
const bufferText = showBuffer ? p.accent + this.editor.buffer + p.reset : "";
|
|
71
|
+
process.stdout.write(promptPrefix + bufferText);
|
|
72
|
+
const bufferVisLen = showBuffer ? this.editor.buffer.length : 0;
|
|
73
|
+
const totalVisLen = promptVisLen + bufferVisLen;
|
|
74
|
+
this.promptWrappedLines = totalVisLen > 0 ? Math.floor((totalVisLen - 1) / termW) : 0;
|
|
75
|
+
// Position cursor within the buffer
|
|
76
|
+
if (showBuffer && this.editor.cursor < this.editor.buffer.length) {
|
|
77
|
+
const charsAfterCursor = this.editor.buffer.length - this.editor.cursor;
|
|
78
|
+
process.stdout.write(`\x1b[${charsAfterCursor}D`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
// Multi-line: render each line with continuation indent
|
|
83
|
+
const lines = this.editor.buffer.split("\n");
|
|
84
|
+
const indent = " ".repeat(promptVisLen);
|
|
85
|
+
let totalTermLines = 0;
|
|
86
|
+
for (let li = 0; li < lines.length; li++) {
|
|
87
|
+
const prefix = li === 0 ? promptPrefix : indent;
|
|
88
|
+
const prefixVisLen = li === 0 ? promptVisLen : promptVisLen;
|
|
89
|
+
const lineText = lines[li];
|
|
90
|
+
process.stdout.write(prefix + p.accent + lineText + p.reset);
|
|
91
|
+
if (li < lines.length - 1)
|
|
92
|
+
process.stdout.write("\n");
|
|
93
|
+
// Count terminal lines this logical line occupies
|
|
94
|
+
const lineVisLen = prefixVisLen + lineText.length;
|
|
95
|
+
totalTermLines += lineVisLen > 0 ? Math.ceil(lineVisLen / termW) : 1;
|
|
96
|
+
}
|
|
97
|
+
this.promptWrappedLines = totalTermLines - 1;
|
|
98
|
+
// Position cursor: find which line and column the cursor is on
|
|
99
|
+
let charsRemaining = this.editor.cursor;
|
|
100
|
+
let cursorLine = 0;
|
|
101
|
+
for (let li = 0; li < lines.length; li++) {
|
|
102
|
+
if (charsRemaining <= lines[li].length) {
|
|
103
|
+
cursorLine = li;
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
charsRemaining -= lines[li].length + 1; // +1 for \n
|
|
107
|
+
cursorLine = li + 1;
|
|
108
|
+
}
|
|
109
|
+
// Move from end position to cursor position
|
|
110
|
+
const linesFromEnd = lines.length - 1 - cursorLine;
|
|
111
|
+
if (linesFromEnd > 0) {
|
|
112
|
+
process.stdout.write(`\x1b[${linesFromEnd}A`);
|
|
113
|
+
}
|
|
114
|
+
const cursorCol = (cursorLine === 0 ? promptVisLen : promptVisLen) + charsRemaining;
|
|
115
|
+
process.stdout.write(`\r\x1b[${cursorCol}C`);
|
|
31
116
|
}
|
|
32
117
|
}
|
|
33
118
|
handleInput(data) {
|
|
@@ -41,10 +126,15 @@ export class InputHandler {
|
|
|
41
126
|
}
|
|
42
127
|
return;
|
|
43
128
|
}
|
|
44
|
-
//
|
|
129
|
+
// Intercept control chars for TUI (Ctrl+T, Ctrl+O) — don't pass to PTY
|
|
45
130
|
if (data.length === 1 && data.charCodeAt(0) < 32 && !this.agentInputMode) {
|
|
46
131
|
const code = data.charCodeAt(0);
|
|
47
|
-
//
|
|
132
|
+
// Keys consumed by TUI extensions
|
|
133
|
+
if (code === 0x14 || code === 0x0f) { // Ctrl+T, Ctrl+O
|
|
134
|
+
this.bus.emit("input:keypress", { key: data });
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
// Forward other control chars that shell mode doesn't handle
|
|
48
138
|
if (code !== 0x0d && code !== 0x03 && code !== 0x04 && code !== 0x09) {
|
|
49
139
|
this.bus.emit("input:keypress", { key: data });
|
|
50
140
|
}
|
|
@@ -95,15 +185,28 @@ export class InputHandler {
|
|
|
95
185
|
enterAgentInputMode() {
|
|
96
186
|
this.agentInputMode = true;
|
|
97
187
|
this.editor.clear();
|
|
188
|
+
// Enable kitty keyboard protocol (progressive enhancement flag 1)
|
|
189
|
+
// so Shift+Enter sends \x1b[13;2u instead of plain \r
|
|
190
|
+
process.stdout.write("\x1b[>1u");
|
|
98
191
|
this.writeAgentPromptLine(false);
|
|
99
192
|
}
|
|
100
193
|
exitAgentInputMode() {
|
|
101
194
|
this.dismissAutocomplete();
|
|
102
195
|
this.agentInputMode = false;
|
|
103
196
|
this.editor.clear();
|
|
104
|
-
|
|
197
|
+
// Disable kitty keyboard protocol
|
|
198
|
+
process.stdout.write("\x1b[<u");
|
|
199
|
+
this.clearPromptArea();
|
|
105
200
|
this.printPrompt();
|
|
106
201
|
}
|
|
202
|
+
/** Move to the start of the prompt area and clear everything below. */
|
|
203
|
+
clearPromptArea() {
|
|
204
|
+
if (this.promptWrappedLines > 0) {
|
|
205
|
+
process.stdout.write(`\x1b[${this.promptWrappedLines}A`);
|
|
206
|
+
}
|
|
207
|
+
process.stdout.write("\r\x1b[J");
|
|
208
|
+
this.promptWrappedLines = 0;
|
|
209
|
+
}
|
|
107
210
|
printPrompt() {
|
|
108
211
|
this.ctx.redrawPrompt();
|
|
109
212
|
}
|
|
@@ -197,10 +300,29 @@ export class InputHandler {
|
|
|
197
300
|
this.autocompleteLines = 0;
|
|
198
301
|
}
|
|
199
302
|
handleAgentInput(data) {
|
|
303
|
+
// Clear any pending escape timer — new data arrived
|
|
304
|
+
if (this.escapeTimer) {
|
|
305
|
+
clearTimeout(this.escapeTimer);
|
|
306
|
+
this.escapeTimer = null;
|
|
307
|
+
}
|
|
200
308
|
const actions = this.editor.feed(data);
|
|
309
|
+
// If the editor is waiting for more escape sequence data, set a short
|
|
310
|
+
// timer — if nothing arrives, treat it as a bare Escape keypress
|
|
311
|
+
if (this.editor.hasPendingEscape()) {
|
|
312
|
+
this.escapeTimer = setTimeout(() => {
|
|
313
|
+
this.escapeTimer = null;
|
|
314
|
+
const flushed = this.editor.flushPendingEscape();
|
|
315
|
+
if (flushed.length > 0)
|
|
316
|
+
this.processAgentActions(flushed);
|
|
317
|
+
}, 50);
|
|
318
|
+
}
|
|
319
|
+
this.processAgentActions(actions);
|
|
320
|
+
}
|
|
321
|
+
processAgentActions(actions) {
|
|
201
322
|
for (const act of actions) {
|
|
202
323
|
switch (act.action) {
|
|
203
324
|
case "changed":
|
|
325
|
+
this.historyIndex = -1;
|
|
204
326
|
this.autocompleteIndex = 0;
|
|
205
327
|
this.renderAgentInput();
|
|
206
328
|
break;
|
|
@@ -209,8 +331,18 @@ export class InputHandler {
|
|
|
209
331
|
this.applyAutocomplete();
|
|
210
332
|
}
|
|
211
333
|
const query = act.buffer.trim();
|
|
334
|
+
if (query) {
|
|
335
|
+
// Add to history (avoid consecutive duplicates)
|
|
336
|
+
if (this.history.length === 0 || this.history[this.history.length - 1] !== query) {
|
|
337
|
+
this.history.push(query);
|
|
338
|
+
this.saveHistory();
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
this.historyIndex = -1;
|
|
342
|
+
this.savedBuffer = "";
|
|
212
343
|
this.clearAutocompleteLines();
|
|
213
|
-
|
|
344
|
+
this.clearPromptArea();
|
|
345
|
+
process.stdout.write("\x1b[<u"); // disable kitty keyboard protocol
|
|
214
346
|
this.agentInputMode = false;
|
|
215
347
|
this.editor.clear();
|
|
216
348
|
this.dismissAutocomplete();
|
|
@@ -247,6 +379,9 @@ export class InputHandler {
|
|
|
247
379
|
this.applyAutocomplete();
|
|
248
380
|
}
|
|
249
381
|
break;
|
|
382
|
+
case "shift+tab":
|
|
383
|
+
this.bus.emit("config:cycle", {});
|
|
384
|
+
break;
|
|
250
385
|
case "arrow-up":
|
|
251
386
|
if (this.autocompleteActive) {
|
|
252
387
|
this.autocompleteIndex =
|
|
@@ -257,6 +392,18 @@ export class InputHandler {
|
|
|
257
392
|
this.writeAgentPromptLine();
|
|
258
393
|
this.renderAutocomplete();
|
|
259
394
|
}
|
|
395
|
+
else if (this.history.length > 0) {
|
|
396
|
+
if (this.historyIndex === -1) {
|
|
397
|
+
this.savedBuffer = this.editor.buffer;
|
|
398
|
+
this.historyIndex = this.history.length - 1;
|
|
399
|
+
}
|
|
400
|
+
else if (this.historyIndex > 0) {
|
|
401
|
+
this.historyIndex--;
|
|
402
|
+
}
|
|
403
|
+
this.editor.buffer = this.history[this.historyIndex];
|
|
404
|
+
this.editor.cursor = this.editor.buffer.length;
|
|
405
|
+
this.renderAgentInput();
|
|
406
|
+
}
|
|
260
407
|
break;
|
|
261
408
|
case "arrow-down":
|
|
262
409
|
if (this.autocompleteActive) {
|
|
@@ -268,6 +415,18 @@ export class InputHandler {
|
|
|
268
415
|
this.writeAgentPromptLine();
|
|
269
416
|
this.renderAutocomplete();
|
|
270
417
|
}
|
|
418
|
+
else if (this.historyIndex !== -1) {
|
|
419
|
+
if (this.historyIndex < this.history.length - 1) {
|
|
420
|
+
this.historyIndex++;
|
|
421
|
+
this.editor.buffer = this.history[this.historyIndex];
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
this.historyIndex = -1;
|
|
425
|
+
this.editor.buffer = this.savedBuffer;
|
|
426
|
+
}
|
|
427
|
+
this.editor.cursor = this.editor.buffer.length;
|
|
428
|
+
this.renderAgentInput();
|
|
429
|
+
}
|
|
271
430
|
break;
|
|
272
431
|
}
|
|
273
432
|
}
|