tsunami-code 3.10.0 → 3.11.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.
Files changed (3) hide show
  1. package/index.js +37 -75
  2. package/lib/ui.js +354 -0
  3. package/package.json +1 -1
package/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import readline from 'readline';
2
+ import { createUI } from './lib/ui.js';
3
3
  import chalk from 'chalk';
4
4
  import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from 'fs';
5
5
  import { join } from 'path';
@@ -227,32 +227,16 @@ if (setServerIdx !== -1 && argv[setServerIdx + 1]) {
227
227
  }
228
228
 
229
229
  // ── Confirm Callback (dangerous command prompt) ─────────────────────────────
230
- function makeConfirmCallback(rl) {
230
+ function makeConfirmCallback(ui) {
231
231
  const cb = async (cmd) => {
232
- return new Promise((resolve) => {
233
- rl.pause();
234
- process.stdout.write(`\n ${yellow('⚠ Dangerous:')} ${dim(cmd.slice(0, 120))}\n`);
235
- process.stdout.write(` ${yellow('Proceed?')} ${dim('(y/N) ')}`);
236
- const handler = (data) => {
237
- process.stdin.removeListener('data', handler);
238
- rl.resume();
239
- process.stdout.write('\n');
240
- resolve(data.toString().trim().toLowerCase() === 'y');
241
- };
242
- process.stdin.once('data', handler);
243
- });
232
+ process.stdout.write(`\n ${yellow('⚠ Dangerous:')} ${dim(cmd.slice(0, 120))}\n`);
233
+ const ch = await ui.readChar(` ${yellow('Proceed?')} ${dim('(y/N) ')}`);
234
+ return ch.toLowerCase() === 'y';
244
235
  };
245
236
 
246
- cb._askUser = (question, resolve) => {
247
- rl.pause();
248
- process.stdout.write(`\n ${cyan('?')} ${question}\n ${dim('> ')}`);
249
- const handler = (data) => {
250
- process.stdin.removeListener('data', handler);
251
- rl.resume();
252
- process.stdout.write('\n');
253
- resolve(data.toString().trim());
254
- };
255
- process.stdin.once('data', handler);
237
+ cb._askUser = async (question, resolve) => {
238
+ const answer = await ui.readLine(`\n ${cyan('?')} ${question}\n ${dim('> ')}`);
239
+ resolve(answer);
256
240
  };
257
241
 
258
242
  return cb;
@@ -351,7 +335,6 @@ async function run() {
351
335
  } catch { return []; }
352
336
  }
353
337
  const historyEntries = loadHistory();
354
- let historyIdx = -1;
355
338
 
356
339
  // Preflight checks
357
340
  process.stdout.write(dim(' Checking server connection...'));
@@ -502,16 +485,6 @@ async function run() {
502
485
  return [[], line];
503
486
  }
504
487
 
505
- const rl = readline.createInterface({
506
- input: process.stdin,
507
- output: process.stdout,
508
- prompt: planMode ? yellow('❯ [plan] ') : cyan('❯ '),
509
- terminal: process.stdin.isTTY,
510
- completer: tabCompleter,
511
- });
512
-
513
- rl.prompt();
514
-
515
488
  let isProcessing = false;
516
489
  let pendingClose = false;
517
490
 
@@ -519,15 +492,23 @@ async function run() {
519
492
  function gracefulExit(code = 0) {
520
493
  try { endSession(sessionDir); } catch {}
521
494
  try { disconnectMcp(); } catch {}
495
+ ui.exitUI();
522
496
  console.log(dim('\n Goodbye.\n'));
523
497
  process.exit(code);
524
498
  }
525
499
 
526
- rl.on('close', () => {
527
- if (isProcessing) { pendingClose = true; return; }
528
- gracefulExit(0);
500
+ const ui = createUI({
501
+ planMode,
502
+ onLine: handleLine,
503
+ onTab: tabCompleter,
504
+ onExit: (code) => {
505
+ if (isProcessing) { pendingClose = true; return; }
506
+ gracefulExit(code);
507
+ },
529
508
  });
530
509
 
510
+ ui.start(historyEntries);
511
+
531
512
  // ── Memory commands ───────────────────────────────────────────────────────────
532
513
  async function handleMemoryCommand(args) {
533
514
  const sub = args[0]?.toLowerCase();
@@ -627,36 +608,32 @@ async function run() {
627
608
  let mlBuffer = [];
628
609
  let mlHeredoc = false;
629
610
 
630
- rl.on('line', async (input) => {
611
+ async function handleLine(input) {
631
612
  // ── Multiline: heredoc mode (""" toggle) ──────────────────────────────────
632
613
  if (input.trimStart() === '"""' || input.trimStart() === "'''") {
633
614
  if (!mlHeredoc) {
634
615
  mlHeredoc = true;
635
- rl.setPrompt(dim('··· '));
636
- rl.prompt();
616
+ ui.setContinuation(true);
637
617
  return;
638
618
  } else {
639
619
  // Close heredoc — submit accumulated buffer
640
620
  mlHeredoc = false;
641
621
  const assembled = mlBuffer.join('\n');
642
622
  mlBuffer = [];
643
- rl.setPrompt(planMode ? yellow('❯ [plan] ') : cyan('❯ '));
644
- // Fall through with assembled as the input
623
+ ui.setContinuation(false);
645
624
  return _handleInput(assembled);
646
625
  }
647
626
  }
648
627
 
649
628
  if (mlHeredoc) {
650
629
  mlBuffer.push(input);
651
- rl.prompt();
652
630
  return;
653
631
  }
654
632
 
655
633
  // ── Multiline: backslash continuation ─────────────────────────────────────
656
634
  if (input.endsWith('\\')) {
657
635
  mlBuffer.push(input.slice(0, -1));
658
- rl.setPrompt(dim('··· '));
659
- rl.prompt();
636
+ ui.setContinuation(true);
660
637
  return;
661
638
  }
662
639
 
@@ -665,21 +642,20 @@ async function run() {
665
642
  mlBuffer.push(input);
666
643
  const assembled = mlBuffer.join('\n');
667
644
  mlBuffer = [];
668
- rl.setPrompt(planMode ? yellow('❯ [plan] ') : cyan('❯ '));
645
+ ui.setContinuation(false);
669
646
  return _handleInput(assembled);
670
647
  }
671
648
 
672
649
  return _handleInput(input);
673
- });
650
+ }
674
651
 
675
652
  async function _handleInput(input) {
676
653
  const line = input.trim();
677
- if (!line) { rl.prompt(); return; }
654
+ if (!line) { return; }
678
655
 
679
656
  // Append to persistent history
680
657
  if (!line.startsWith('/')) {
681
658
  appendHistory(line);
682
- historyIdx = -1;
683
659
  }
684
660
 
685
661
  if (line.startsWith('/')) {
@@ -696,7 +672,7 @@ async function run() {
696
672
  : skillMatch.prompt;
697
673
  messages.push({ role: 'user', content: userContent });
698
674
  const fullMessages = [{ role: 'system', content: systemPrompt }, ...messages];
699
- rl.pause(); isProcessing = true;
675
+ ui.pause(); isProcessing = true;
700
676
  process.stdout.write('\n' + dim(` ◈ Running skill: ${skillMatch.skill.name}\n\n`));
701
677
  let firstToken = true;
702
678
  try {
@@ -704,10 +680,10 @@ async function run() {
704
680
  if (firstToken) { process.stdout.write(' '); firstToken = false; }
705
681
  process.stdout.write(token);
706
682
  }, (name, args) => { printToolCall(name, args); firstToken = true; },
707
- { sessionDir, cwd, planMode }, makeConfirmCallback(rl));
683
+ { sessionDir, cwd, planMode }, makeConfirmCallback(ui));
708
684
  process.stdout.write('\n\n');
709
685
  } catch(e) { console.error(red(` Error: ${e.message}\n`)); }
710
- isProcessing = false; rl.resume(); rl.prompt();
686
+ isProcessing = false; ui.resume();
711
687
  return;
712
688
  }
713
689
 
@@ -758,7 +734,7 @@ async function run() {
758
734
  planMode = !planMode;
759
735
  if (planMode) console.log(yellow(' Plan mode ON — read-only, no writes or execution.\n'));
760
736
  else console.log(green(' Plan mode OFF — full capabilities restored.\n'));
761
- rl.setPrompt(planMode ? yellow('❯ [plan] ') : cyan('❯ '));
737
+ ui.setPlanMode(planMode);
762
738
  break;
763
739
  case 'undo': {
764
740
  const restored = undo();
@@ -1174,7 +1150,7 @@ Output ONLY the TSUNAMI.md content, starting with "# Project: <name>"`;
1174
1150
  default:
1175
1151
  console.log(red(` Unknown command: /${cmd}\n`));
1176
1152
  }
1177
- rl.prompt();
1153
+ ui.resume();
1178
1154
  return;
1179
1155
  }
1180
1156
 
@@ -1206,7 +1182,7 @@ Output ONLY the TSUNAMI.md content, starting with "# Project: <name>"`;
1206
1182
  ];
1207
1183
  messages.push({ role: 'user', content: userContent });
1208
1184
 
1209
- rl.pause();
1185
+ ui.pause();
1210
1186
  isProcessing = true;
1211
1187
  process.stdout.write('\n');
1212
1188
 
@@ -1243,7 +1219,7 @@ Output ONLY the TSUNAMI.md content, starting with "# Project: <name>"`;
1243
1219
  firstToken = true;
1244
1220
  },
1245
1221
  { sessionDir, cwd, planMode },
1246
- makeConfirmCallback(rl)
1222
+ makeConfirmCallback(ui)
1247
1223
  );
1248
1224
  spinner.stop();
1249
1225
  flushHighlighter(highlight);
@@ -1286,27 +1262,13 @@ Output ONLY the TSUNAMI.md content, starting with "# Project: <name>"`;
1286
1262
  gracefulExit(0);
1287
1263
  return;
1288
1264
  }
1289
- rl.resume();
1290
- rl.prompt();
1265
+ ui.resume();
1291
1266
  }
1292
1267
 
1268
+ // SIGINT from kill -INT (keyboard Ctrl+C is handled in raw mode inside ui.js)
1293
1269
  process.on('SIGINT', () => {
1294
- if (!isProcessing) {
1295
- gracefulExit(0);
1296
- } else {
1297
- // Remove interrupted command from history (from history.ts removeLastFromHistory)
1298
- try {
1299
- if (existsSync(HISTORY_FILE)) {
1300
- const lines = readFileSync(HISTORY_FILE, 'utf8').trimEnd().split('\n').filter(Boolean);
1301
- if (lines.length > 0) lines.pop();
1302
- import('fs').then(({ writeFileSync: wfs }) => {
1303
- try { wfs(HISTORY_FILE, lines.join('\n') + (lines.length ? '\n' : ''), 'utf8'); } catch {}
1304
- });
1305
- }
1306
- } catch {}
1307
- pendingClose = true;
1308
- console.log(dim('\n (interrupted)\n'));
1309
- }
1270
+ if (!isProcessing) gracefulExit(0);
1271
+ else pendingClose = true;
1310
1272
  });
1311
1273
  }
1312
1274
 
package/lib/ui.js ADDED
@@ -0,0 +1,354 @@
1
+ // lib/ui.js — sticky-bottom terminal UI
2
+ // Pins a 3-line input box at the bottom of the terminal.
3
+ // All output (model replies, tool calls, spinner) scrolls above it.
4
+ // Uses ANSI scroll region (\x1b[top;botr) so the bottom box never scrolls.
5
+
6
+ import chalk from 'chalk';
7
+
8
+ const cyan = (s) => chalk.cyan(s);
9
+ const yellow = (s) => chalk.yellow(s);
10
+ const dim = (s) => chalk.dim(s);
11
+
12
+ const BOX_LINES = 3; // ╭─ header ─╮ / │ ❯ input │ / ╰──────────╯
13
+
14
+ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit }) {
15
+ let planMode = initPlanMode;
16
+ let continuation = false; // true → show ··· prompt
17
+ let inputBuf = '';
18
+ let cursorPos = 0;
19
+ let history = []; // strings, oldest→newest
20
+ let histIdx = 0; // points past end when not navigating
21
+ let histDraft = ''; // saved current input while browsing history
22
+ let processing = false;
23
+ let lineHandler = null; // set during readLine / readChar
24
+ let _interrupted = false;
25
+
26
+ const rows = () => process.stdout.rows || 24;
27
+ const cols = () => process.stdout.columns || 80;
28
+
29
+ // ── Scroll region ──────────────────────────────────────────────────────────
30
+ // Confines terminal scroll to rows 1..(rows-BOX_LINES).
31
+ // The bottom BOX_LINES rows sit outside the scroll region and stay fixed.
32
+
33
+ function setScrollRegion() {
34
+ process.stdout.write(`\x1b[1;${rows() - BOX_LINES}r`);
35
+ }
36
+
37
+ function resetScrollRegion() {
38
+ process.stdout.write('\x1b[r');
39
+ }
40
+
41
+ function goOutputArea() {
42
+ // Place cursor at the last line of the scroll region so next write lands there.
43
+ process.stdout.write(`\x1b[${rows() - BOX_LINES};1H`);
44
+ }
45
+
46
+ // ── Box drawing ────────────────────────────────────────────────────────────
47
+
48
+ function drawBox() {
49
+ const w = cols();
50
+ const r = rows();
51
+ const r1 = r - BOX_LINES + 1; // top border row
52
+ const r2 = r - BOX_LINES + 2; // input row
53
+ const r3 = r - BOX_LINES + 3; // bottom border row
54
+
55
+ // Save cursor so we can restore it after drawing.
56
+ process.stdout.write('\x1b[s');
57
+
58
+ // Top border ─────────────────────────────────────────
59
+ const label = ' tsunami ';
60
+ const rightFill = Math.max(0, w - 2 - 1 - label.length);
61
+ const topLine = `╭─${label}${'─'.repeat(rightFill)}╮`;
62
+ process.stdout.write(`\x1b[${r1};1H\x1b[2K${topLine.slice(0, w)}`);
63
+
64
+ // Input line ──────────────────────────────────────────
65
+ let prefix;
66
+ if (continuation) prefix = ` ${dim('···')} `;
67
+ else if (planMode) prefix = ` ${yellow('❯')} ${dim('[plan]')} `;
68
+ else prefix = ` ${cyan('❯')} `;
69
+
70
+ // Strip ANSI codes to measure visible width of prefix.
71
+ const prefixVis = prefix.replace(/\x1b\[[0-9;]*m/g, '');
72
+ const innerW = w - 2; // space between │ │
73
+ const maxInput = Math.max(0, innerW - prefixVis.length - 1); // trailing space
74
+
75
+ // Slide window so cursor stays visible.
76
+ let dispInput = inputBuf;
77
+ let dispCursor = cursorPos;
78
+ if (dispInput.length > maxInput) {
79
+ const start = Math.max(0, cursorPos - maxInput + 1);
80
+ dispInput = inputBuf.slice(start);
81
+ dispCursor = cursorPos - start;
82
+ }
83
+ const display = dispInput.slice(0, maxInput);
84
+ const pad = ' '.repeat(Math.max(0, maxInput - display.length));
85
+ process.stdout.write(`\x1b[${r2};1H\x1b[2K│${prefix}${display}${pad} │`);
86
+
87
+ // Bottom border ───────────────────────────────────────
88
+ const botLine = `╰${'─'.repeat(w - 2)}╯`;
89
+ process.stdout.write(`\x1b[${r3};1H\x1b[2K${botLine.slice(0, w)}`);
90
+
91
+ // Restore cursor to correct column in the input row.
92
+ const curCol = 1 + prefixVis.length + 1 + Math.min(dispCursor, display.length);
93
+ process.stdout.write(`\x1b[${r2};${curCol}H`);
94
+ }
95
+
96
+ // ── Raw key handler ────────────────────────────────────────────────────────
97
+
98
+ function handleKey(chunk) {
99
+ const key = chunk.toString('utf8');
100
+
101
+ // Delegate to temporary line handler (readLine / readChar in progress).
102
+ if (lineHandler) { lineHandler(key); return; }
103
+
104
+ // Ctrl+C ──────────────────────────────────────────────
105
+ if (key === '\x03') {
106
+ if (processing) {
107
+ _interrupted = true;
108
+ process.stdout.write(dim('\n (interrupted)\n'));
109
+ } else if (inputBuf !== '') {
110
+ inputBuf = '';
111
+ cursorPos = 0;
112
+ drawBox();
113
+ } else {
114
+ exitUI();
115
+ onExit(0);
116
+ }
117
+ return;
118
+ }
119
+
120
+ // Ctrl+D ──────────────────────────────────────────────
121
+ if (key === '\x04') {
122
+ if (!processing && inputBuf === '') { exitUI(); onExit(0); }
123
+ return;
124
+ }
125
+
126
+ if (processing) return; // swallow all other keys while model is running
127
+
128
+ // Enter ───────────────────────────────────────────────
129
+ if (key === '\r' || key === '\n') {
130
+ const line = inputBuf;
131
+ if (line || continuation) {
132
+ if (line) {
133
+ history.push(line);
134
+ histIdx = history.length;
135
+ histDraft = '';
136
+ }
137
+ inputBuf = '';
138
+ cursorPos = 0;
139
+ drawBox();
140
+ goOutputArea();
141
+ process.stdout.write('\n');
142
+ onLine(line);
143
+ }
144
+ return;
145
+ }
146
+
147
+ // Backspace ───────────────────────────────────────────
148
+ if (key === '\x7f' || key === '\x08') {
149
+ if (cursorPos > 0) {
150
+ inputBuf = inputBuf.slice(0, cursorPos - 1) + inputBuf.slice(cursorPos);
151
+ cursorPos--;
152
+ drawBox();
153
+ }
154
+ return;
155
+ }
156
+
157
+ // Tab ─────────────────────────────────────────────────
158
+ if (key === '\t') {
159
+ if (!onTab) return;
160
+ const [hits] = onTab(inputBuf);
161
+ if (!hits.length) return;
162
+ if (hits.length === 1) {
163
+ inputBuf = hits[0];
164
+ cursorPos = hits[0].length;
165
+ drawBox();
166
+ return;
167
+ }
168
+ // Multiple hits — show list and fill common prefix.
169
+ const prefix = hits.reduce((a, b) => {
170
+ let i = 0;
171
+ while (i < a.length && i < b.length && a[i] === b[i]) i++;
172
+ return a.slice(0, i);
173
+ });
174
+ goOutputArea();
175
+ const shown = hits.slice(0, 8).join(' ') + (hits.length > 8 ? ` …+${hits.length - 8}` : '');
176
+ process.stdout.write('\n ' + dim(shown) + '\n');
177
+ if (prefix.length > inputBuf.length) {
178
+ inputBuf = prefix;
179
+ cursorPos = prefix.length;
180
+ }
181
+ drawBox();
182
+ return;
183
+ }
184
+
185
+ // Arrow / navigation escape sequences ─────────────────
186
+ if (key === '\x1b[A') { // up — history back
187
+ if (histIdx > 0) {
188
+ if (histIdx === history.length) histDraft = inputBuf;
189
+ histIdx--;
190
+ inputBuf = history[histIdx] || '';
191
+ cursorPos = inputBuf.length;
192
+ drawBox();
193
+ }
194
+ return;
195
+ }
196
+ if (key === '\x1b[B') { // down — history forward
197
+ if (histIdx < history.length) {
198
+ histIdx++;
199
+ inputBuf = histIdx === history.length ? histDraft : history[histIdx] || '';
200
+ cursorPos = inputBuf.length;
201
+ drawBox();
202
+ }
203
+ return;
204
+ }
205
+ if (key === '\x1b[D') { cursorPos = Math.max(0, cursorPos - 1); drawBox(); return; } // ←
206
+ if (key === '\x1b[C') { cursorPos = Math.min(inputBuf.length, cursorPos+1); drawBox(); return; } // →
207
+ if (key === '\x1b[H' || key === '\x01') { cursorPos = 0; drawBox(); return; } // Home / Ctrl+A
208
+ if (key === '\x1b[F' || key === '\x05') { cursorPos = inputBuf.length; drawBox(); return; } // End / Ctrl+E
209
+ if (key === '\x1b[3~') { // Delete
210
+ if (cursorPos < inputBuf.length) {
211
+ inputBuf = inputBuf.slice(0, cursorPos) + inputBuf.slice(cursorPos + 1);
212
+ drawBox();
213
+ }
214
+ return;
215
+ }
216
+
217
+ // Editing shortcuts ────────────────────────────────────
218
+ if (key === '\x0b') { inputBuf = inputBuf.slice(0, cursorPos); drawBox(); return; } // Ctrl+K
219
+ if (key === '\x15') { inputBuf = inputBuf.slice(cursorPos); cursorPos = 0; drawBox(); return; } // Ctrl+U
220
+ if (key === '\x17') { // Ctrl+W — kill word backwards
221
+ const before = inputBuf.slice(0, cursorPos).trimEnd();
222
+ const lastSpace = before.lastIndexOf(' ');
223
+ const newBefore = lastSpace < 0 ? '' : before.slice(0, lastSpace + 1);
224
+ inputBuf = newBefore + inputBuf.slice(cursorPos);
225
+ cursorPos = newBefore.length;
226
+ drawBox();
227
+ return;
228
+ }
229
+ if (key === '\x0c') { // Ctrl+L — clear screen
230
+ process.stdout.write('\x1b[2J\x1b[1;1H');
231
+ setScrollRegion();
232
+ drawBox();
233
+ return;
234
+ }
235
+
236
+ // Ignore unhandled escape sequences.
237
+ if (key.startsWith('\x1b')) return;
238
+
239
+ // Printable chars (including multi-byte UTF-8).
240
+ if (key >= ' ' || key.charCodeAt(0) > 127) {
241
+ inputBuf = inputBuf.slice(0, cursorPos) + key + inputBuf.slice(cursorPos);
242
+ cursorPos += key.length;
243
+ drawBox();
244
+ }
245
+ }
246
+
247
+ // ── readLine — full line read during processing (AskUser tool) ────────────
248
+ function readLine(promptText) {
249
+ return new Promise((resolve) => {
250
+ let buf = '';
251
+ process.stdout.write(promptText);
252
+ lineHandler = (key) => {
253
+ if (key === '\r' || key === '\n') {
254
+ lineHandler = null;
255
+ process.stdout.write('\n');
256
+ resolve(buf);
257
+ } else if (key === '\x7f' || key === '\x08') {
258
+ if (buf.length > 0) { buf = buf.slice(0, -1); process.stdout.write('\x08 \x08'); }
259
+ } else if (key === '\x03') {
260
+ lineHandler = null;
261
+ process.stdout.write('\n');
262
+ resolve('');
263
+ } else if (key >= ' ') {
264
+ buf += key;
265
+ process.stdout.write(key);
266
+ }
267
+ };
268
+ });
269
+ }
270
+
271
+ // ── readChar — single-key confirm prompt ──────────────────────────────────
272
+ function readChar(promptText) {
273
+ return new Promise((resolve) => {
274
+ process.stdout.write(promptText);
275
+ lineHandler = (key) => {
276
+ if (key === '\x03' || key === '\r' || key === '\n' || key >= ' ') {
277
+ lineHandler = null;
278
+ const ch = (key === '\r' || key === '\n' || key === '\x03') ? '' : key;
279
+ process.stdout.write(ch + '\n');
280
+ resolve(ch);
281
+ }
282
+ };
283
+ });
284
+ }
285
+
286
+ // ── Public API ─────────────────────────────────────────────────────────────
287
+
288
+ function start(preloadHistory = []) {
289
+ if (!process.stdin.isTTY) return;
290
+
291
+ history = [...preloadHistory]; // oldest→newest
292
+ histIdx = history.length;
293
+
294
+ process.stdin.setRawMode(true);
295
+ process.stdin.resume();
296
+ process.stdin.setEncoding('utf8');
297
+ process.stdin.on('data', handleKey);
298
+
299
+ process.stdout.on('resize', () => {
300
+ setScrollRegion();
301
+ drawBox();
302
+ });
303
+
304
+ // Push content up to make room for the box at the bottom.
305
+ process.stdout.write('\n'.repeat(BOX_LINES));
306
+ setScrollRegion();
307
+ goOutputArea();
308
+ drawBox();
309
+ }
310
+
311
+ function pause() {
312
+ processing = true;
313
+ goOutputArea(); // cursor into scroll region, ready for output
314
+ }
315
+
316
+ function resume() {
317
+ processing = false;
318
+ drawBox(); // cursor back into input box
319
+ }
320
+
321
+ function setPlanMode(val) {
322
+ planMode = val;
323
+ if (!processing) drawBox();
324
+ }
325
+
326
+ function setContinuation(val) {
327
+ continuation = val;
328
+ if (!processing) drawBox();
329
+ }
330
+
331
+ function wasInterrupted() {
332
+ const v = _interrupted;
333
+ _interrupted = false;
334
+ return v;
335
+ }
336
+
337
+ function exitUI() {
338
+ resetScrollRegion();
339
+ // Clear the box lines so the terminal is clean on exit.
340
+ const r = rows();
341
+ for (let i = 0; i < BOX_LINES; i++) {
342
+ process.stdout.write(`\x1b[${r - BOX_LINES + 1 + i};1H\x1b[2K`);
343
+ }
344
+ process.stdout.write(`\x1b[${r - BOX_LINES + 1};1H`);
345
+ try { process.stdin.setRawMode(false); } catch {}
346
+ }
347
+
348
+ function stop(code = 0) {
349
+ exitUI();
350
+ process.exit(code);
351
+ }
352
+
353
+ return { start, pause, resume, setPlanMode, setContinuation, readLine, readChar, wasInterrupted, stop, exitUI };
354
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tsunami-code",
3
- "version": "3.10.0",
3
+ "version": "3.11.0",
4
4
  "description": "Tsunami Code CLI — AI coding agent by Keystone World Management Navy Seal Unit XI3",
5
5
  "type": "module",
6
6
  "bin": {