tsunami-code 3.11.0 → 3.11.2

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 +1 -0
  2. package/lib/ui.js +86 -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(`Tsunami Code CLI v${VERSION}`);
511
512
 
512
513
  // ── Memory commands ───────────────────────────────────────────────────────────
513
514
  async function handleMemoryCommand(args) {
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 = 5; // top border + input + bottom border + footer label + blank row
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,39 @@ 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 - 4; // top border ╭─────╮
56
+ const rI = r - 3; // input line │ ❯ │
57
+ const rB = r - 2; // bottom border ╰─────╯
58
+ const rM = r - 1; // footer label
59
+ // r // blank spacing row
54
60
 
55
- // Save cursor so we can restore it after drawing.
56
- process.stdout.write('\x1b[s');
61
+ process.stdout.write('\x1b[s'); // save cursor
57
62
 
58
- // Top border ─────────────────────────────────────────
63
+ // Top border ─────────────────────────────────────────────────────
59
64
  const label = ' tsunami ';
60
65
  const rightFill = Math.max(0, w - 2 - 1 - label.length);
61
66
  const topLine = `╭─${label}${'─'.repeat(rightFill)}╮`;
62
- process.stdout.write(`\x1b[${r1};1H\x1b[2K${topLine.slice(0, w)}`);
67
+ process.stdout.write(`\x1b[${rT};1H\x1b[2K${topLine.slice(0, w)}`);
63
68
 
64
- // Input line ──────────────────────────────────────────
69
+ // Input line ──────────────────────────────────────────────────────
65
70
  let prefix;
66
71
  if (continuation) prefix = ` ${dim('···')} `;
67
72
  else if (planMode) prefix = ` ${yellow('❯')} ${dim('[plan]')} `;
68
73
  else prefix = ` ${cyan('❯')} `;
69
74
 
70
- // Strip ANSI codes to measure visible width of prefix.
71
75
  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
76
+ const innerW = w - 2;
77
+ const maxInput = Math.max(0, innerW - prefixVis.length - 1);
74
78
 
75
- // Slide window so cursor stays visible.
76
79
  let dispInput = inputBuf;
77
80
  let dispCursor = cursorPos;
78
81
  if (dispInput.length > maxInput) {
@@ -82,15 +85,22 @@ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit
82
85
  }
83
86
  const display = dispInput.slice(0, maxInput);
84
87
  const pad = ' '.repeat(Math.max(0, maxInput - display.length));
85
- process.stdout.write(`\x1b[${r2};1H\x1b[2K│${prefix}${display}${pad} │`);
88
+ process.stdout.write(`\x1b[${rI};1H\x1b[2K│${prefix}${display}${pad} │`);
86
89
 
87
- // Bottom border ───────────────────────────────────────
90
+ // Bottom border ───────────────────────────────────────────────────
88
91
  const botLine = `╰${'─'.repeat(w - 2)}╯`;
89
- process.stdout.write(`\x1b[${r3};1H\x1b[2K${botLine.slice(0, w)}`);
92
+ process.stdout.write(`\x1b[${rB};1H\x1b[2K${botLine.slice(0, w)}`);
93
+
94
+ // Footer label (version string) ───────────────────────────────────
95
+ const ml = modelLabel ? ` ${dim(modelLabel)}` : '';
96
+ process.stdout.write(`\x1b[${rM};1H\x1b[2K${ml}`);
97
+
98
+ // Blank spacing row at very bottom ────────────────────────────────
99
+ process.stdout.write(`\x1b[${r};1H\x1b[2K`);
90
100
 
91
- // Restore cursor to correct column in the input row.
101
+ // Position cursor in input line at correct column ─────────────────
92
102
  const curCol = 1 + prefixVis.length + 1 + Math.min(dispCursor, display.length);
93
- process.stdout.write(`\x1b[${r2};${curCol}H`);
103
+ process.stdout.write(`\x1b[${rI};${curCol}H`);
94
104
  }
95
105
 
96
106
  // ── Raw key handler ────────────────────────────────────────────────────────
