agent-sh 0.12.2 → 0.12.4
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/dist/agent/agent-loop.js +40 -7
- package/dist/agent/skills.js +2 -2
- package/dist/agent/system-prompt.js +2 -3
- package/dist/core.js +4 -3
- package/dist/event-bus.d.ts +46 -0
- package/dist/event-bus.js +51 -3
- package/dist/extension-loader.js +1 -0
- package/dist/extensions/agent-backend.js +4 -1
- package/dist/extensions/openrouter.js +32 -0
- package/dist/init.js +1 -2
- package/dist/settings.d.ts +8 -0
- package/dist/settings.js +7 -3
- package/dist/shell/input-handler.d.ts +8 -18
- package/dist/shell/input-handler.js +57 -227
- package/dist/shell/shell.js +1 -1
- package/dist/shell/tui-input-view.d.ts +37 -0
- package/dist/shell/tui-input-view.js +140 -0
- package/dist/types.d.ts +6 -0
- package/dist/utils/compositor.d.ts +7 -1
- package/dist/utils/compositor.js +13 -1
- package/dist/utils/floating-panel.d.ts +6 -2
- package/dist/utils/floating-panel.js +17 -17
- package/dist/utils/ref-counter.d.ts +9 -0
- package/dist/utils/ref-counter.js +9 -0
- package/package.json +1 -1
- package/dist/utils/frame-renderer.d.ts +0 -26
- package/dist/utils/frame-renderer.js +0 -76
- package/dist/utils/output-writer.d.ts +0 -36
- package/dist/utils/output-writer.js +0 -45
|
@@ -1,41 +1,38 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { visibleLen } from "../utils/ansi.js";
|
|
4
|
-
import { palette as p } from "../utils/palette.js";
|
|
5
3
|
import { LineEditor } from "../utils/line-editor.js";
|
|
6
4
|
import { CONFIG_DIR, getSettings } from "../settings.js";
|
|
5
|
+
import { TuiInputView } from "./tui-input-view.js";
|
|
7
6
|
const HISTORY_FILE = path.join(CONFIG_DIR, "input-history");
|
|
7
|
+
/** Line editor + shell-passthrough buffer. Delegates rendering to TuiInputView. */
|
|
8
8
|
export class InputHandler {
|
|
9
9
|
ctx;
|
|
10
10
|
lineBuffer = "";
|
|
11
11
|
activeMode = null;
|
|
12
|
-
pendingReturnMode = null;
|
|
13
|
-
modes = new Map();
|
|
14
|
-
modesById = new Map();
|
|
12
|
+
pendingReturnMode = null;
|
|
13
|
+
modes = new Map();
|
|
14
|
+
modesById = new Map();
|
|
15
15
|
editor = new LineEditor();
|
|
16
16
|
autocompleteActive = false;
|
|
17
17
|
autocompleteIndex = 0;
|
|
18
18
|
autocompleteItems = [];
|
|
19
|
-
autocompleteLines = 0;
|
|
20
19
|
history = [];
|
|
21
|
-
historyIndex = -1;
|
|
22
|
-
savedBuffer = "";
|
|
23
|
-
cursorRowsBelow = 0; // rows from prompt top to cursor row
|
|
24
|
-
cursorTermCol = 1; // 1-indexed terminal column of cursor
|
|
20
|
+
historyIndex = -1;
|
|
21
|
+
savedBuffer = "";
|
|
25
22
|
escapeTimer = null;
|
|
26
23
|
bus;
|
|
27
24
|
onShowAgentInfo;
|
|
25
|
+
view;
|
|
28
26
|
constructor(opts) {
|
|
29
27
|
this.ctx = opts.ctx;
|
|
30
28
|
this.bus = opts.bus;
|
|
31
29
|
this.onShowAgentInfo = opts.onShowAgentInfo;
|
|
30
|
+
this.view = opts.view ?? new TuiInputView();
|
|
32
31
|
this.loadHistory();
|
|
33
|
-
// Re-render prompt when config changes (e.g. thinking level cycled)
|
|
34
32
|
this.bus.on("config:changed", () => {
|
|
35
33
|
if (this.activeMode)
|
|
36
|
-
this.
|
|
34
|
+
this.drawPrompt();
|
|
37
35
|
});
|
|
38
|
-
// Listen for mode registrations from extensions
|
|
39
36
|
this.bus.on("input-mode:register", (config) => {
|
|
40
37
|
this.registerMode(config);
|
|
41
38
|
});
|
|
@@ -56,7 +53,6 @@ export class InputHandler {
|
|
|
56
53
|
this.history = data.split("\n").filter(Boolean);
|
|
57
54
|
}
|
|
58
55
|
catch {
|
|
59
|
-
// No history file yet
|
|
60
56
|
}
|
|
61
57
|
}
|
|
62
58
|
saveHistory() {
|
|
@@ -67,108 +63,22 @@ export class InputHandler {
|
|
|
67
63
|
fs.writeFileSync(HISTORY_FILE, lines.join("\n") + "\n");
|
|
68
64
|
}
|
|
69
65
|
catch {
|
|
70
|
-
// Non-critical — ignore write failures
|
|
71
66
|
}
|
|
72
67
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
process.stdout.write("\r\x1b[J");
|
|
83
|
-
const agentInfo = this.onShowAgentInfo();
|
|
84
|
-
const indicator = this.activeMode?.indicator ?? "●";
|
|
85
|
-
const infoPrefix = agentInfo.info
|
|
86
|
-
? `${agentInfo.info} ${p.success}${indicator}${p.reset} `
|
|
87
|
-
: `${p.success}${indicator}${p.reset} `;
|
|
88
|
-
const icon = this.activeMode?.promptIcon ?? "❯";
|
|
89
|
-
const promptPrefix = infoPrefix + p.warning + p.bold + icon + " " + p.reset;
|
|
90
|
-
const promptVisLen = visibleLen(infoPrefix) + visibleLen(icon) + 1; // icon + space
|
|
91
|
-
const display = showBuffer ? this.editor.displayText : "";
|
|
92
|
-
const dCursor = showBuffer ? this.editor.displayCursor : 0;
|
|
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);
|
|
116
|
-
}
|
|
117
|
-
else {
|
|
118
|
-
// Multi-line: render each line with continuation indent.
|
|
119
|
-
// Same save/restore strategy — cursor position is never computed.
|
|
120
|
-
const lines = display.split("\n");
|
|
121
|
-
const indent = " ".repeat(promptVisLen);
|
|
122
|
-
// Locate cursor: which logical line and offset within it.
|
|
123
|
-
let charsRemaining = dCursor;
|
|
124
|
-
let cursorLine = 0;
|
|
125
|
-
for (let li = 0; li < lines.length; li++) {
|
|
126
|
-
if (charsRemaining <= lines[li].length) {
|
|
127
|
-
cursorLine = li;
|
|
128
|
-
break;
|
|
129
|
-
}
|
|
130
|
-
charsRemaining -= lines[li].length + 1; // +1 for \n
|
|
131
|
-
cursorLine = li + 1;
|
|
132
|
-
}
|
|
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;
|
|
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;
|
|
164
|
-
}
|
|
68
|
+
drawPrompt(showBuffer = true) {
|
|
69
|
+
this.view.drawPrompt({
|
|
70
|
+
showBuffer,
|
|
71
|
+
displayText: this.editor.displayText,
|
|
72
|
+
displayCursor: this.editor.displayCursor,
|
|
73
|
+
indicator: this.activeMode?.indicator ?? "●",
|
|
74
|
+
promptIcon: this.activeMode?.promptIcon ?? "❯",
|
|
75
|
+
agentInfo: this.onShowAgentInfo(),
|
|
76
|
+
});
|
|
165
77
|
}
|
|
166
78
|
handleInput(data) {
|
|
167
|
-
// Allow extensions to capture raw input (e.g. overlay prompt during vim)
|
|
168
79
|
const intercepted = this.bus.emitPipe("input:intercept", { data, consumed: false });
|
|
169
80
|
if (intercepted.consumed)
|
|
170
81
|
return;
|
|
171
|
-
// If agent is running (processing a query), only Ctrl-C and control keys
|
|
172
82
|
if (this.ctx.isAgentActive()) {
|
|
173
83
|
if (data === "\x03") {
|
|
174
84
|
this.bus.emit("agent:cancel-request", {});
|
|
@@ -178,20 +88,16 @@ export class InputHandler {
|
|
|
178
88
|
}
|
|
179
89
|
return;
|
|
180
90
|
}
|
|
181
|
-
// Intercept control chars for TUI (Ctrl+T, Ctrl+O) — don't pass to PTY
|
|
182
91
|
if (data.length === 1 && data.charCodeAt(0) < 32 && !this.activeMode) {
|
|
183
92
|
const code = data.charCodeAt(0);
|
|
184
|
-
// Keys consumed by TUI extensions
|
|
185
93
|
if (code === 0x14 || code === 0x0f) { // Ctrl+T, Ctrl+O
|
|
186
94
|
this.bus.emit("input:keypress", { key: data });
|
|
187
95
|
return;
|
|
188
96
|
}
|
|
189
|
-
// Forward other control chars that shell mode doesn't handle
|
|
190
97
|
if (code !== 0x0d && code !== 0x03 && code !== 0x04 && code !== 0x09) {
|
|
191
98
|
this.bus.emit("input:keypress", { key: data });
|
|
192
99
|
}
|
|
193
100
|
}
|
|
194
|
-
// If in an input mode (typing a query)
|
|
195
101
|
if (this.activeMode) {
|
|
196
102
|
this.handleModeInput(data);
|
|
197
103
|
return;
|
|
@@ -199,7 +105,6 @@ export class InputHandler {
|
|
|
199
105
|
for (let i = 0; i < data.length; i++) {
|
|
200
106
|
const ch = data[i];
|
|
201
107
|
if (ch === "\r") {
|
|
202
|
-
// Record the command — output will be captured until next prompt marker
|
|
203
108
|
if (this.lineBuffer.trim()) {
|
|
204
109
|
this.ctx.onCommandEntered(this.lineBuffer.trim(), this.ctx.getCwd());
|
|
205
110
|
}
|
|
@@ -210,29 +115,22 @@ export class InputHandler {
|
|
|
210
115
|
this.lineBuffer = this.lineBuffer.slice(0, -1);
|
|
211
116
|
this.ctx.writeToPty(ch);
|
|
212
117
|
}
|
|
213
|
-
else if (ch === "\x03") {
|
|
214
|
-
this.lineBuffer = "";
|
|
215
|
-
this.ctx.writeToPty(ch);
|
|
216
|
-
}
|
|
217
|
-
else if (ch === "\x04") {
|
|
118
|
+
else if (ch === "\x03" || ch === "\x04") {
|
|
218
119
|
this.lineBuffer = "";
|
|
219
120
|
this.ctx.writeToPty(ch);
|
|
220
121
|
}
|
|
221
122
|
else if (ch === "\x0b" || ch === "\x15") {
|
|
222
|
-
// Ctrl-K / Ctrl-U
|
|
223
|
-
// mode-trigger check sees an empty buffer. Not cursor-accurate.
|
|
123
|
+
// Ctrl-K / Ctrl-U: shell kills the line; mirror so lineBuffer stays in sync.
|
|
224
124
|
this.lineBuffer = "";
|
|
225
125
|
this.ctx.writeToPty(ch);
|
|
226
126
|
}
|
|
227
127
|
else if (ch === "\x1b") {
|
|
228
|
-
//
|
|
229
|
-
//
|
|
230
|
-
// and SS3 (ESC O <char>) sequences; anything else: just ESC.
|
|
128
|
+
// Forward whole escape sequence as a unit — otherwise payload bytes
|
|
129
|
+
// (e.g. OSC color-query response) can leak into lineBuffer.
|
|
231
130
|
let seq = ch;
|
|
232
131
|
if (i + 1 < data.length) {
|
|
233
132
|
const next = data[i + 1];
|
|
234
133
|
if (next === "[") {
|
|
235
|
-
// CSI: ESC [ (params) (intermediates) final_byte
|
|
236
134
|
seq += next;
|
|
237
135
|
i++;
|
|
238
136
|
while (i + 1 < data.length && data[i + 1].charCodeAt(0) < 0x40) {
|
|
@@ -242,10 +140,9 @@ export class InputHandler {
|
|
|
242
140
|
if (i + 1 < data.length) {
|
|
243
141
|
i++;
|
|
244
142
|
seq += data[i];
|
|
245
|
-
}
|
|
143
|
+
}
|
|
246
144
|
}
|
|
247
145
|
else if (next === "O") {
|
|
248
|
-
// SS3: ESC O <char>
|
|
249
146
|
seq += next;
|
|
250
147
|
i++;
|
|
251
148
|
if (i + 1 < data.length) {
|
|
@@ -254,12 +151,7 @@ export class InputHandler {
|
|
|
254
151
|
}
|
|
255
152
|
}
|
|
256
153
|
else if (next === "]" || next === "P" || next === "_" || next === "^") {
|
|
257
|
-
//
|
|
258
|
-
// OSC (ESC ]) — OSC 10/11 color-query responses
|
|
259
|
-
// DCS (ESC P) — tmux XTVERSION query response (iTerm2 etc.)
|
|
260
|
-
// APC (ESC _), PM (ESC ^) — rarer, same termination
|
|
261
|
-
// Forward as a unit so the payload doesn't leak into lineBuffer
|
|
262
|
-
// and onto the bash command line after a foreground app exits.
|
|
154
|
+
// OSC/DCS/APC/PM — terminated by BEL or ST (ESC \).
|
|
263
155
|
let j = i + 2;
|
|
264
156
|
let termEnd = -1;
|
|
265
157
|
while (j < data.length) {
|
|
@@ -284,7 +176,6 @@ export class InputHandler {
|
|
|
284
176
|
}
|
|
285
177
|
}
|
|
286
178
|
else {
|
|
287
|
-
// ESC + single char (alt-key, etc.)
|
|
288
179
|
seq += next;
|
|
289
180
|
i++;
|
|
290
181
|
}
|
|
@@ -295,12 +186,10 @@ export class InputHandler {
|
|
|
295
186
|
this.ctx.writeToPty(ch);
|
|
296
187
|
}
|
|
297
188
|
else {
|
|
298
|
-
// Check if trigger char at start of empty line → enter that mode
|
|
299
|
-
// But not if a foreground process (ssh, vim, etc.) is running
|
|
300
189
|
const mode = this.modes.get(ch);
|
|
301
190
|
if (this.lineBuffer === "" && mode && !this.ctx.isForegroundBusy()) {
|
|
302
191
|
this.enterMode(mode);
|
|
303
|
-
return;
|
|
192
|
+
return;
|
|
304
193
|
}
|
|
305
194
|
if (!this.ctx.isForegroundBusy())
|
|
306
195
|
this.lineBuffer += ch;
|
|
@@ -311,38 +200,23 @@ export class InputHandler {
|
|
|
311
200
|
enterMode(mode) {
|
|
312
201
|
this.activeMode = mode;
|
|
313
202
|
this.editor.clear();
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
// Enable bracket paste mode so pasted text doesn't trigger submit.
|
|
317
|
-
process.stdout.write("\x1b[>1u\x1b[?2004h");
|
|
318
|
-
this.writeModePromptLine(false);
|
|
203
|
+
this.view.enableModeKeys();
|
|
204
|
+
this.drawPrompt(false);
|
|
319
205
|
}
|
|
320
206
|
exitMode() {
|
|
321
207
|
this.dismissAutocomplete();
|
|
322
208
|
this.activeMode = null;
|
|
323
209
|
this.editor.clear();
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
this.
|
|
327
|
-
this.cursorRowsBelow = 0;
|
|
328
|
-
this.cursorTermCol = 1;
|
|
210
|
+
this.view.disableModeKeys();
|
|
211
|
+
this.view.clearPromptArea();
|
|
212
|
+
this.view.resetCursor();
|
|
329
213
|
this.printPrompt();
|
|
330
214
|
}
|
|
331
|
-
/** Move to the start of the prompt area and clear everything below. */
|
|
332
|
-
clearPromptArea() {
|
|
333
|
-
if (this.cursorRowsBelow > 0) {
|
|
334
|
-
process.stdout.write(`\x1b[${this.cursorRowsBelow}A`);
|
|
335
|
-
}
|
|
336
|
-
process.stdout.write("\r\x1b[J");
|
|
337
|
-
this.cursorRowsBelow = 0;
|
|
338
|
-
}
|
|
339
215
|
printPrompt() {
|
|
340
216
|
this.ctx.redrawPrompt();
|
|
341
217
|
}
|
|
342
|
-
/**
|
|
343
|
-
*
|
|
344
|
-
* handler re-entered a mode (so caller should skip shell prompt).
|
|
345
|
-
*/
|
|
218
|
+
/** Called when agent processing completes. Returns true if the input
|
|
219
|
+
* handler re-entered a mode (so caller should skip shell prompt). */
|
|
346
220
|
handleProcessingDone() {
|
|
347
221
|
if (this.pendingReturnMode) {
|
|
348
222
|
const mode = this.modesById.get(this.pendingReturnMode);
|
|
@@ -355,8 +229,8 @@ export class InputHandler {
|
|
|
355
229
|
return false;
|
|
356
230
|
}
|
|
357
231
|
renderModeInput() {
|
|
358
|
-
this.
|
|
359
|
-
this.
|
|
232
|
+
this.view.clearAutocomplete();
|
|
233
|
+
this.drawPrompt();
|
|
360
234
|
this.updateAutocomplete();
|
|
361
235
|
}
|
|
362
236
|
updateAutocomplete() {
|
|
@@ -381,38 +255,13 @@ export class InputHandler {
|
|
|
381
255
|
this.autocompleteActive = true;
|
|
382
256
|
if (this.autocompleteIndex >= items.length)
|
|
383
257
|
this.autocompleteIndex = 0;
|
|
384
|
-
this.
|
|
258
|
+
this.view.drawAutocomplete({ items: this.autocompleteItems, selected: this.autocompleteIndex });
|
|
385
259
|
}
|
|
386
260
|
else {
|
|
387
261
|
this.autocompleteActive = false;
|
|
388
262
|
this.autocompleteItems = [];
|
|
389
|
-
this.autocompleteLines = 0;
|
|
390
263
|
}
|
|
391
264
|
}
|
|
392
|
-
renderAutocomplete() {
|
|
393
|
-
if (!this.autocompleteActive || this.autocompleteItems.length === 0)
|
|
394
|
-
return;
|
|
395
|
-
const lines = [];
|
|
396
|
-
for (let i = 0; i < this.autocompleteItems.length; i++) {
|
|
397
|
-
const item = this.autocompleteItems[i];
|
|
398
|
-
const selected = i === this.autocompleteIndex;
|
|
399
|
-
if (selected) {
|
|
400
|
-
lines.push(` \x1b[7m ${p.accent}${item.name.padEnd(12)}${p.reset}\x1b[7m ${item.description} ${p.reset}`);
|
|
401
|
-
}
|
|
402
|
-
else {
|
|
403
|
-
lines.push(` ${p.muted}${item.name.padEnd(12)} ${item.description}${p.reset}`);
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
process.stdout.write("\n" + lines.join("\n"));
|
|
407
|
-
this.autocompleteLines = lines.length;
|
|
408
|
-
if (this.autocompleteLines > 0) {
|
|
409
|
-
process.stdout.write(`\x1b[${this.autocompleteLines}A`);
|
|
410
|
-
}
|
|
411
|
-
// Restore cursor column — use explicit column set instead of DECRC
|
|
412
|
-
// because writing \n above may have scrolled the terminal, which
|
|
413
|
-
// invalidates the absolute position saved by DECSC.
|
|
414
|
-
process.stdout.write(`\x1b[${this.cursorTermCol}G`);
|
|
415
|
-
}
|
|
416
265
|
applyAutocomplete() {
|
|
417
266
|
if (!this.autocompleteActive || this.autocompleteItems.length === 0)
|
|
418
267
|
return;
|
|
@@ -429,40 +278,26 @@ export class InputHandler {
|
|
|
429
278
|
else {
|
|
430
279
|
this.editor.setText(selected.name);
|
|
431
280
|
}
|
|
432
|
-
this.
|
|
281
|
+
this.view.clearAutocomplete();
|
|
433
282
|
this.autocompleteActive = false;
|
|
434
283
|
this.autocompleteItems = [];
|
|
435
284
|
this.autocompleteIndex = 0;
|
|
436
|
-
this.
|
|
285
|
+
this.drawPrompt();
|
|
437
286
|
if (isFileAc)
|
|
438
287
|
this.updateAutocomplete();
|
|
439
288
|
}
|
|
440
289
|
dismissAutocomplete() {
|
|
441
|
-
this.
|
|
290
|
+
this.view.clearAutocomplete();
|
|
442
291
|
this.autocompleteActive = false;
|
|
443
292
|
this.autocompleteItems = [];
|
|
444
293
|
this.autocompleteIndex = 0;
|
|
445
294
|
}
|
|
446
|
-
clearAutocompleteLines() {
|
|
447
|
-
if (this.autocompleteLines <= 0)
|
|
448
|
-
return;
|
|
449
|
-
// Use CSI B (cursor down, bounded) instead of \n to avoid scroll
|
|
450
|
-
for (let i = 0; i < this.autocompleteLines; i++) {
|
|
451
|
-
process.stdout.write("\x1b[B\x1b[2K"); // move down, clear line
|
|
452
|
-
}
|
|
453
|
-
// Move back up and restore column with relative movement (scroll-safe)
|
|
454
|
-
process.stdout.write(`\x1b[${this.autocompleteLines}A\x1b[${this.cursorTermCol}G`);
|
|
455
|
-
this.autocompleteLines = 0;
|
|
456
|
-
}
|
|
457
295
|
handleModeInput(data) {
|
|
458
|
-
// Clear any pending escape timer — new data arrived
|
|
459
296
|
if (this.escapeTimer) {
|
|
460
297
|
clearTimeout(this.escapeTimer);
|
|
461
298
|
this.escapeTimer = null;
|
|
462
299
|
}
|
|
463
300
|
const actions = this.editor.feed(data);
|
|
464
|
-
// If the editor is waiting for more escape sequence data, set a short
|
|
465
|
-
// timer — if nothing arrives, treat it as a bare Escape keypress
|
|
466
301
|
if (this.editor.hasPendingEscape()) {
|
|
467
302
|
this.escapeTimer = setTimeout(() => {
|
|
468
303
|
this.escapeTimer = null;
|
|
@@ -477,14 +312,13 @@ export class InputHandler {
|
|
|
477
312
|
for (const act of actions) {
|
|
478
313
|
switch (act.action) {
|
|
479
314
|
case "changed": {
|
|
480
|
-
// If the buffer is exactly a trigger char for a different mode, switch to it
|
|
481
315
|
const switchMode = this.modes.get(this.editor.text);
|
|
482
316
|
if (this.editor.text.length === 1 && switchMode && switchMode !== this.activeMode) {
|
|
483
317
|
this.dismissAutocomplete();
|
|
484
|
-
this.clearPromptArea();
|
|
318
|
+
this.view.clearPromptArea();
|
|
485
319
|
this.activeMode = switchMode;
|
|
486
320
|
this.editor.clear();
|
|
487
|
-
this.
|
|
321
|
+
this.drawPrompt(false);
|
|
488
322
|
break;
|
|
489
323
|
}
|
|
490
324
|
this.historyIndex = -1;
|
|
@@ -496,12 +330,9 @@ export class InputHandler {
|
|
|
496
330
|
if (this.autocompleteActive) {
|
|
497
331
|
this.applyAutocomplete();
|
|
498
332
|
}
|
|
499
|
-
// Use editor.text (not act.buffer) so autocomplete selections
|
|
500
|
-
// take effect — act.buffer is a stale snapshot from before
|
|
501
|
-
// applyAutocomplete() updated the editor.
|
|
333
|
+
// Use editor.text (not act.buffer) so autocomplete selections take effect.
|
|
502
334
|
const query = this.editor.text.trim();
|
|
503
335
|
if (query) {
|
|
504
|
-
// Add to history (avoid consecutive duplicates)
|
|
505
336
|
if (this.history.length === 0 || this.history[this.history.length - 1] !== query) {
|
|
506
337
|
this.history.push(query);
|
|
507
338
|
this.saveHistory();
|
|
@@ -509,14 +340,13 @@ export class InputHandler {
|
|
|
509
340
|
}
|
|
510
341
|
this.historyIndex = -1;
|
|
511
342
|
this.savedBuffer = "";
|
|
512
|
-
this.
|
|
513
|
-
this.clearPromptArea();
|
|
514
|
-
|
|
343
|
+
this.view.clearAutocomplete();
|
|
344
|
+
this.view.clearPromptArea();
|
|
345
|
+
this.view.disableModeKeys();
|
|
515
346
|
const currentMode = this.activeMode;
|
|
516
347
|
this.activeMode = null;
|
|
517
348
|
this.editor.clear();
|
|
518
|
-
this.
|
|
519
|
-
this.cursorTermCol = 1;
|
|
349
|
+
this.view.resetCursor();
|
|
520
350
|
this.dismissAutocomplete();
|
|
521
351
|
if (query && query.startsWith("/")) {
|
|
522
352
|
const spaceIdx = query.indexOf(" ");
|
|
@@ -542,7 +372,7 @@ export class InputHandler {
|
|
|
542
372
|
case "cancel":
|
|
543
373
|
if (this.autocompleteActive) {
|
|
544
374
|
this.dismissAutocomplete();
|
|
545
|
-
this.
|
|
375
|
+
this.drawPrompt();
|
|
546
376
|
}
|
|
547
377
|
else {
|
|
548
378
|
this.exitMode();
|
|
@@ -563,9 +393,9 @@ export class InputHandler {
|
|
|
563
393
|
this.autocompleteIndex === 0
|
|
564
394
|
? this.autocompleteItems.length - 1
|
|
565
395
|
: this.autocompleteIndex - 1;
|
|
566
|
-
this.
|
|
567
|
-
this.
|
|
568
|
-
this.
|
|
396
|
+
this.view.clearAutocomplete();
|
|
397
|
+
this.drawPrompt();
|
|
398
|
+
this.view.drawAutocomplete({ items: this.autocompleteItems, selected: this.autocompleteIndex });
|
|
569
399
|
}
|
|
570
400
|
else if (this.history.length > 0) {
|
|
571
401
|
if (this.historyIndex === -1) {
|
|
@@ -576,8 +406,8 @@ export class InputHandler {
|
|
|
576
406
|
this.historyIndex--;
|
|
577
407
|
}
|
|
578
408
|
this.editor.setText(this.history[this.historyIndex]);
|
|
579
|
-
this.
|
|
580
|
-
this.
|
|
409
|
+
this.view.clearAutocomplete();
|
|
410
|
+
this.drawPrompt();
|
|
581
411
|
}
|
|
582
412
|
break;
|
|
583
413
|
case "arrow-down":
|
|
@@ -586,9 +416,9 @@ export class InputHandler {
|
|
|
586
416
|
this.autocompleteIndex === this.autocompleteItems.length - 1
|
|
587
417
|
? 0
|
|
588
418
|
: this.autocompleteIndex + 1;
|
|
589
|
-
this.
|
|
590
|
-
this.
|
|
591
|
-
this.
|
|
419
|
+
this.view.clearAutocomplete();
|
|
420
|
+
this.drawPrompt();
|
|
421
|
+
this.view.drawAutocomplete({ items: this.autocompleteItems, selected: this.autocompleteIndex });
|
|
592
422
|
}
|
|
593
423
|
else if (this.historyIndex !== -1) {
|
|
594
424
|
if (this.historyIndex < this.history.length - 1) {
|
|
@@ -599,8 +429,8 @@ export class InputHandler {
|
|
|
599
429
|
this.historyIndex = -1;
|
|
600
430
|
this.editor.setText(this.savedBuffer);
|
|
601
431
|
}
|
|
602
|
-
this.
|
|
603
|
-
this.
|
|
432
|
+
this.view.clearAutocomplete();
|
|
433
|
+
this.drawPrompt();
|
|
604
434
|
}
|
|
605
435
|
break;
|
|
606
436
|
}
|
package/dist/shell/shell.js
CHANGED
|
@@ -5,7 +5,7 @@ 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/
|
|
8
|
+
import { RefCounter } from "../utils/ref-counter.js";
|
|
9
9
|
export class Shell {
|
|
10
10
|
ptyProcess;
|
|
11
11
|
bus;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal renderer for the input-mode prompt and autocomplete dropdown.
|
|
3
|
+
* Owns screen state (cursor row/col, autocomplete line count) and the
|
|
4
|
+
* ANSI redraw. The controller drives it via a small VM shape.
|
|
5
|
+
*/
|
|
6
|
+
import type { RenderSurface } from "../utils/compositor.js";
|
|
7
|
+
export interface PromptVM {
|
|
8
|
+
showBuffer: boolean;
|
|
9
|
+
displayText: string;
|
|
10
|
+
displayCursor: number;
|
|
11
|
+
indicator: string;
|
|
12
|
+
promptIcon: string;
|
|
13
|
+
agentInfo: {
|
|
14
|
+
info: string;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export interface AutocompleteVM {
|
|
18
|
+
items: {
|
|
19
|
+
name: string;
|
|
20
|
+
description: string;
|
|
21
|
+
}[];
|
|
22
|
+
selected: number;
|
|
23
|
+
}
|
|
24
|
+
export declare class TuiInputView {
|
|
25
|
+
private cursorRowsBelow;
|
|
26
|
+
private cursorTermCol;
|
|
27
|
+
private autocompleteLines;
|
|
28
|
+
private readonly surface;
|
|
29
|
+
constructor(surface?: RenderSurface);
|
|
30
|
+
resetCursor(): void;
|
|
31
|
+
enableModeKeys(): void;
|
|
32
|
+
disableModeKeys(): void;
|
|
33
|
+
clearPromptArea(): void;
|
|
34
|
+
drawPrompt(vm: PromptVM): void;
|
|
35
|
+
drawAutocomplete(vm: AutocompleteVM): void;
|
|
36
|
+
clearAutocomplete(): void;
|
|
37
|
+
}
|