tsunami-code 3.10.0 → 3.11.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.
Files changed (3) hide show
  1. package/index.js +39 -75
  2. package/lib/ui.js +365 -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,24 @@ 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
+ ui.setModelLabel(getModel());
512
+
531
513
  // ── Memory commands ───────────────────────────────────────────────────────────
532
514
  async function handleMemoryCommand(args) {
533
515
  const sub = args[0]?.toLowerCase();
@@ -627,36 +609,32 @@ async function run() {
627
609
  let mlBuffer = [];
628
610
  let mlHeredoc = false;
629
611
 
630
- rl.on('line', async (input) => {
612
+ async function handleLine(input) {
631
613
  // ── Multiline: heredoc mode (""" toggle) ──────────────────────────────────
632
614
  if (input.trimStart() === '"""' || input.trimStart() === "'''") {
633
615
  if (!mlHeredoc) {
634
616
  mlHeredoc = true;
635
- rl.setPrompt(dim('··· '));
636
- rl.prompt();
617
+ ui.setContinuation(true);
637
618
  return;
638
619
  } else {
639
620
  // Close heredoc — submit accumulated buffer
640
621
  mlHeredoc = false;
641
622
  const assembled = mlBuffer.join('\n');
642
623
  mlBuffer = [];
643
- rl.setPrompt(planMode ? yellow('❯ [plan] ') : cyan('❯ '));
644
- // Fall through with assembled as the input
624
+ ui.setContinuation(false);
645
625
  return _handleInput(assembled);
646
626
  }
647
627
  }
648
628
 
649
629
  if (mlHeredoc) {
650
630
  mlBuffer.push(input);
651
- rl.prompt();
652
631
  return;
653
632
  }
654
633
 
655
634
  // ── Multiline: backslash continuation ─────────────────────────────────────
656
635
  if (input.endsWith('\\')) {
657
636
  mlBuffer.push(input.slice(0, -1));
658
- rl.setPrompt(dim('··· '));
659
- rl.prompt();
637
+ ui.setContinuation(true);
660
638
  return;
661
639
  }
662
640
 
@@ -665,21 +643,20 @@ async function run() {
665
643
  mlBuffer.push(input);
666
644
  const assembled = mlBuffer.join('\n');
667
645
  mlBuffer = [];
668
- rl.setPrompt(planMode ? yellow('❯ [plan] ') : cyan('❯ '));
646
+ ui.setContinuation(false);
669
647
  return _handleInput(assembled);
670
648
  }
671
649
 
672
650
  return _handleInput(input);
673
- });
651
+ }
674
652
 
675
653
  async function _handleInput(input) {
676
654
  const line = input.trim();
677
- if (!line) { rl.prompt(); return; }
655
+ if (!line) { return; }
678
656
 
679
657
  // Append to persistent history
680
658
  if (!line.startsWith('/')) {
681
659
  appendHistory(line);
682
- historyIdx = -1;
683
660
  }
684
661
 
685
662
  if (line.startsWith('/')) {
@@ -696,7 +673,7 @@ async function run() {
696
673
  : skillMatch.prompt;
697
674
  messages.push({ role: 'user', content: userContent });
698
675
  const fullMessages = [{ role: 'system', content: systemPrompt }, ...messages];
699
- rl.pause(); isProcessing = true;
676
+ ui.pause(); isProcessing = true;
700
677
  process.stdout.write('\n' + dim(` ◈ Running skill: ${skillMatch.skill.name}\n\n`));
701
678
  let firstToken = true;
702
679
  try {
@@ -704,10 +681,10 @@ async function run() {
704
681
  if (firstToken) { process.stdout.write(' '); firstToken = false; }
705
682
  process.stdout.write(token);
706
683
  }, (name, args) => { printToolCall(name, args); firstToken = true; },
707
- { sessionDir, cwd, planMode }, makeConfirmCallback(rl));
684
+ { sessionDir, cwd, planMode }, makeConfirmCallback(ui));
708
685
  process.stdout.write('\n\n');
709
686
  } catch(e) { console.error(red(` Error: ${e.message}\n`)); }
710
- isProcessing = false; rl.resume(); rl.prompt();
687
+ isProcessing = false; ui.resume();
711
688
  return;
712
689
  }
713
690
 
@@ -758,7 +735,7 @@ async function run() {
758
735
  planMode = !planMode;
759
736
  if (planMode) console.log(yellow(' Plan mode ON — read-only, no writes or execution.\n'));
760
737
  else console.log(green(' Plan mode OFF — full capabilities restored.\n'));
761
- rl.setPrompt(planMode ? yellow('❯ [plan] ') : cyan('❯ '));
738
+ ui.setPlanMode(planMode);
762
739
  break;
763
740
  case 'undo': {
764
741
  const restored = undo();
@@ -997,6 +974,7 @@ async function run() {
997
974
  case 'model':
998
975
  if (rest[0]) {
999
976
  setModel(rest[0]);
977
+ ui.setModelLabel(rest[0]);
1000
978
  console.log(green(` Model changed to: ${rest[0]}\n`));
1001
979
  } else {
1002
980
  console.log(dim(` Current model: ${getModel()}\n`));
@@ -1174,7 +1152,7 @@ Output ONLY the TSUNAMI.md content, starting with "# Project: <name>"`;
1174
1152
  default:
1175
1153
  console.log(red(` Unknown command: /${cmd}\n`));
1176
1154
  }
1177
- rl.prompt();
1155
+ ui.resume();
1178
1156
  return;
1179
1157
  }
1180
1158
 
@@ -1206,7 +1184,7 @@ Output ONLY the TSUNAMI.md content, starting with "# Project: <name>"`;
1206
1184
  ];
1207
1185
  messages.push({ role: 'user', content: userContent });
1208
1186
 
1209
- rl.pause();
1187
+ ui.pause();
1210
1188
  isProcessing = true;
1211
1189
  process.stdout.write('\n');
1212
1190
 
@@ -1243,7 +1221,7 @@ Output ONLY the TSUNAMI.md content, starting with "# Project: <name>"`;
1243
1221
  firstToken = true;
1244
1222
  },
1245
1223
  { sessionDir, cwd, planMode },
1246
- makeConfirmCallback(rl)
1224
+ makeConfirmCallback(ui)
1247
1225
  );
1248
1226
  spinner.stop();
1249
1227
  flushHighlighter(highlight);
@@ -1286,27 +1264,13 @@ Output ONLY the TSUNAMI.md content, starting with "# Project: <name>"`;
1286
1264
  gracefulExit(0);
1287
1265
  return;
1288
1266
  }
1289
- rl.resume();
1290
- rl.prompt();
1267
+ ui.resume();
1291
1268
  }
1292
1269
 
1270
+ // SIGINT from kill -INT (keyboard Ctrl+C is handled in raw mode inside ui.js)
1293
1271
  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
- }
1272
+ if (!isProcessing) gracefulExit(0);
1273
+ else pendingClose = true;
1310
1274
  });