@@ -98,10 +108,9 @@ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit
98
108
  function handleKey(chunk) {
99
109
  const key = chunk.toString('utf8');
100
110
 
101
- // Delegate to temporary line handler (readLine / readChar in progress).
102
111
  if (lineHandler) { lineHandler(key); return; }
103
112
 
104
- // Ctrl+C ──────────────────────────────────────────────
113
+ // Ctrl+C
105
114
  if (key === '\x03') {
106
115
  if (processing) {
107
116
  _interrupted = true;
@@ -117,15 +126,15 @@ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit
117
126
  return;
118
127
  }
119
128
 
120
- // Ctrl+D ──────────────────────────────────────────────
129
+ // Ctrl+D
121
130
  if (key === '\x04') {
122
131
  if (!processing && inputBuf === '') { exitUI(); onExit(0); }
123
132
  return;
124
133
  }
125
134
 
126
- if (processing) return; // swallow all other keys while model is running
135
+ if (processing) return;
127
136
 
128
- // Enter ───────────────────────────────────────────────
137
+ // Enter
129
138
  if (key === '\r' || key === '\n') {
130
139
  const line = inputBuf;
131
140
  if (line || continuation) {
@@ -144,7 +153,7 @@ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit
144
153
  return;
145
154
  }
146
155
 
147
- // Backspace ───────────────────────────────────────────
156
+ // Backspace
148
157
  if (key === '\x7f' || key === '\x08') {
149
158
  if (cursorPos > 0) {
150
159
  inputBuf = inputBuf.slice(0, cursorPos - 1) + inputBuf.slice(cursorPos);
@@ -154,7 +163,7 @@ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit
154
163
  return;
155
164
  }
156
165
 
157
- // Tab ─────────────────────────────────────────────────
166
+ // Tab
158
167
  if (key === '\t') {
159
168
  if (!onTab) return;
160
169
  const [hits] = onTab(inputBuf);
@@ -165,7 +174,6 @@ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit
165
174
  drawBox();
166
175
  return;
167
176
  }
168
- // Multiple hits — show list and fill common prefix.
169
177
  const prefix = hits.reduce((a, b) => {
170
178
  let i = 0;
171
179
  while (i < a.length && i < b.length && a[i] === b[i]) i++;
@@ -182,8 +190,8 @@ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit
182
190
  return;
183
191
  }
184
192
 
185
- // Arrow / navigation escape sequences ─────────────────
186
- if (key === '\x1b[A') { // up — history back
193
+ // Arrow keys / navigation
194
+ if (key === '\x1b[A') {
187
195
  if (histIdx > 0) {
188
196
  if (histIdx === history.length) histDraft = inputBuf;
189
197
  histIdx--;
@@ -193,7 +201,7 @@ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit
193
201
  }
194
202
  return;
195
203
  }
196
- if (key === '\x1b[B') { // down — history forward
204
+ if (key === '\x1b[B') {
197
205
  if (histIdx < history.length) {
198
206
  histIdx++;
199
207
  inputBuf = histIdx === history.length ? histDraft : history[histIdx] || '';
@@ -202,11 +210,11 @@ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit
202
210
  }
203
211
  return;
204
212
  }
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
213
+ if (key === '\x1b[D') { cursorPos = Math.max(0, cursorPos - 1); drawBox(); return; }
214
+ if (key === '\x1b[C') { cursorPos = Math.min(inputBuf.length, cursorPos+1); drawBox(); return; }
215
+ if (key === '\x1b[H' || key === '\x01') { cursorPos = 0; drawBox(); return; }
216
+ if (key === '\x1b[F' || key === '\x05') { cursorPos = inputBuf.length; drawBox(); return; }
217
+ if (key === '\x1b[3~') {
210
218
  if (cursorPos < inputBuf.length) {
211
219
  inputBuf = inputBuf.slice(0, cursorPos) + inputBuf.slice(cursorPos + 1);
212
220
  drawBox();
@@ -214,10 +222,10 @@ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit
214
222
  return;
215
223
  }
216
224
 
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
225
+ // Editing shortcuts
226
+ if (key === '\x0b') { inputBuf = inputBuf.slice(0, cursorPos); drawBox(); return; }
227
+ if (key === '\x15') { inputBuf = inputBuf.slice(cursorPos); cursorPos = 0; drawBox(); return; }
228
+ if (key === '\x17') {
221
229
  const before = inputBuf.slice(0, cursorPos).trimEnd();
222
230
  const lastSpace = before.lastIndexOf(' ');
223
231
  const newBefore = lastSpace < 0 ? '' : before.slice(0, lastSpace + 1);
@@ -226,17 +234,15 @@ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit
226
234
  drawBox();
227
235
  return;
228
236
  }
229
- if (key === '\x0c') { // Ctrl+L — clear screen
237
+ if (key === '\x0c') {
230
238
  process.stdout.write('\x1b[2J\x1b[1;1H');
231
239
  setScrollRegion();
232
240
  drawBox();
233
241
  return;
234
242
  }
235
243
 
236
- // Ignore unhandled escape sequences.
237
244
  if (key.startsWith('\x1b')) return;
238
245
 
239
- // Printable chars (including multi-byte UTF-8).
240
246
  if (key >= ' ' || key.charCodeAt(0) > 127) {
241
247
  inputBuf = inputBuf.slice(0, cursorPos) + key + inputBuf.slice(cursorPos);
242
248
  cursorPos += key.length;
@@ -244,7 +250,8 @@ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit
244
250
  }
245
251
  }
246
252
 
247
- // ── readLine full line read during processing (AskUser tool) ────────────
253
+ // ── readLine / readChar ────────────────────────────────────────────────────
254
+
248
255
  function readLine(promptText) {
249
256
  return new Promise((resolve) => {
250
257
  let buf = '';
@@ -268,7 +275,6 @@ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit
268
275
  });
269
276
  }
270
277
 
271
- // ── readChar — single-key confirm prompt ──────────────────────────────────
272
278
  function readChar(promptText) {
273
279
  return new Promise((resolve) => {
274
280
  process.stdout.write(promptText);
@@ -288,7 +294,7 @@ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit
288
294
  function start(preloadHistory = []) {
289
295
  if (!process.stdin.isTTY) return;
290
296
 
291
- history = [...preloadHistory]; // oldest→newest
297
+ history = [...preloadHistory];
292
298
  histIdx = history.length;
293
299
 
294
300
  process.stdin.setRawMode(true);
@@ -301,7 +307,7 @@ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit
301
307
  drawBox();
302
308
  });
303
309
 
304
- // Push content up to make room for the box at the bottom.
310
+ // Push existing content up to make room for the box.
305
311
  process.stdout.write('\n'.repeat(BOX_LINES));
306
312
  setScrollRegion();
307
313
  goOutputArea();
@@ -310,12 +316,12 @@ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit
310
316
 
311
317
  function pause() {
312
318
  processing = true;
313
- goOutputArea(); // cursor into scroll region, ready for output
319
+ goOutputArea();
314
320
  }
315
321
 
316
322
  function resume() {
317
323
  processing = false;
318
- drawBox(); // cursor back into input box
324
+ drawBox();
319
325
  }
320
326
 
321
327
  function setPlanMode(val) {
@@ -328,6 +334,11 @@ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit
328
334
  if (!processing) drawBox();
329
335
  }
330
336
 
337
+ function setModelLabel(label) {
338
+ modelLabel = label;
339
+ if (!processing) drawBox();
340
+ }
341
+
331
342
  function wasInterrupted() {
332
343
  const v = _interrupted;
333
344
  _interrupted = false;
@@ -336,7 +347,6 @@ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit
336
347
 
337
348
  function exitUI() {
338
349
  resetScrollRegion();
339
- // Clear the box lines so the terminal is clean on exit.
340
350
  const r = rows();
341
351
  for (let i = 0; i < BOX_LINES; i++) {
342
352
  process.stdout.write(`\x1b[${r - BOX_LINES + 1 + i};1H\x1b[2K`);
@@ -350,5 +360,10 @@ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit
350
360
  process.exit(code);
351
361
  }
352
362
 
353
- return { start, pause, resume, setPlanMode, setContinuation, readLine, readChar, wasInterrupted, stop, exitUI };
363
+ return {
364
+ start, pause, resume,
365
+ setPlanMode, setContinuation, setModelLabel,
366
+ readLine, readChar,
367
+ wasInterrupted, stop, exitUI,
368
+ };
354
369
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tsunami-code",
3
- "version": "3.11.0",
3
+ "version": "3.11.2",
4
4
  "description": "Tsunami Code CLI — AI coding agent by Keystone World Management Navy Seal Unit XI3",
5
5
  "type": "module",
6
6
  "bin": {