tsunami-code 3.11.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 +2 -0
  2. package/lib/ui.js +82 -71
  3. package/package.json +1 -1
package/index.js CHANGED
@@ -508,6 +508,7 @@ async function run() {
508
508
  });
509
509
 
510
510
  ui.start(historyEntries);
511
+ ui.setModelLabel(getModel());
511
512
 
512
513
  // ── Memory commands ───────────────────────────────────────────────────────────
513
514
  async function handleMemoryCommand(args) {
@@ -973,6 +974,7 @@ async function run() {
973
974
  case 'model':
974
975
  if (rest[0]) {
975
976
  setModel(rest[0]);
977
+ ui.setModelLabel(rest[0]);
976
978
  console.log(green(` Model changed to: ${rest[0]}\n`));
977
979
  } else {
978
980
  console.log(dim(` Current model: ${getModel()}\n`));
package/lib/ui.js CHANGED
@@ -1,34 +1,38 @@
1
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.
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.
5
9
 
6
10
  import chalk from 'chalk';
7
11
 
8
- const cyan = (s) => chalk.cyan(s);
12
+ const cyan = (s) => chalk.cyan(s);
9
13
  const yellow = (s) => chalk.yellow(s);
10
- const dim = (s) => chalk.dim(s);
14
+ const dim = (s) => chalk.dim(s);
11
15
 
12
- const BOX_LINES = 3; // ╭─ header ─╮ / input / ╰──────────╯
16
+ const BOX_LINES = 4; // top border + input + bottom border + model label
13
17
 
14
18
  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;
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;
27
32
  const cols = () => process.stdout.columns || 80;
28
33
 
29
34
  // ── 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.
35
+ // Rows 1 .. (rows-BOX_LINES) scroll. The bottom BOX_LINES rows stay fixed.
32
36
 
33
37
  function setScrollRegion() {
34
38
  process.stdout.write(`\x1b[1;${rows() - BOX_LINES}r`);
@@ -39,40 +43,38 @@ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit
39
43
  }
40
44
 
41
45
  function goOutputArea() {
42
- // Place cursor at the last line of the scroll region so next write lands there.
43
46
  process.stdout.write(`\x1b[${rows() - BOX_LINES};1H`);
44
47
  }
45
48
 
46
49
  // ── Box drawing ────────────────────────────────────────────────────────────
50
+ // Draws using absolute row numbers from the actual terminal bottom.
47
51
 
48
52
  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
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
54
59
 
55
- // Save cursor so we can restore it after drawing.
56
- process.stdout.write('\x1b[s');
60
+ process.stdout.write('\x1b[s'); // save cursor
57
61
 
58
- // Top border ─────────────────────────────────────────
62
+ // Top border ─────────────────────────────────────────────────────
59
63
  const label = ' tsunami ';
60
64
  const rightFill = Math.max(0, w - 2 - 1 - label.length);
61
65
  const topLine = `╭─${label}${'─'.repeat(rightFill)}╮`;
62
- process.stdout.write(`\x1b[${r1};1H\x1b[2K${topLine.slice(0, w)}`);
66
+ process.stdout.write(`\x1b[${rT};1H\x1b[2K${topLine.slice(0, w)}`);
63
67
 
64
- // Input line ──────────────────────────────────────────
68
+ // Input line ──────────────────────────────────────────────────────
65
69
  let prefix;
66
70
  if (continuation) prefix = ` ${dim('···')} `;
67
71
  else if (planMode) prefix = ` ${yellow('❯')} ${dim('[plan]')} `;
68
72
  else prefix = ` ${cyan('❯')} `;
69
73
 
70
- // Strip ANSI codes to measure visible width of prefix.
71
74
  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
75
+ const innerW = w - 2;
76
+ const maxInput = Math.max(0, innerW - prefixVis.length - 1);
74
77
 
75
- // Slide window so cursor stays visible.
76
78
  let dispInput = inputBuf;
77
79
  let dispCursor = cursorPos;
78
80
  if (dispInput.length > maxInput) {
@@ -82,15 +84,19 @@ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit
82
84
  }
83
85
  const display = dispInput.slice(0, maxInput);
84
86
  const pad = ' '.repeat(Math.max(0, maxInput - display.length));
85
- process.stdout.write(`\x1b[${r2};1H\x1b[2K│${prefix}${display}${pad} │`);
87
+ process.stdout.write(`\x1b[${rI};1H\x1b[2K│${prefix}${display}${pad} │`);
86
88
 
87
- // Bottom border ───────────────────────────────────────
89
+ // Bottom border ───────────────────────────────────────────────────
88
90
  const botLine = `╰${'─'.repeat(w - 2)}╯`;
89
- process.stdout.write(`\x1b[${r3};1H\x1b[2K${botLine.slice(0, w)}`);
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}`);
90
96
 
91
- // Restore cursor to correct column in the input row.
97
+ // Position cursor in input line at correct column ─────────────────
92
98
  const curCol = 1 + prefixVis.length + 1 + Math.min(dispCursor, display.length);
93
- process.stdout.write(`\x1b[${r2};${curCol}H`);
99
+ process.stdout.write(`\x1b[${rI};${curCol}H`);
94
100
  }
95
101
 
96
102
  // ── Raw key handler ────────────────────────────────────────────────────────
@@ -98,10 +104,9 @@ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit
98
104
  function handleKey(chunk) {
99
105
  const key = chunk.toString('utf8');
100
106
 
101
- // Delegate to temporary line handler (readLine / readChar in progress).
102
107
  if (lineHandler) { lineHandler(key); return; }
103
108
 
104
- // Ctrl+C ──────────────────────────────────────────────
109
+ // Ctrl+C
105
110
  if (key === '\x03') {
106
111
  if (processing) {
107
112
  _interrupted = true;
@@ -117,15 +122,15 @@ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit
117
122
  return;
118
123
  }
119
124
 
120
- // Ctrl+D ──────────────────────────────────────────────
125
+ // Ctrl+D
121
126
  if (key === '\x04') {
122
127
  if (!processing && inputBuf === '') { exitUI(); onExit(0); }
123
128
  return;
124
129
  }
125
130
 
126
- if (processing) return; // swallow all other keys while model is running
131
+ if (processing) return;
127
132
 
128
- // Enter ───────────────────────────────────────────────
133
+ // Enter
129
134
  if (key === '\r' || key === '\n') {
130
135
  const line = inputBuf;
131
136
  if (line || continuation) {
@@ -144,7 +149,7 @@ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit
144
149
  return;
145
150
  }
146
151
 
147
- // Backspace ───────────────────────────────────────────
152
+ // Backspace
148
153
  if (key === '\x7f' || key === '\x08') {
149
154
  if (cursorPos > 0) {
150
155
  inputBuf = inputBuf.slice(0, cursorPos - 1) + inputBuf.slice(cursorPos);
@@ -154,7 +159,7 @@ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit
154
159
  return;
155
160
  }
156
161
 
157
- // Tab ─────────────────────────────────────────────────
162
+ // Tab
158
163
  if (key === '\t') {
159
164
  if (!onTab) return;
160
165
  const [hits] = onTab(inputBuf);
@@ -165,7 +170,6 @@ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit
165
170
  drawBox();
166
171
  return;
167
172
  }
168
- // Multiple hits — show list and fill common prefix.
169
173
  const prefix = hits.reduce((a, b) => {
170
174
  let i = 0;
171
175
  while (i < a.length && i < b.length && a[i] === b[i]) i++;
@@ -182,8 +186,8 @@ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit
182
186
  return;
183
187
  }
184
188
 
185
- // Arrow / navigation escape sequences ─────────────────
186
- if (key === '\x1b[A') { // up — history back
189
+ // Arrow keys / navigation
190
+ if (key === '\x1b[A') {
187
191
  if (histIdx > 0) {
188
192
  if (histIdx === history.length) histDraft = inputBuf;
189
193
  histIdx--;
@@ -193,7 +197,7 @@ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit
193
197
  }
194
198
  return;
195
199
  }
196
- if (key === '\x1b[B') { // down — history forward
200
+ if (key === '\x1b[B') {
197
201
  if (histIdx < history.length) {
198
202
  histIdx++;
199
203
  inputBuf = histIdx === history.length ? histDraft : history[histIdx] || '';
@@ -202,11 +206,11 @@ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit
202
206
  }
203
207
  return;
204
208
  }
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
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~') {
210
214
  if (cursorPos < inputBuf.length) {
211
215
  inputBuf = inputBuf.slice(0, cursorPos) + inputBuf.slice(cursorPos + 1);
212
216
  drawBox();
@@ -214,10 +218,10 @@ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit
214
218
  return;
215
219
  }
216
220
 
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
+ // 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') {
221
225
  const before = inputBuf.slice(0, cursorPos).trimEnd();
222
226
  const lastSpace = before.lastIndexOf(' ');
223
227
  const newBefore = lastSpace < 0 ? '' : before.slice(0, lastSpace + 1);
@@ -226,17 +230,15 @@ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit
226
230
  drawBox();
227
231
  return;
228
232
  }
229
- if (key === '\x0c') { // Ctrl+L — clear screen
233
+ if (key === '\x0c') {
230
234
  process.stdout.write('\x1b[2J\x1b[1;1H');
231
235
  setScrollRegion();
232
236
  drawBox();
233
237
  return;
234
238
  }
235
239
 
236
- // Ignore unhandled escape sequences.
237
240
  if (key.startsWith('\x1b')) return;
238
241
 
239
- // Printable chars (including multi-byte UTF-8).
240
242
  if (key >= ' ' || key.charCodeAt(0) > 127) {
241
243
  inputBuf = inputBuf.slice(0, cursorPos) + key + inputBuf.slice(cursorPos);
242
244
  cursorPos += key.length;
@@ -244,7 +246,8 @@ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit
244
246
  }
245
247
  }
246
248
 
247
- // ── readLine full line read during processing (AskUser tool) ────────────
249
+ // ── readLine / readChar ────────────────────────────────────────────────────
250
+
248
251
  function readLine(promptText) {
249
252
  return new Promise((resolve) => {
250
253
  let buf = '';
@@ -268,7 +271,6 @@ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit
268
271
  });
269
272
  }
270
273
 
271
- // ── readChar — single-key confirm prompt ──────────────────────────────────
272
274
  function readChar(promptText) {
273
275
  return new Promise((resolve) => {
274
276
  process.stdout.write(promptText);
@@ -288,7 +290,7 @@ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit
288
290
  function start(preloadHistory = []) {
289
291
  if (!process.stdin.isTTY) return;
290
292
 
291
- history = [...preloadHistory]; // oldest→newest
293
+ history = [...preloadHistory];
292
294
  histIdx = history.length;
293
295
 
294
296
  process.stdin.setRawMode(true);
@@ -301,7 +303,7 @@ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit
301
303
  drawBox();
302
304
  });
303
305
 
304
- // Push content up to make room for the box at the bottom.
306
+ // Push existing content up to make room for the box.
305
307
  process.stdout.write('\n'.repeat(BOX_LINES));
306
308
  setScrollRegion();
307
309
  goOutputArea();
@@ -310,12 +312,12 @@ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit
310
312
 
311
313
  function pause() {
312
314
  processing = true;
313
- goOutputArea(); // cursor into scroll region, ready for output
315
+ goOutputArea();
314
316
  }
315
317
 
316
318
  function resume() {
317
319
  processing = false;
318
- drawBox(); // cursor back into input box
320
+ drawBox();
319
321
  }
320
322
 
321
323
  function setPlanMode(val) {
@@ -328,6 +330,11 @@ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit
328
330
  if (!processing) drawBox();
329
331
  }
330
332
 
333
+ function setModelLabel(label) {
334
+ modelLabel = label;
335
+ if (!processing) drawBox();
336
+ }
337
+
331
338
  function wasInterrupted() {
332
339
  const v = _interrupted;
333
340
  _interrupted = false;
@@ -336,7 +343,6 @@ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit
336
343
 
337
344
  function exitUI() {
338
345
  resetScrollRegion();
339
- // Clear the box lines so the terminal is clean on exit.
340
346
  const r = rows();
341
347
  for (let i = 0; i < BOX_LINES; i++) {
342
348
  process.stdout.write(`\x1b[${r - BOX_LINES + 1 + i};1H\x1b[2K`);
@@ -350,5 +356,10 @@ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit
350
356
  process.exit(code);
351
357
  }
352
358
 
353
- return { start, pause, resume, setPlanMode, setContinuation, readLine, readChar, wasInterrupted, stop, exitUI };
359
+ return {
360
+ start, pause, resume,
361
+ setPlanMode, setContinuation, setModelLabel,
362
+ readLine, readChar,
363
+ wasInterrupted, stop, exitUI,
364
+ };
354
365
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tsunami-code",
3
- "version": "3.11.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": {