tsunami-code 3.11.6 → 3.11.8

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 +48 -27
  2. package/lib/loop.js +1 -1
  3. 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.6';
29
+ const VERSION = '3.11.8';
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
 
@@ -546,7 +566,7 @@ async function run() {
546
566
  ui.setModelLabel(`Tsunami Code CLI v${VERSION}`);
547
567
 
548
568
  // ── Permission mode ───────────────────────────────────────────────────────
549
- const PERM_MODES = ['auto', 'accept-edits', 'confirm-writes', 'confirm-all', 'readonly', 'bypass'];
569
+ const PERM_MODES = ['auto', 'accept-edits', 'confirm-writes', 'confirm-all', 'readonly', 'bypass-permissions'];
550
570
  let permMode = 'auto';
551
571
 
552
572
  // ── Mode label helper ─────────────────────────────────────────────────────
@@ -561,8 +581,8 @@ async function run() {
561
581
  'accept-edits': dim('[accept-edits]'),
562
582
  'confirm-writes': cyan('[confirm-writes]'),
563
583
  'confirm-all': yellow('[confirm-all]'),
564
- 'readonly': chalk.bgBlue.white(' readonly '),
565
- 'bypass': chalk.bgRed.white.bold(' bypass '),
584
+ 'readonly': chalk.blue.bold('[readonly]'),
585
+ 'bypass-permissions': chalk.red.bold('[bypass-permissions]'),
566
586
  }[permMode];
567
587
  if (permBadge) badges.push(permBadge);
568
588
 
@@ -1290,16 +1310,13 @@ Output ONLY the TSUNAMI.md content, starting with "# Project: <name>"`;
1290
1310
  let toolTimers = {}; // track per-tool duration
1291
1311
 
1292
1312
  spinner.start();
1293
- // Spinner stops the moment first text actually reaches the screen,
1294
- // not on first raw token (highlighter buffers until word boundary).
1295
- const highlight = createHighlighter((s) => {
1296
- if (firstToken) {
1297
- spinner.stop();
1298
- process.stdout.write(' ');
1299
- firstToken = false;
1300
- }
1301
- process.stdout.write(s);
1313
+ const tw = createTypewriter((ch) => process.stdout.write(ch), 12);
1314
+ tw.setOnFirst(() => {
1315
+ spinner.stop();
1316
+ process.stdout.write(' ');
1317
+ firstToken = false;
1302
1318
  });
1319
+ const highlight = createHighlighter((s) => tw.push(s));
1303
1320
 
1304
1321
  try {
1305
1322
  await agentLoop(
@@ -1310,18 +1327,22 @@ Output ONLY the TSUNAMI.md content, starting with "# Project: <name>"`;
1310
1327
  },
1311
1328
  (toolName, toolArgs) => {
1312
1329
  flushHighlighter(highlight);
1330
+ tw.drainNow();
1331
+ tw.reset();
1313
1332
  spinner.stop();
1314
1333
  const elapsed = toolTimers[toolName] != null ? Date.now() - toolTimers[toolName] : null;
1315
1334
  toolTimers[toolName] = Date.now();
1316
1335
  printToolCall(toolName, toolArgs, elapsed);
1317
1336
  spinner.start();
1318
1337
  firstToken = true;
1338
+ tw.setOnFirst(() => { spinner.stop(); process.stdout.write(' '); firstToken = false; });
1319
1339
  },
1320
1340
  { sessionDir, cwd, planMode, permMode },
1321
1341
  makeConfirmCallback(ui)
1322
1342
  );
1323
1343
  spinner.stop();
1324
1344
  flushHighlighter(highlight);
1345
+ tw.drainNow();
1325
1346
 
1326
1347
  const elapsed = ((Date.now() - turnStart) / 1000).toFixed(1);
1327
1348
  const tok = tokenStats.output > 0 ? ` · ${tokenStats.output - (_outputTokens || 0)} tok` : '';
package/lib/loop.js CHANGED
@@ -460,7 +460,7 @@ export async function agentLoop(serverUrl, messages, onToken, onToolCall, sessio
460
460
  }
461
461
 
462
462
  // ── Permission gate (skip entirely in bypass mode) ───────────────
463
- if (permMode !== 'bypass' && confirmCallback) {
463
+ if (permMode !== 'bypass-permissions' && confirmCallback) {
464
464
  const needsConfirm =
465
465
  (permMode === 'confirm-all' && ['Write', 'Edit', 'Bash'].includes(tc.name)) ||
466
466
  (permMode === 'confirm-writes' && ['Write', 'Edit'].includes(tc.name)) ||
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tsunami-code",
3
- "version": "3.11.6",
3
+ "version": "3.11.8",
4
4
  "description": "Tsunami Code CLI — AI coding agent by Keystone World Management Navy Seal Unit XI3",
5
5
  "type": "module",
6
6
  "bin": {