1311
1275
  }
1312
1276
 
package/lib/ui.js ADDED
@@ -0,0 +1,365 @@
1
+ // lib/ui.js — sticky-bottom terminal UI
2
+ // Pins a 4-line area at the bottom of the terminal:
3
+ // ╭─ tsunami ───────────────────────────────────────────────╮
4
+ // │ ❯ input │
5
+ // ╰─────────────────────────────────────────────────────────╯
6
+ // model-name (dim)
7
+ // All output (replies, tool calls, spinner) scrolls above.
8
+ // Uses ANSI scroll region so the bottom box never scrolls away.
9
+
10
+ import chalk from 'chalk';
11
+
12
+ const cyan = (s) => chalk.cyan(s);
13
+ const yellow = (s) => chalk.yellow(s);
14
+ const dim = (s) => chalk.dim(s);
15
+
16
+ const BOX_LINES = 4; // top border + input + bottom border + model label
17
+
18
+ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit }) {
19
+ let planMode = initPlanMode;
20
+ let continuation = false;
21
+ let modelLabel = '';
22
+ let inputBuf = '';
23
+ let cursorPos = 0;
24
+ let history = [];
25
+ let histIdx = 0;
26
+ let histDraft = '';
27
+ let processing = false;
28
+ let lineHandler = null;
29
+ let _interrupted = false;
30
+
31
+ const rows = () => process.stdout.rows || 24;
32
+ const cols = () => process.stdout.columns || 80;
33
+
34
+ // ── Scroll region ──────────────────────────────────────────────────────────
35
+ // Rows 1 .. (rows-BOX_LINES) scroll. The bottom BOX_LINES rows stay fixed.
36
+
37
+ function setScrollRegion() {
38
+ process.stdout.write(`\x1b[1;${rows() - BOX_LINES}r`);
39
+ }
40
+
41
+ function resetScrollRegion() {
42
+ process.stdout.write('\x1b[r');
43
+ }
44
+
45
+ function goOutputArea() {
46
+ process.stdout.write(`\x1b[${rows() - BOX_LINES};1H`);
47
+ }
48
+
49
+ // ── Box drawing ────────────────────────────────────────────────────────────
50
+ // Draws using absolute row numbers from the actual terminal bottom.
51
+
52
+ function drawBox() {
53
+ const w = cols();
54
+ const r = rows(); // last row of terminal
55
+ const rT = r - 3; // top border ╭─────╮
56
+ const rI = r - 2; // input line │ ❯ │
57
+ const rB = r - 1; // bottom border ╰─────╯
58
+ const rM = r; // model label
59
+
60
+ process.stdout.write('\x1b[s'); // save cursor
61
+
62
+ // Top border ─────────────────────────────────────────────────────
63
+ const label = ' tsunami ';
64
+ const rightFill = Math.max(0, w - 2 - 1 - label.length);
65
+ const topLine = `╭─${label}${'─'.repeat(rightFill)}╮`;
66
+ process.stdout.write(`\x1b[${rT};1H\x1b[2K${topLine.slice(0, w)}`);
67
+
68
+ // Input line ──────────────────────────────────────────────────────
69
+ let prefix;
70
+ if (continuation) prefix = ` ${dim('···')} `;
71
+ else if (planMode) prefix = ` ${yellow('❯')} ${dim('[plan]')} `;
72
+ else prefix = ` ${cyan('❯')} `;
73
+
74
+ const prefixVis = prefix.replace(/\x1b\[[0-9;]*m/g, '');
75
+ const innerW = w - 2;
76
+ const maxInput = Math.max(0, innerW - prefixVis.length - 1);
77
+
78
+ let dispInput = inputBuf;
79
+ let dispCursor = cursorPos;
80
+ if (dispInput.length > maxInput) {
81
+ const start = Math.max(0, cursorPos - maxInput + 1);
82
+ dispInput = inputBuf.slice(start);
83
+ dispCursor = cursorPos - start;
84
+ }
85
+ const display = dispInput.slice(0, maxInput);
86
+ const pad = ' '.repeat(Math.max(0, maxInput - display.length));
87
+ process.stdout.write(`\x1b[${rI};1H\x1b[2K│${prefix}${display}${pad} │`);
88
+
89
+ // Bottom border ───────────────────────────────────────────────────
90
+ const botLine = `╰${'─'.repeat(w - 2)}╯`;
91
+ process.stdout.write(`\x1b[${rB};1H\x1b[2K${botLine.slice(0, w)}`);
92
+
93
+ // Model label ─────────────────────────────────────────────────────
94
+ const ml = modelLabel ? ` ${dim(modelLabel)}` : '';
95
+ process.stdout.write(`\x1b[${rM};1H\x1b[2K${ml}`);
96
+
97
+ // Position cursor in input line at correct column ─────────────────
98
+ const curCol = 1 + prefixVis.length + 1 + Math.min(dispCursor, display.length);
99
+ process.stdout.write(`\x1b[${rI};${curCol}H`);
100
+ }
101
+
102
+ // ── Raw key handler ────────────────────────────────────────────────────────
103
+
104
+ function handleKey(chunk) {
105
+ const key = chunk.toString('utf8');
106
+
107
+ if (lineHandler) { lineHandler(key); return; }
108
+
109
+ // Ctrl+C
110
+ if (key === '\x03') {
111
+ if (processing) {
112
+ _interrupted = true;
113
+ process.stdout.write(dim('\n (interrupted)\n'));
114
+ } else if (inputBuf !== '') {
115
+ inputBuf = '';
116
+ cursorPos = 0;
117
+ drawBox();
118
+ } else {
119
+ exitUI();
120
+ onExit(0);
121
+ }
122
+ return;
123
+ }
124
+
125
+ // Ctrl+D
126
+ if (key === '\x04') {
127
+ if (!processing && inputBuf === '') { exitUI(); onExit(0); }
128
+ return;
129
+ }
130
+
131
+ if (processing) return;
132
+
133
+ // Enter
134
+ if (key === '\r' || key === '\n') {
135
+ const line = inputBuf;
136
+ if (line || continuation) {
137
+ if (line) {
138
+ history.push(line);
139
+ histIdx = history.length;
140
+ histDraft = '';
141
+ }
142
+ inputBuf = '';
143
+ cursorPos = 0;
144
+ drawBox();
145
+ goOutputArea();
146
+ process.stdout.write('\n');
147
+ onLine(line);
148
+ }
149
+ return;
150
+ }
151
+
152
+ // Backspace
153
+ if (key === '\x7f' || key === '\x08') {
154
+ if (cursorPos > 0) {
155
+ inputBuf = inputBuf.slice(0, cursorPos - 1) + inputBuf.slice(cursorPos);
156
+ cursorPos--;
157
+ drawBox();
158
+ }
159
+ return;
160
+ }
161
+
162
+ // Tab
163
+ if (key === '\t') {
164
+ if (!onTab) return;
165
+ const [hits] = onTab(inputBuf);
166
+ if (!hits.length) return;
167
+ if (hits.length === 1) {
168
+ inputBuf = hits[0];
169
+ cursorPos = hits[0].length;
170
+ drawBox();
171
+ return;
172
+ }
173
+ const prefix = hits.reduce((a, b) => {
174
+ let i = 0;
175
+ while (i < a.length && i < b.length && a[i] === b[i]) i++;
176
+ return a.slice(0, i);
177
+ });
178
+ goOutputArea();
179
+ const shown = hits.slice(0, 8).join(' ') + (hits.length > 8 ? ` …+${hits.length - 8}` : '');
180
+ process.stdout.write('\n ' + dim(shown) + '\n');
181
+ if (prefix.length > inputBuf.length) {
182
+ inputBuf = prefix;
183
+ cursorPos = prefix.length;
184
+ }
185
+ drawBox();
186
+ return;
187
+ }
188
+
189
+ // Arrow keys / navigation
190
+ if (key === '\x1b[A') {
191
+ if (histIdx > 0) {
192
+ if (histIdx === history.length) histDraft = inputBuf;
193
+ histIdx--;
194
+ inputBuf = history[histIdx] || '';
195
+ cursorPos = inputBuf.length;
196
+ drawBox();
197
+ }
198
+ return;
199
+ }
200
+ if (key === '\x1b[B') {
201
+ if (histIdx < history.length) {
202
+ histIdx++;
203
+ inputBuf = histIdx === history.length ? histDraft : history[histIdx] || '';
204
+ cursorPos = inputBuf.length;
205
+ drawBox();
206
+ }
207
+ return;
208
+ }
209
+ if (key === '\x1b[D') { cursorPos = Math.max(0, cursorPos - 1); drawBox(); return; }
210
+ if (key === '\x1b[C') { cursorPos = Math.min(inputBuf.length, cursorPos+1); drawBox(); return; }
211
+ if (key === '\x1b[H' || key === '\x01') { cursorPos = 0; drawBox(); return; }
212
+ if (key === '\x1b[F' || key === '\x05') { cursorPos = inputBuf.length; drawBox(); return; }
213
+ if (key === '\x1b[3~') {
214
+ if (cursorPos < inputBuf.length) {
215
+ inputBuf = inputBuf.slice(0, cursorPos) + inputBuf.slice(cursorPos + 1);
216
+ drawBox();
217
+ }
218
+ return;
219
+ }
220
+
221
+ // Editing shortcuts
222
+ if (key === '\x0b') { inputBuf = inputBuf.slice(0, cursorPos); drawBox(); return; }
223
+ if (key === '\x15') { inputBuf = inputBuf.slice(cursorPos); cursorPos = 0; drawBox(); return; }
224
+ if (key === '\x17') {
225
+ const before = inputBuf.slice(0, cursorPos).trimEnd();
226
+ const lastSpace = before.lastIndexOf(' ');
227
+ const newBefore = lastSpace < 0 ? '' : before.slice(0, lastSpace + 1);
228
+ inputBuf = newBefore + inputBuf.slice(cursorPos);
229
+ cursorPos = newBefore.length;
230
+ drawBox();
231
+ return;
232
+ }
233
+ if (key === '\x0c') {
234
+ process.stdout.write('\x1b[2J\x1b[1;1H');
235
+ setScrollRegion();
236
+ drawBox();
237
+ return;
238
+ }
239
+
240
+ if (key.startsWith('\x1b')) return;
241
+
242
+ if (key >= ' ' || key.charCodeAt(0) > 127) {
243
+ inputBuf = inputBuf.slice(0, cursorPos) + key + inputBuf.slice(cursorPos);
244
+ cursorPos += key.length;
245
+ drawBox();
246
+ }
247
+ }
248
+
249
+ // ── readLine / readChar ────────────────────────────────────────────────────
250
+
251
+ function readLine(promptText) {
252
+ return new Promise((resolve) => {
253
+ let buf = '';
254
+ process.stdout.write(promptText);
255
+ lineHandler = (key) => {
256
+ if (key === '\r' || key === '\n') {
257
+ lineHandler = null;
258
+ process.stdout.write('\n');
259
+ resolve(buf);
260
+ } else if (key === '\x7f' || key === '\x08') {
261
+ if (buf.length > 0) { buf = buf.slice(0, -1); process.stdout.write('\x08 \x08'); }
262
+ } else if (key === '\x03') {
263
+ lineHandler = null;
264
+ process.stdout.write('\n');
265
+ resolve('');
266
+ } else if (key >= ' ') {
267
+ buf += key;
268
+ process.stdout.write(key);
269
+ }
270
+ };
271
+ });
272
+ }
273
+
274
+ function readChar(promptText) {
275
+ return new Promise((resolve) => {
276
+ process.stdout.write(promptText);
277
+ lineHandler = (key) => {
278
+ if (key === '\x03' || key === '\r' || key === '\n' || key >= ' ') {
279
+ lineHandler = null;
280
+ const ch = (key === '\r' || key === '\n' || key === '\x03') ? '' : key;
281
+ process.stdout.write(ch + '\n');
282
+ resolve(ch);
283
+ }
284
+ };
285
+ });
286
+ }
287
+
288
+ // ── Public API ─────────────────────────────────────────────────────────────
289
+
290
+ function start(preloadHistory = []) {
291
+ if (!process.stdin.isTTY) return;
292
+
293
+ history = [...preloadHistory];
294
+ histIdx = history.length;
295
+
296
+ process.stdin.setRawMode(true);
297
+ process.stdin.resume();
298
+ process.stdin.setEncoding('utf8');
299
+ process.stdin.on('data', handleKey);
300
+
301
+ process.stdout.on('resize', () => {
302
+ setScrollRegion();
303
+ drawBox();
304
+ });
305
+
306
+ // Push existing content up to make room for the box.
307
+ process.stdout.write('\n'.repeat(BOX_LINES));
308
+ setScrollRegion();
309
+ goOutputArea();
310
+ drawBox();
311
+ }
312
+
313
+ function pause() {
314
+ processing = true;
315
+ goOutputArea();
316
+ }
317
+
318
+ function resume() {
319
+ processing = false;
320
+ drawBox();
321
+ }
322
+
323
+ function setPlanMode(val) {
324
+ planMode = val;
325
+ if (!processing) drawBox();
326
+ }
327
+
328
+ function setContinuation(val) {
329
+ continuation = val;
330
+ if (!processing) drawBox();
331
+ }
332
+
333
+ function setModelLabel(label) {
334
+ modelLabel = label;
335
+ if (!processing) drawBox();
336
+ }
337
+
338
+ function wasInterrupted() {
339
+ const v = _interrupted;
340
+ _interrupted = false;
341
+ return v;
342
+ }
343
+
344
+ function exitUI() {
345
+ resetScrollRegion();
346
+ const r = rows();
347
+ for (let i = 0; i < BOX_LINES; i++) {
348
+ process.stdout.write(`\x1b[${r - BOX_LINES + 1 + i};1H\x1b[2K`);
349
+ }
350
+ process.stdout.write(`\x1b[${r - BOX_LINES + 1};1H`);
351
+ try { process.stdin.setRawMode(false); } catch {}
352
+ }
353
+
354
+ function stop(code = 0) {
355
+ exitUI();
356
+ process.exit(code);
357
+ }
358
+
359
+ return {
360
+ start, pause, resume,
361
+ setPlanMode, setContinuation, setModelLabel,
362
+ readLine, readChar,
363
+ wasInterrupted, stop, exitUI,
364
+ };
365
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tsunami-code",
3
- "version": "3.10.0",
3
+ "version": "3.11.1",
4
4
  "description": "Tsunami Code CLI — AI coding agent by Keystone World Management Navy Seal Unit XI3",
5
5
  "type": "module",
6
6
  "bin": {