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.
- package/index.js +2 -0
- package/lib/ui.js +82 -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(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
|
-
//
|
|
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 = 4; // top border + input + bottom border + model label
|
|
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,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
|
|
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 - 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
|
-
//
|
|
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[${
|
|
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;
|
|
73
|
-
const maxInput = Math.max(0, innerW - prefixVis.length - 1);
|
|
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[${
|
|
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[${
|
|
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
|
-
//
|
|
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[${
|
|
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;
|
|
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
|
|
186
|
-
if (key === '\x1b[A') {
|
|
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') {
|
|
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; }
|
|
208
|
-
if (key === '\x1b[F' || key === '\x05') { cursorPos = inputBuf.length; drawBox(); return; }
|
|
209
|
-
if (key === '\x1b[3~') {
|
|
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; }
|
|
219
|
-
if (key === '\x15') { inputBuf = inputBuf.slice(cursorPos); cursorPos = 0; drawBox(); return; }
|
|
220
|
-
if (key === '\x17') {
|
|
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') {
|
|
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
|
|
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];
|
|
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
|
|
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();
|
|
315
|
+
goOutputArea();
|
|
314
316
|
}
|
|
315
317
|
|
|
316
318
|
function resume() {
|
|
317
319
|
processing = false;
|
|
318
|
-
drawBox();
|
|
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 {
|
|
359
|
+
return {
|
|
360
|
+
start, pause, resume,
|
|
361
|
+
setPlanMode, setContinuation, setModelLabel,
|
|
362
|
+
readLine, readChar,
|
|
363
|
+
wasInterrupted, stop, exitUI,
|
|
364
|
+
};
|
|
354
365
|
}
|