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.
@@ -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; // mode id to return to after processing
13
- modes = new Map(); // keyed by trigger char
14
- modesById = new Map(); // keyed by id
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; // -1 = not browsing history
22
- savedBuffer = ""; // buffer saved when entering history
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.writeModePromptLine();
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
- /** Write the mode prompt line with cursor at the correct position. */
74
- writeModePromptLine(showBuffer = true) {
75
- const termW = process.stdout.columns || 80;
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`);
80
- }
81
- // Clear from here to end of screen — removes current + all wrapped lines below
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 kill the line in the shell; mirror that so the
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
- // Escape sequence forward the entire sequence to the PTY but
229
- // don't let it corrupt lineBuffer. Skip CSI (ESC [ ... final)
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
- } // final byte
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
- // String sequences terminated by BEL or ST (ESC \):
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; // don't process remaining chars
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
- // Enable kitty keyboard protocol (progressive enhancement flag 1)
315
- // so Shift+Enter sends \x1b[13;2u instead of plain \r.
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
- // Disable kitty keyboard protocol and bracket paste mode
325
- process.stdout.write("\x1b[<u\x1b[?2004l");
326
- this.clearPromptArea();
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
- * Called when agent processing completes. Returns true if the input
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.clearAutocompleteLines();
359
- this.writeModePromptLine();
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.renderAutocomplete();
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.clearAutocompleteLines();
281
+ this.view.clearAutocomplete();
433
282
  this.autocompleteActive = false;
434
283
  this.autocompleteItems = [];
435
284
  this.autocompleteIndex = 0;
436
- this.writeModePromptLine();
285
+ this.drawPrompt();
437
286
  if (isFileAc)
438
287
  this.updateAutocomplete();
439
288
  }
440
289
  dismissAutocomplete() {
441
- this.clearAutocompleteLines();
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.writeModePromptLine(false);
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.clearAutocompleteLines();
513
- this.clearPromptArea();
514
- process.stdout.write("\x1b[<u\x1b[?2004l"); // disable kitty + bracket paste
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.cursorRowsBelow = 0;
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.writeModePromptLine();
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.clearAutocompleteLines();
567
- this.writeModePromptLine();
568
- this.renderAutocomplete();
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.clearAutocompleteLines();
580
- this.writeModePromptLine();
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.clearAutocompleteLines();
590
- this.writeModePromptLine();
591
- this.renderAutocomplete();
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.clearAutocompleteLines();
603
- this.writeModePromptLine();
432
+ this.view.clearAutocomplete();
433
+ this.drawPrompt();
604
434
  }
605
435
  break;
606
436
  }
@@ -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/output-writer.js";
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
+ }