tsunami-code 3.11.7 → 3.11.9
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 +57 -29
- package/lib/ui.js +38 -1
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
} from './lib/memory.js';
|
|
27
27
|
import { listMemories, readMemory, saveMemory, deleteMemory, getMemdirPath } from './lib/memdir.js';
|
|
28
28
|
|
|
29
|
-
const VERSION = '3.11.
|
|
29
|
+
const VERSION = '3.11.9';
|
|
30
30
|
const CONFIG_DIR = join(os.homedir(), '.tsunami-code');
|
|
31
31
|
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
32
32
|
const DEFAULT_SERVER = 'https://radiometric-reita-amuck.ngrok-free.dev';
|
|
@@ -129,6 +129,40 @@ function formatBytes(bytes) {
|
|
|
129
129
|
return `${(bytes / 1024 / 1024).toFixed(2)}MB`;
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
// ── Typewriter — drains a char queue at a fixed rate for smooth output ───────
|
|
133
|
+
// charDelay: ms between characters. onFirstChar fires once before first write.
|
|
134
|
+
function createTypewriter(writeFn, charDelay = 12) {
|
|
135
|
+
const queue = [];
|
|
136
|
+
let timer = null;
|
|
137
|
+
let onFirst = null;
|
|
138
|
+
let firstDone = false;
|
|
139
|
+
|
|
140
|
+
function tick() {
|
|
141
|
+
if (queue.length === 0) { timer = null; return; }
|
|
142
|
+
if (!firstDone && onFirst) { onFirst(); firstDone = true; }
|
|
143
|
+
writeFn(queue.shift());
|
|
144
|
+
timer = setTimeout(tick, charDelay);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
setOnFirst(fn) { onFirst = fn; },
|
|
149
|
+
push(text) {
|
|
150
|
+
for (const ch of text) queue.push(ch);
|
|
151
|
+
if (!timer) timer = setTimeout(tick, 0); // start immediately
|
|
152
|
+
},
|
|
153
|
+
drainNow() {
|
|
154
|
+
if (timer) { clearTimeout(timer); timer = null; }
|
|
155
|
+
if (!firstDone && queue.length && onFirst) { onFirst(); firstDone = true; }
|
|
156
|
+
while (queue.length) writeFn(queue.shift());
|
|
157
|
+
},
|
|
158
|
+
reset() {
|
|
159
|
+
if (timer) { clearTimeout(timer); timer = null; }
|
|
160
|
+
queue.length = 0;
|
|
161
|
+
firstDone = false;
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
132
166
|
// ── Syntax highlighter — line-buffered, applied to streaming model output ──────
|
|
133
167
|
// Buffers tokens until newlines, then applies chalk markup per line.
|
|
134
168
|
// createHighlighter(write) returns an onToken(token) function.
|
|
@@ -186,20 +220,6 @@ function createHighlighter(write) {
|
|
|
186
220
|
renderLine(buf.slice(0, nl));
|
|
187
221
|
buf = buf.slice(nl + 1);
|
|
188
222
|
}
|
|
189
|
-
// Flush partial line at word boundaries so text streams word-by-word
|
|
190
|
-
// (skip inside code fences where exact formatting matters)
|
|
191
|
-
if (!inFence && buf.length > 0) {
|
|
192
|
-
const lastSpace = buf.lastIndexOf(' ');
|
|
193
|
-
if (lastSpace > 0) {
|
|
194
|
-
const partial = buf.slice(0, lastSpace + 1);
|
|
195
|
-
const styled = partial
|
|
196
|
-
.replace(/\*\*([^*\n]+)\*\*/g, (_, t) => chalk.bold(t))
|
|
197
|
-
.replace(/__([^_\n]+)__/g, (_, t) => chalk.bold(t))
|
|
198
|
-
.replace(/`([^`\n]+)`/g, (_, t) => chalk.yellow('`' + t + '`'));
|
|
199
|
-
write(styled);
|
|
200
|
-
buf = buf.slice(lastSpace + 1);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
223
|
};
|
|
204
224
|
}
|
|
205
225
|
|
|
@@ -810,12 +830,19 @@ async function run() {
|
|
|
810
830
|
case 'mode': {
|
|
811
831
|
const m = rest[0]?.toLowerCase();
|
|
812
832
|
if (!m) {
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
833
|
+
// Interactive arrow-key picker
|
|
834
|
+
process.stdout.write(blue('\n Select permission mode:\n\n'));
|
|
835
|
+
const currentIdx = PERM_MODES.indexOf(permMode);
|
|
836
|
+
const selected = await ui.selectFromList(PERM_MODES, currentIdx >= 0 ? currentIdx : 0);
|
|
837
|
+
if (selected) {
|
|
838
|
+
permMode = selected;
|
|
839
|
+
planMode = selected === 'readonly';
|
|
840
|
+
ui.setPlanMode(planMode);
|
|
841
|
+
updateModeLabel();
|
|
842
|
+
console.log(green(`\n Mode: ${selected}\n`));
|
|
843
|
+
} else {
|
|
844
|
+
console.log(dim(' Cancelled.\n'));
|
|
817
845
|
}
|
|
818
|
-
console.log(dim('\n Usage: /mode <name>\n'));
|
|
819
846
|
break;
|
|
820
847
|
}
|
|
821
848
|
if (!PERM_MODES.includes(m)) {
|
|
@@ -1290,16 +1317,13 @@ Output ONLY the TSUNAMI.md content, starting with "# Project: <name>"`;
|
|
|
1290
1317
|
let toolTimers = {}; // track per-tool duration
|
|
1291
1318
|
|
|
1292
1319
|
spinner.start();
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
process.stdout.write(' ');
|
|
1299
|
-
firstToken = false;
|
|
1300
|
-
}
|
|
1301
|
-
process.stdout.write(s);
|
|
1320
|
+
const tw = createTypewriter((ch) => process.stdout.write(ch), 12);
|
|
1321
|
+
tw.setOnFirst(() => {
|
|
1322
|
+
spinner.stop();
|
|
1323
|
+
process.stdout.write(' ');
|
|
1324
|
+
firstToken = false;
|
|
1302
1325
|
});
|
|
1326
|
+
const highlight = createHighlighter((s) => tw.push(s));
|
|
1303
1327
|
|
|
1304
1328
|
try {
|
|
1305
1329
|
await agentLoop(
|
|
@@ -1310,18 +1334,22 @@ Output ONLY the TSUNAMI.md content, starting with "# Project: <name>"`;
|
|
|
1310
1334
|
},
|
|
1311
1335
|
(toolName, toolArgs) => {
|
|
1312
1336
|
flushHighlighter(highlight);
|
|
1337
|
+
tw.drainNow();
|
|
1338
|
+
tw.reset();
|
|
1313
1339
|
spinner.stop();
|
|
1314
1340
|
const elapsed = toolTimers[toolName] != null ? Date.now() - toolTimers[toolName] : null;
|
|
1315
1341
|
toolTimers[toolName] = Date.now();
|
|
1316
1342
|
printToolCall(toolName, toolArgs, elapsed);
|
|
1317
1343
|
spinner.start();
|
|
1318
1344
|
firstToken = true;
|
|
1345
|
+
tw.setOnFirst(() => { spinner.stop(); process.stdout.write(' '); firstToken = false; });
|
|
1319
1346
|
},
|
|
1320
1347
|
{ sessionDir, cwd, planMode, permMode },
|
|
1321
1348
|
makeConfirmCallback(ui)
|
|
1322
1349
|
);
|
|
1323
1350
|
spinner.stop();
|
|
1324
1351
|
flushHighlighter(highlight);
|
|
1352
|
+
tw.drainNow();
|
|
1325
1353
|
|
|
1326
1354
|
const elapsed = ((Date.now() - turnStart) / 1000).toFixed(1);
|
|
1327
1355
|
const tok = tokenStats.output > 0 ? ` · ${tokenStats.output - (_outputTokens || 0)} tok` : '';
|
package/lib/ui.js
CHANGED
|
@@ -281,6 +281,43 @@ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit
|
|
|
281
281
|
});
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
+
// ── selectFromList — arrow-key interactive picker ─────────────────────────
|
|
285
|
+
function selectFromList(items, initialIdx = 0) {
|
|
286
|
+
return new Promise((resolve) => {
|
|
287
|
+
let idx = initialIdx;
|
|
288
|
+
const n = items.length;
|
|
289
|
+
|
|
290
|
+
function render(isFirst) {
|
|
291
|
+
if (!isFirst) process.stdout.write(`\x1b[${n}A`); // move back up
|
|
292
|
+
for (let i = 0; i < n; i++) {
|
|
293
|
+
const active = i === idx;
|
|
294
|
+
const label = active ? ` ${cyan('❯')} ${items[i]}` : ` ${dim(items[i])}`;
|
|
295
|
+
process.stdout.write(`\x1b[2K${label}\n`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
render(true);
|
|
300
|
+
|
|
301
|
+
lineHandler = (key) => {
|
|
302
|
+
if (key === '\x1b[A') { // up
|
|
303
|
+
idx = (idx - 1 + n) % n;
|
|
304
|
+
render(false);
|
|
305
|
+
} else if (key === '\x1b[B') { // down
|
|
306
|
+
idx = (idx + 1) % n;
|
|
307
|
+
render(false);
|
|
308
|
+
} else if (key === '\r' || key === '\n') { // confirm
|
|
309
|
+
lineHandler = null;
|
|
310
|
+
process.stdout.write('\n');
|
|
311
|
+
resolve(items[idx]);
|
|
312
|
+
} else if (key === '\x1b' || key === '\x03') { // cancel
|
|
313
|
+
lineHandler = null;
|
|
314
|
+
process.stdout.write('\n');
|
|
315
|
+
resolve(null);
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
284
321
|
function readChar(promptText) {
|
|
285
322
|
return new Promise((resolve) => {
|
|
286
323
|
process.stdout.write(promptText);
|
|
@@ -374,7 +411,7 @@ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit
|
|
|
374
411
|
return {
|
|
375
412
|
start, pause, resume,
|
|
376
413
|
setPlanMode, setContinuation, setModelLabel, setModeLabel,
|
|
377
|
-
readLine, readChar,
|
|
414
|
+
readLine, readChar, selectFromList,
|
|
378
415
|
wasInterrupted, stop, exitUI,
|
|
379
416
|
};
|
|
380
417
|
}
|