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.
- package/index.js +1 -0
- package/lib/ui.js +86 -71
- 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
|
-
//
|
|
4
|
-
//
|
|
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
|
|
12
|
+
const cyan = (s) => chalk.cyan(s);
|
|
9
13
|
const yellow = (s) => chalk.yellow(s);
|
|
10
|
-
const dim
|
|
14
|
+
const dim = (s) => chalk.dim(s);
|
|
11
15
|
|
|
12
|
-
const BOX_LINES =
|
|
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
|
|
16
|
-
let continuation
|
|
17
|
-
let
|
|
18
|
-
let
|
|
19
|
-
let
|
|
20
|
-
let
|
|
21
|
-
let
|
|
22
|
-
let
|
|
23
|
-
let
|
|
24
|
-
let
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
//
|
|
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
|
|
50
|
-
const r
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
const
|
|
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
|
-
//
|
|
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[${
|
|
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;
|
|
73
|
-
const maxInput = Math.max(0, innerW - prefixVis.length - 1);
|
|
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[${
|
|
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[${
|
|
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
|
-
//
|
|
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[${
|
|
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;
|
|
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
|
|
186
|
-
if (key === '\x1b[A') {
|
|
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') {
|
|
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; }
|
|
208
|
-
if (key === '\x1b[F' || key === '\x05') { cursorPos = inputBuf.length; drawBox(); return; }
|
|
209
|
-
if (key === '\x1b[3~') {
|
|
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; }
|
|
219
|
-
if (key === '\x15') { inputBuf = inputBuf.slice(cursorPos); cursorPos = 0; drawBox(); return; }
|
|
220
|
-
if (key === '\x17') {
|
|
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') {
|
|
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
|
|
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];
|
|
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
|
|
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();
|
|
319
|
+
goOutputArea();
|
|
314
320
|
}
|
|
315
321
|
|
|
316
322
|
function resume() {
|
|
317
323
|
processing = false;
|
|
318
|
-
drawBox();
|
|
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 {
|
|
363
|
+
return {
|
|
364
|
+
start, pause, resume,
|
|
365
|
+
setPlanMode, setContinuation, setModelLabel,
|
|
366
|
+
readLine, readChar,
|
|
367
|
+
wasInterrupted, stop, exitUI,
|
|
368
|
+
};
|
|
354
369
|
}
|