aemeathcli 1.0.12 → 1.1.0

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.
@@ -11,39 +11,18 @@ import { DEFAULT_CONFIG, PACKAGE_VERSION } from './chunk-2Y7TR6BS.js';
11
11
  import './chunk-IR5HLBMH.js';
12
12
  import './chunk-D275MCIH.js';
13
13
  import React11, { useRef, useState, useCallback, useEffect, useMemo, startTransition } from 'react';
14
- import { Box, Text, render, useInput } from 'ink';
14
+ import { Box, Text, render, useInput, useStdout } from 'ink';
15
15
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
16
16
  import { randomUUID } from 'crypto';
17
17
 
18
18
  // src/ui/theme.ts
19
19
  var BRAND_COLOR = "#F0C5DA";
20
20
  var colors = {
21
- text: {
22
- primary: "#F9F5F5",
23
- secondary: "#d3acb3",
24
- muted: "#9e8085",
25
- accent: "#F0C5DA",
26
- response: "#F9F5F5"
27
- },
28
- border: {
29
- dim: "#6b5459",
30
- active: "#d3acb3"},
31
- syntax: {
32
- keyword: "#F0C5DA",
33
- string: "#EDD6DC"},
34
- status: {
35
- success: "#F0C5DA",
36
- error: "#f87171",
37
- warning: "#EDD6DC",
38
- info: "#F0C5DA",
39
- active: "#F0C5DA"
40
- },
41
- role: {
42
- user: "#F0C5DA",
43
- assistant: "#F9F5F5",
44
- system: "#EDD6DC",
45
- tool: "#d3acb3"
46
- }
21
+ text: { primary: "#F9F5F5", secondary: "#d3acb3", muted: "#9e8085", accent: "#F0C5DA", response: "#F9F5F5" },
22
+ border: { dim: "#6b5459", active: "#d3acb3"},
23
+ syntax: { keyword: "#F0C5DA", string: "#EDD6DC"},
24
+ status: { success: "#F0C5DA", error: "#f87171", warning: "#EDD6DC", info: "#F0C5DA", active: "#F0C5DA" },
25
+ role: { user: "#F0C5DA", assistant: "#F9F5F5", system: "#EDD6DC", tool: "#d3acb3" }
47
26
  };
48
27
  function parseBlocks(raw) {
49
28
  const blocks = [];
@@ -180,12 +159,24 @@ function InlineMarkdown({
180
159
  }
181
160
  const linkMatch = remaining.match(/^\[([^\]]+)\]\(([^)]+)\)/);
182
161
  if (linkMatch) {
162
+ const linkLabel = linkMatch[1] ?? "";
163
+ const linkUrl = linkMatch[2] ?? "";
164
+ const isLocalPath = linkUrl.startsWith("/") || linkUrl.startsWith("./") || linkUrl.startsWith("../");
165
+ const displayText = isLocalPath ? linkUrl : linkLabel;
183
166
  segments.push(
184
- /* @__PURE__ */ jsx(Text, { color: colors.status.info, underline: true, children: linkMatch[1] }, key++)
167
+ /* @__PURE__ */ jsx(Text, { color: colors.status.info, underline: true, children: displayText }, key++)
185
168
  );
186
169
  remaining = remaining.slice(linkMatch[0].length);
187
170
  continue;
188
171
  }
172
+ const bareUrlMatch = remaining.match(/^(https?:\/\/[^\s)>\]]+)/);
173
+ if (bareUrlMatch) {
174
+ segments.push(
175
+ /* @__PURE__ */ jsx(Text, { color: colors.status.info, underline: true, children: bareUrlMatch[1] }, key++)
176
+ );
177
+ remaining = remaining.slice(bareUrlMatch[0].length);
178
+ continue;
179
+ }
189
180
  const nextSpecial = remaining.search(/[[*`~]/);
190
181
  if (nextSpecial === -1) {
191
182
  segments.push(/* @__PURE__ */ jsx(Text, { children: remaining }, key++));
@@ -367,6 +358,96 @@ function useAnimationTick(intervalMs, enabled = true) {
367
358
  }, [enabled, intervalMs]);
368
359
  return tick;
369
360
  }
361
+
362
+ // src/ui/shimmer.ts
363
+ var DEFAULT_PERIOD_MS = 2e3;
364
+ var BAND_WIDTH = 10;
365
+ var MAX_INTENSITY = 0.9;
366
+ var trueColorCached;
367
+ function supportsTrueColor() {
368
+ if (trueColorCached === void 0) {
369
+ const ct = process.env["COLORTERM"] ?? "";
370
+ trueColorCached = ct === "truecolor" || ct === "24bit";
371
+ }
372
+ return trueColorCached;
373
+ }
374
+ function parseHex(hex) {
375
+ let raw = hex.startsWith("#") ? hex.slice(1) : hex;
376
+ if (raw.length === 3) {
377
+ const c0 = raw[0] ?? "f";
378
+ const c1 = raw[1] ?? "f";
379
+ const c2 = raw[2] ?? "f";
380
+ raw = c0 + c0 + c1 + c1 + c2 + c2;
381
+ }
382
+ if (raw.length !== 6) return [255, 255, 255];
383
+ const n = parseInt(raw, 16);
384
+ if (Number.isNaN(n)) return [255, 255, 255];
385
+ return [n >> 16 & 255, n >> 8 & 255, n & 255];
386
+ }
387
+ function toHex(r, g, b) {
388
+ const c = (v) => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, "0");
389
+ return "#" + c(r) + c(g) + c(b);
390
+ }
391
+ function blendChannel(base, intensity) {
392
+ return base + (255 - base) * intensity;
393
+ }
394
+ function shimmerIntensity(charIndex, totalLength, periodMs = DEFAULT_PERIOD_MS) {
395
+ const totalWidth = totalLength + BAND_WIDTH;
396
+ const progress = Date.now() % periodMs / periodMs;
397
+ const bandCenter = progress * totalWidth;
398
+ const distance = Math.abs(charIndex - bandCenter) / (totalWidth / 2);
399
+ return Math.max(0, Math.cos(distance * (Math.PI / 2))) * MAX_INTENSITY;
400
+ }
401
+ function shimmerText(text, baseColor) {
402
+ const len = text.length;
403
+ const trueColor = supportsTrueColor();
404
+ const [br, bg, bb] = parseHex(baseColor);
405
+ const result = [];
406
+ for (let i = 0; i < len; i++) {
407
+ const intensity = shimmerIntensity(i, len);
408
+ if (trueColor) {
409
+ const color = toHex(
410
+ blendChannel(br, intensity),
411
+ blendChannel(bg, intensity),
412
+ blendChannel(bb, intensity)
413
+ );
414
+ const ch = text[i] ?? " ";
415
+ result.push({ char: ch, intensity, color, bold: false, dim: false });
416
+ } else {
417
+ const bold = intensity > 0.6;
418
+ const dim = intensity <= 0.1;
419
+ const ch = text[i] ?? " ";
420
+ result.push({ char: ch, intensity, color: baseColor, bold, dim });
421
+ }
422
+ }
423
+ return result;
424
+ }
425
+ function shimmerToInkSpans(text, baseColor) {
426
+ const chars = shimmerText(text, baseColor);
427
+ if (chars.length === 0) return [];
428
+ const spans = [];
429
+ const first = chars[0];
430
+ if (first === void 0) return [];
431
+ let curText = first.char;
432
+ let curColor = first.color;
433
+ let curBold = first.bold;
434
+ let curDim = first.dim;
435
+ for (let i = 1; i < chars.length; i++) {
436
+ const ch = chars[i];
437
+ if (ch === void 0) continue;
438
+ if (ch.color === curColor && ch.bold === curBold && ch.dim === curDim) {
439
+ curText += ch.char;
440
+ } else {
441
+ spans.push({ text: curText, color: curColor, bold: curBold, dim: curDim });
442
+ curText = ch.char;
443
+ curColor = ch.color;
444
+ curBold = ch.bold;
445
+ curDim = ch.dim;
446
+ }
447
+ }
448
+ spans.push({ text: curText, color: curColor, bold: curBold, dim: curDim });
449
+ return spans;
450
+ }
370
451
  var SPINNER_VARIANTS = {
371
452
  dots: {
372
453
  frames: ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"],
@@ -389,11 +470,13 @@ var SPINNER_VARIANTS = {
389
470
  interval: 120
390
471
  }
391
472
  };
473
+ var SHIMMER_INTERVAL_MS = 32;
392
474
  function GradientSpinner({
393
475
  label,
394
476
  labelColor = "#888888",
395
477
  variant = "dots",
396
- speed
478
+ speed,
479
+ shimmer = false
397
480
  }) {
398
481
  const spinnerDef = SPINNER_VARIANTS[variant] ?? SPINNER_VARIANTS["dots"];
399
482
  if (!spinnerDef) {
@@ -401,10 +484,27 @@ function GradientSpinner({
401
484
  }
402
485
  const interval = speed ?? spinnerDef.interval;
403
486
  const tick = useAnimationTick(interval);
487
+ useAnimationTick(SHIMMER_INTERVAL_MS, shimmer && label !== void 0 && label.length > 0);
404
488
  const frame = tick % spinnerDef.frames.length;
489
+ const shimmerSpans = useMemo(() => {
490
+ if (!shimmer || !label) return null;
491
+ return shimmerToInkSpans(label, labelColor);
492
+ }, [shimmer, label, labelColor, tick]);
405
493
  return /* @__PURE__ */ jsxs(Text, { children: [
406
494
  /* @__PURE__ */ jsx(Text, { color: BRAND_COLOR, children: spinnerDef.frames[frame] }),
407
- label ? /* @__PURE__ */ jsxs(Text, { color: labelColor, children: [
495
+ label ? shimmerSpans ? /* @__PURE__ */ jsxs(Text, { children: [
496
+ " ",
497
+ shimmerSpans.map((span, i) => /* @__PURE__ */ jsx(
498
+ Text,
499
+ {
500
+ color: span.color,
501
+ bold: span.bold,
502
+ dimColor: span.dim,
503
+ children: span.text
504
+ },
505
+ i
506
+ ))
507
+ ] }) : /* @__PURE__ */ jsxs(Text, { color: labelColor, children: [
408
508
  " ",
409
509
  label
410
510
  ] }) : null
@@ -428,31 +528,62 @@ function StatusIcon({
428
528
  }
429
529
  function formatDuration(ms) {
430
530
  if (ms < 1e3) return `${ms}ms`;
431
- return `${(ms / 1e3).toFixed(1)}s`;
531
+ const totalSec = Math.floor(ms / 1e3);
532
+ if (totalSec < 60) {
533
+ const frac = ms % 1e3;
534
+ return frac >= 100 ? `${(ms / 1e3).toFixed(1)}s` : `${totalSec}s`;
535
+ }
536
+ const minutes = Math.floor(totalSec / 60);
537
+ const seconds = totalSec % 60;
538
+ if (minutes < 60) {
539
+ return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
540
+ }
541
+ const hours = Math.floor(minutes / 60);
542
+ const remainMin = minutes % 60;
543
+ return seconds > 0 ? `${hours}h ${remainMin}m ${seconds}s` : `${hours}h ${remainMin}m`;
432
544
  }
545
+ var TOOL_ICONS = {
546
+ read: "\u{1F4C4}",
547
+ write: "\u270F\uFE0F",
548
+ edit: "\u{1F4DD}",
549
+ glob: "\u{1F50D}",
550
+ grep: "\u{1F50E}",
551
+ bash: "\u26A1",
552
+ web_search: "\u{1F310}",
553
+ webSearch: "\u{1F310}",
554
+ web_fetch: "\u{1F4E1}",
555
+ webFetch: "\u{1F4E1}"
556
+ };
433
557
  function getToolIcon(name) {
434
- switch (name) {
435
- case "read":
436
- return "\u{1F4C4}";
437
- case "write":
438
- return "\u270F\uFE0F";
439
- case "edit":
440
- return "\u{1F4DD}";
441
- case "glob":
442
- return "\u{1F50D}";
443
- case "grep":
444
- return "\u{1F50E}";
445
- case "bash":
446
- return "\u26A1";
447
- case "web_search":
448
- case "webSearch":
449
- return "\u{1F310}";
450
- case "web_fetch":
451
- case "webFetch":
452
- return "\u{1F4E1}";
453
- default:
454
- return "\u2699";
558
+ return TOOL_ICONS[name] ?? "\u2699";
559
+ }
560
+ var HEAD_LINES = 5;
561
+ var TAIL_LINES = 2;
562
+ function formatOutputLines(raw) {
563
+ const allLines = raw.split("\n");
564
+ if (allLines.length > 0 && allLines[allLines.length - 1] === "") {
565
+ allLines.pop();
566
+ }
567
+ const total = allLines.length;
568
+ if (total <= HEAD_LINES + TAIL_LINES) {
569
+ return allLines.map((line, i) => {
570
+ const prefix = i === 0 ? " \u2514 " : " ";
571
+ return `${prefix}${line}`;
572
+ });
455
573
  }
574
+ const head = allLines.slice(0, HEAD_LINES);
575
+ const tail = allLines.slice(total - TAIL_LINES);
576
+ const omitted = total - HEAD_LINES - TAIL_LINES;
577
+ const result = [];
578
+ head.forEach((line, i) => {
579
+ const prefix = i === 0 ? " \u2514 " : " ";
580
+ result.push(`${prefix}${line}`);
581
+ });
582
+ result.push(` \u2026 +${omitted} lines`);
583
+ tail.forEach((line) => {
584
+ result.push(` ${line}`);
585
+ });
586
+ return result;
456
587
  }
457
588
  function ToolCallDisplay({
458
589
  toolName,
@@ -463,7 +594,6 @@ function ToolCallDisplay({
463
594
  duration,
464
595
  isCollapsed = true
465
596
  }) {
466
- const borderColor = status === "error" ? colors.status.error : status === "executing" ? colors.status.active : colors.border.dim;
467
597
  const icon = getToolIcon(toolName);
468
598
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginY: 0, children: [
469
599
  /* @__PURE__ */ jsxs(Box, { children: [
@@ -485,28 +615,16 @@ function ToolCallDisplay({
485
615
  ")"
486
616
  ] }) : null
487
617
  ] }),
488
- !isCollapsed && output ? /* @__PURE__ */ jsx(
489
- Box,
618
+ !isCollapsed && output ? /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: formatOutputLines(output).map((line, i) => /* @__PURE__ */ jsx(
619
+ Text,
490
620
  {
491
- flexDirection: "column",
492
- marginLeft: 2,
493
- borderStyle: "single",
494
- borderLeft: true,
495
- borderRight: false,
496
- borderTop: false,
497
- borderBottom: false,
498
- borderColor,
499
- paddingLeft: 1,
500
- children: /* @__PURE__ */ jsx(
501
- Text,
502
- {
503
- wrap: "wrap",
504
- color: isError ? colors.status.error : colors.text.secondary,
505
- children: output.length > 2e3 ? output.slice(0, 2e3) + "\n\u2026 (truncated)" : output
506
- }
507
- )
508
- }
509
- ) : null
621
+ wrap: "truncate",
622
+ dimColor: true,
623
+ color: isError ? colors.status.error : colors.text.secondary,
624
+ children: line
625
+ },
626
+ i
627
+ )) }) : null
510
628
  ] });
511
629
  }
512
630
  function getRoleColor(role) {
@@ -966,6 +1084,10 @@ function InputBar({
966
1084
  const isAutocompleteActiveRef = useRef(false);
967
1085
  const previousHistoryIndexRef = useRef(void 0);
968
1086
  const historyCacheRef = useRef({});
1087
+ const lastInputTimeRef = useRef(0);
1088
+ const pasteBufferRef = useRef("");
1089
+ const pasteTimeoutRef = useRef(null);
1090
+ const isPastingRef = useRef(false);
969
1091
  const setInputWithCursor = useCallback(
970
1092
  (nextInput, cursorPosition = "end") => {
971
1093
  const nextCursor = cursorPosition === "start" ? 0 : cursorPosition === "end" ? codePointLength(nextInput) : clampCursorOffset(nextInput, cursorPosition);
@@ -1032,6 +1154,17 @@ function InputBar({
1032
1154
  });
1033
1155
  }
1034
1156
  }, [initialHistory]);
1157
+ useEffect(() => {
1158
+ process.stdout.write("\x1B[?2004h");
1159
+ return () => {
1160
+ process.stdout.write("\x1B[?2004l");
1161
+ };
1162
+ }, []);
1163
+ useEffect(() => {
1164
+ return () => {
1165
+ if (pasteTimeoutRef.current) clearTimeout(pasteTimeoutRef.current);
1166
+ };
1167
+ }, []);
1035
1168
  useEffect(() => {
1036
1169
  inputRef.current = input;
1037
1170
  cursorOffsetRef.current = cursorOffset;
@@ -1178,6 +1311,24 @@ function InputBar({
1178
1311
  }
1179
1312
  if (key.ctrl && inputChar === "l") return;
1180
1313
  if (!key.ctrl && !key.meta && inputChar) {
1314
+ const now = Date.now();
1315
+ const timeSinceLastInput = now - lastInputTimeRef.current;
1316
+ lastInputTimeRef.current = now;
1317
+ if (timeSinceLastInput < 5) {
1318
+ isPastingRef.current = true;
1319
+ pasteBufferRef.current += inputChar;
1320
+ if (pasteTimeoutRef.current) clearTimeout(pasteTimeoutRef.current);
1321
+ pasteTimeoutRef.current = setTimeout(() => {
1322
+ const pasteText = pasteBufferRef.current;
1323
+ pasteBufferRef.current = "";
1324
+ isPastingRef.current = false;
1325
+ if (pasteText) {
1326
+ const result2 = insertTextAtCursor(inputRef.current, cursorOffsetRef.current, pasteText);
1327
+ setInputWithCursor(result2.text, result2.cursorOffset);
1328
+ }
1329
+ }, 10);
1330
+ return;
1331
+ }
1181
1332
  const result = insertTextAtCursor(currentInput, currentCursorOffset, inputChar);
1182
1333
  setInputWithCursor(result.text, result.cursorOffset);
1183
1334
  }
@@ -1238,6 +1389,12 @@ function StatusBar({
1238
1389
  gitBranch,
1239
1390
  gitChanges
1240
1391
  }) {
1392
+ const { stdout } = useStdout();
1393
+ const width = stdout.columns ?? 120;
1394
+ const showRole = width > 100 && !!role;
1395
+ const showGit = width > 60 && !!gitBranch;
1396
+ const showTokens = width > 45;
1397
+ const showCost = width > 45;
1241
1398
  return /* @__PURE__ */ jsxs(Box, { borderStyle: "round", borderColor: colors.border.dim, paddingX: 1, children: [
1242
1399
  /* @__PURE__ */ jsxs(Text, { color: BRAND_COLOR, bold: true, children: [
1243
1400
  "\u25C6",
@@ -1246,18 +1403,22 @@ function StatusBar({
1246
1403
  /* @__PURE__ */ jsx(Text, { color: colors.status.active, bold: true, children: "Aemeath Agent Swarm" }),
1247
1404
  /* @__PURE__ */ jsx(Text, { color: colors.text.muted, children: SEP }),
1248
1405
  /* @__PURE__ */ jsx(Text, { color: colors.status.warning, bold: true, children: shortModelLabel(model) }),
1249
- role ? /* @__PURE__ */ jsxs(Fragment, { children: [
1406
+ showRole ? /* @__PURE__ */ jsxs(Fragment, { children: [
1250
1407
  /* @__PURE__ */ jsx(Text, { color: colors.text.muted, children: SEP }),
1251
1408
  /* @__PURE__ */ jsx(Text, { color: colors.role.tool, children: role })
1252
1409
  ] }) : null,
1253
- /* @__PURE__ */ jsx(Text, { color: colors.text.muted, children: SEP }),
1254
- /* @__PURE__ */ jsxs(Text, { color: colors.text.secondary, children: [
1255
- tokenCount,
1256
- " tok"
1257
- ] }),
1258
- /* @__PURE__ */ jsx(Text, { color: colors.text.muted, children: SEP }),
1259
- /* @__PURE__ */ jsx(Text, { color: colors.status.success, children: cost }),
1260
- gitBranch ? /* @__PURE__ */ jsxs(Fragment, { children: [
1410
+ showTokens ? /* @__PURE__ */ jsxs(Fragment, { children: [
1411
+ /* @__PURE__ */ jsx(Text, { color: colors.text.muted, children: SEP }),
1412
+ /* @__PURE__ */ jsxs(Text, { color: colors.text.secondary, children: [
1413
+ tokenCount,
1414
+ " tok"
1415
+ ] })
1416
+ ] }) : null,
1417
+ showCost ? /* @__PURE__ */ jsxs(Fragment, { children: [
1418
+ /* @__PURE__ */ jsx(Text, { color: colors.text.muted, children: SEP }),
1419
+ /* @__PURE__ */ jsx(Text, { color: colors.status.success, children: cost })
1420
+ ] }) : null,
1421
+ showGit ? /* @__PURE__ */ jsxs(Fragment, { children: [
1261
1422
  /* @__PURE__ */ jsx(Text, { color: colors.text.muted, children: SEP }),
1262
1423
  /* @__PURE__ */ jsxs(Text, { color: colors.status.info, children: [
1263
1424
  "\u2387",
@@ -1278,26 +1439,49 @@ var THINKING_PHRASES = [
1278
1439
  "Evaluating"
1279
1440
  ];
1280
1441
  var PHRASE_CYCLE_MS = 2500;
1442
+ function formatElapsed(ms) {
1443
+ const totalSecs = Math.floor(ms / 1e3);
1444
+ if (totalSecs < 1) return "0s";
1445
+ if (totalSecs < 60) return `${totalSecs}s`;
1446
+ const mins = Math.floor(totalSecs / 60);
1447
+ const secs = totalSecs % 60;
1448
+ if (mins < 60) return `${mins}m ${String(secs).padStart(2, "0")}s`;
1449
+ const hrs = Math.floor(mins / 60);
1450
+ const remMins = mins % 60;
1451
+ return `${hrs}h ${String(remMins).padStart(2, "0")}m ${String(secs).padStart(2, "0")}s`;
1452
+ }
1281
1453
  function ThinkingIndicator({
1282
1454
  activity,
1283
1455
  isStreaming,
1284
1456
  modelName,
1285
- startTime
1457
+ startTime,
1458
+ isPaused = false,
1459
+ details,
1460
+ maxDetailLines = 3
1286
1461
  }) {
1287
1462
  const tick = useAnimationTick(1e3);
1288
- const elapsed = startTime === void 0 ? 0 : Math.max(0, Date.now() - startTime);
1463
+ const pausedAccumRef = useRef(0);
1464
+ const pauseStartRef = useRef(void 0);
1465
+ if (isPaused && pauseStartRef.current === void 0) {
1466
+ pauseStartRef.current = Date.now();
1467
+ } else if (!isPaused && pauseStartRef.current !== void 0) {
1468
+ pausedAccumRef.current += Date.now() - pauseStartRef.current;
1469
+ pauseStartRef.current = void 0;
1470
+ }
1471
+ const now = isPaused ? pauseStartRef.current ?? Date.now() : Date.now();
1472
+ const rawElapsed = startTime === void 0 ? 0 : Math.max(0, now - startTime);
1473
+ const elapsed = Math.max(0, rawElapsed - pausedAccumRef.current);
1289
1474
  const phraseIndex = Math.floor(elapsed / PHRASE_CYCLE_MS) % THINKING_PHRASES.length;
1290
1475
  const elapsedStr = useMemo(() => {
1291
- const secs = Math.floor(elapsed / 1e3);
1292
- if (secs < 1) return "";
1293
- if (secs < 60) return `${secs}s`;
1294
- const mins = Math.floor(secs / 60);
1295
- return `${mins}m${secs % 60}s`;
1476
+ if (elapsed < 1e3) return "";
1477
+ return formatElapsed(elapsed);
1296
1478
  }, [elapsed]);
1297
1479
  const dotCount = tick % 4;
1298
1480
  const dots = ".".repeat(dotCount);
1299
1481
  const phrase = THINKING_PHRASES[phraseIndex] ?? "Thinking";
1300
- const displayText = activity ? activity : isStreaming ? "Streaming response" : `${phrase}${dots}`;
1482
+ const displayText = isPaused ? "Rate limited \u2014 waiting" : activity ? activity : isStreaming ? "Streaming response" : `${phrase}${dots}`;
1483
+ const visibleDetails = details ? details.slice(0, maxDetailLines) : void 0;
1484
+ const hiddenDetailCount = details ? Math.max(0, details.length - maxDetailLines) : 0;
1301
1485
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
1302
1486
  modelName ? /* @__PURE__ */ jsxs(Box, { children: [
1303
1487
  /* @__PURE__ */ jsxs(Text, { color: colors.role.assistant, bold: true, children: [
@@ -1310,21 +1494,35 @@ function ThinkingIndicator({
1310
1494
  /* @__PURE__ */ jsx(
1311
1495
  GradientSpinner,
1312
1496
  {
1313
- variant: activity ? "braille" : "dots",
1497
+ variant: isPaused ? "pulse" : activity ? "braille" : "dots",
1314
1498
  label: displayText,
1315
- labelColor: activity ? colors.text.secondary : colors.text.muted
1499
+ labelColor: isPaused ? colors.status.warning : activity ? colors.text.secondary : colors.text.muted
1316
1500
  }
1317
1501
  ),
1318
1502
  elapsedStr ? /* @__PURE__ */ jsxs(Text, { color: colors.text.muted, children: [
1319
- " (",
1503
+ " ",
1504
+ "(",
1320
1505
  elapsedStr,
1321
- ")"
1322
- ] }) : null
1506
+ " ",
1507
+ "\u2022",
1508
+ " esc to interrupt)"
1509
+ ] }) : /* @__PURE__ */ jsxs(Text, { color: colors.text.muted, children: [
1510
+ " ",
1511
+ "(esc to interrupt)"
1512
+ ] })
1323
1513
  ] }),
1324
- /* @__PURE__ */ jsx(Box, { marginLeft: 2, children: /* @__PURE__ */ jsxs(Text, { color: colors.text.muted, dimColor: true, children: [
1325
- " ",
1326
- "esc to cancel"
1327
- ] }) })
1514
+ visibleDetails && visibleDetails.length > 0 ? /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [
1515
+ visibleDetails.map((detail, i) => /* @__PURE__ */ jsxs(Text, { color: colors.text.muted, dimColor: true, wrap: "truncate", children: [
1516
+ " \u2514 ",
1517
+ detail
1518
+ ] }, i)),
1519
+ hiddenDetailCount > 0 ? /* @__PURE__ */ jsxs(Text, { color: colors.text.muted, dimColor: true, children: [
1520
+ " ",
1521
+ "(+",
1522
+ hiddenDetailCount,
1523
+ " more)"
1524
+ ] }) : null
1525
+ ] }) : null
1328
1526
  ] });
1329
1527
  }
1330
1528
  var MASCOT_LINES = [
@@ -1764,6 +1962,111 @@ function useModel(config, initialModel, initialRole) {
1764
1962
  router
1765
1963
  };
1766
1964
  }
1965
+
1966
+ // src/ui/streaming-controller.ts
1967
+ var StreamingController = class {
1968
+ buffer;
1969
+ committed;
1970
+ lineCounter;
1971
+ headerEmitted;
1972
+ /** Index into `committed` marking where the last drain ended. */
1973
+ drainCursor;
1974
+ constructor() {
1975
+ this.buffer = "";
1976
+ this.committed = [];
1977
+ this.lineCounter = 0;
1978
+ this.headerEmitted = false;
1979
+ this.drainCursor = 0;
1980
+ }
1981
+ /**
1982
+ * Feed a text chunk into the controller.
1983
+ *
1984
+ * Lines terminated by `\n` are committed immediately.
1985
+ * Any trailing text without a newline stays in the buffer.
1986
+ *
1987
+ * @returns Newly committed lines from this chunk.
1988
+ */
1989
+ push(chunk) {
1990
+ this.buffer += chunk;
1991
+ const newLines = [];
1992
+ let newlineIdx = this.buffer.indexOf("\n");
1993
+ while (newlineIdx !== -1) {
1994
+ const lineText = this.buffer.slice(0, newlineIdx);
1995
+ this.buffer = this.buffer.slice(newlineIdx + 1);
1996
+ this.lineCounter += 1;
1997
+ const committed = {
1998
+ text: lineText,
1999
+ lineNumber: this.lineCounter
2000
+ };
2001
+ this.committed.push(committed);
2002
+ newLines.push(committed);
2003
+ newlineIdx = this.buffer.indexOf("\n");
2004
+ }
2005
+ if (newLines.length > 0) {
2006
+ this.headerEmitted = true;
2007
+ }
2008
+ return newLines;
2009
+ }
2010
+ /**
2011
+ * Flush the remaining buffer as a final committed line.
2012
+ *
2013
+ * Call this when the stream ends to commit any trailing
2014
+ * partial line that never received a terminating newline.
2015
+ *
2016
+ * @returns The flushed line, or `null` if the buffer was empty.
2017
+ */
2018
+ flush() {
2019
+ if (this.buffer.length === 0) {
2020
+ return null;
2021
+ }
2022
+ this.lineCounter += 1;
2023
+ const committed = {
2024
+ text: this.buffer,
2025
+ lineNumber: this.lineCounter
2026
+ };
2027
+ this.committed.push(committed);
2028
+ this.buffer = "";
2029
+ this.headerEmitted = true;
2030
+ return committed;
2031
+ }
2032
+ /** Get an immutable snapshot of the current controller state. */
2033
+ getState() {
2034
+ return {
2035
+ committedLines: [...this.committed],
2036
+ pendingLine: this.buffer,
2037
+ hasEmittedHeader: this.headerEmitted,
2038
+ totalLines: this.lineCounter
2039
+ };
2040
+ }
2041
+ /** Get only the pending (incomplete) line text. */
2042
+ getPendingLine() {
2043
+ return this.buffer;
2044
+ }
2045
+ /** Reset the controller to its initial state. */
2046
+ reset() {
2047
+ this.buffer = "";
2048
+ this.committed = [];
2049
+ this.lineCounter = 0;
2050
+ this.headerEmitted = false;
2051
+ this.drainCursor = 0;
2052
+ }
2053
+ /**
2054
+ * Get committed lines since the last call to this method.
2055
+ *
2056
+ * Useful for incremental rendering — each call returns only
2057
+ * the lines that were committed after the previous drain.
2058
+ */
2059
+ drainNewLines() {
2060
+ const newLines = this.committed.slice(this.drainCursor);
2061
+ this.drainCursor = this.committed.length;
2062
+ return newLines;
2063
+ }
2064
+ };
2065
+ function createStreamingController() {
2066
+ return new StreamingController();
2067
+ }
2068
+
2069
+ // src/ui/hooks/useStream.ts
1767
2070
  function formatToolActivity(toolCall) {
1768
2071
  const args = toolCall.arguments;
1769
2072
  switch (toolCall.name) {
@@ -1809,25 +2112,32 @@ function useStream() {
1809
2112
  const [state, setState] = useState({
1810
2113
  isStreaming: false,
1811
2114
  content: "",
2115
+ pendingLine: "",
1812
2116
  usage: void 0,
1813
2117
  error: void 0,
1814
2118
  activity: void 0,
1815
2119
  toolCalls: [],
1816
- startTime: void 0
2120
+ startTime: void 0,
2121
+ isPaused: false
1817
2122
  });
1818
2123
  const cancelRef = useRef(false);
2124
+ const streamingCtrlRef = useRef(null);
1819
2125
  const isCancelled = () => cancelRef.current;
1820
2126
  const startStream = useCallback(
1821
2127
  async (stream) => {
1822
2128
  cancelRef.current = false;
2129
+ const ctrl = createStreamingController();
2130
+ streamingCtrlRef.current = ctrl;
1823
2131
  setState({
1824
2132
  isStreaming: true,
1825
2133
  content: "",
2134
+ pendingLine: "",
1826
2135
  usage: void 0,
1827
2136
  error: void 0,
1828
2137
  activity: void 0,
1829
2138
  toolCalls: [],
1830
- startTime: Date.now()
2139
+ startTime: Date.now(),
2140
+ isPaused: false
1831
2141
  });
1832
2142
  try {
1833
2143
  for await (const chunk of stream) {
@@ -1836,9 +2146,12 @@ function useStream() {
1836
2146
  case "text":
1837
2147
  if (chunk.content !== void 0) {
1838
2148
  const text = chunk.content;
2149
+ ctrl.push(text);
2150
+ const ctrlState = ctrl.getState();
1839
2151
  setState((prev) => ({
1840
2152
  ...prev,
1841
- content: prev.content + text,
2153
+ content: ctrlState.committedLines.map((l) => l.text).join("\n"),
2154
+ pendingLine: ctrlState.pendingLine,
1842
2155
  activity: void 0
1843
2156
  }));
1844
2157
  }
@@ -1894,12 +2207,17 @@ function useStream() {
1894
2207
  )
1895
2208
  }));
1896
2209
  return;
1897
- case "done":
2210
+ case "done": {
2211
+ ctrl.flush();
2212
+ const finalState = ctrl.getState();
1898
2213
  setState((prev) => ({
1899
2214
  ...prev,
1900
2215
  isStreaming: false,
2216
+ content: finalState.committedLines.map((l) => l.text).join("\n"),
2217
+ pendingLine: "",
1901
2218
  usage: chunk.usage ?? prev.usage,
1902
2219
  activity: void 0,
2220
+ isPaused: false,
1903
2221
  toolCalls: prev.toolCalls.map(
1904
2222
  (tc) => tc.status === "executing" ? {
1905
2223
  ...tc,
@@ -1909,12 +2227,18 @@ function useStream() {
1909
2227
  )
1910
2228
  }));
1911
2229
  return;
2230
+ }
1912
2231
  }
1913
2232
  }
2233
+ ctrl.flush();
2234
+ const endState = ctrl.getState();
1914
2235
  setState((prev) => ({
1915
2236
  ...prev,
1916
2237
  isStreaming: false,
2238
+ content: endState.committedLines.map((l) => l.text).join("\n"),
2239
+ pendingLine: "",
1917
2240
  activity: void 0,
2241
+ isPaused: false,
1918
2242
  toolCalls: prev.toolCalls.map(
1919
2243
  (tc) => tc.status === "executing" ? {
1920
2244
  ...tc,
@@ -1928,7 +2252,8 @@ function useStream() {
1928
2252
  ...prev,
1929
2253
  isStreaming: false,
1930
2254
  error: error instanceof Error ? error.message : String(error),
1931
- activity: void 0
2255
+ activity: void 0,
2256
+ isPaused: false
1932
2257
  }));
1933
2258
  }
1934
2259
  },
@@ -1936,28 +2261,52 @@ function useStream() {
1936
2261
  );
1937
2262
  const cancelStream = useCallback(() => {
1938
2263
  cancelRef.current = true;
1939
- setState((prev) => ({
1940
- ...prev,
1941
- isStreaming: false,
1942
- toolCalls: prev.toolCalls.map(
1943
- (tc) => tc.status === "executing" ? {
1944
- ...tc,
1945
- status: "cancelled",
1946
- endTime: Date.now()
1947
- } : tc
1948
- )
1949
- }));
2264
+ const ctrl = streamingCtrlRef.current;
2265
+ if (ctrl) {
2266
+ ctrl.flush();
2267
+ const finalState = ctrl.getState();
2268
+ setState((prev) => ({
2269
+ ...prev,
2270
+ isStreaming: false,
2271
+ content: finalState.committedLines.map((l) => l.text).join("\n"),
2272
+ pendingLine: "",
2273
+ isPaused: false,
2274
+ toolCalls: prev.toolCalls.map(
2275
+ (tc) => tc.status === "executing" ? {
2276
+ ...tc,
2277
+ status: "cancelled",
2278
+ endTime: Date.now()
2279
+ } : tc
2280
+ )
2281
+ }));
2282
+ } else {
2283
+ setState((prev) => ({
2284
+ ...prev,
2285
+ isStreaming: false,
2286
+ isPaused: false,
2287
+ toolCalls: prev.toolCalls.map(
2288
+ (tc) => tc.status === "executing" ? {
2289
+ ...tc,
2290
+ status: "cancelled",
2291
+ endTime: Date.now()
2292
+ } : tc
2293
+ )
2294
+ }));
2295
+ }
1950
2296
  }, []);
1951
2297
  const reset = useCallback(() => {
1952
2298
  cancelRef.current = true;
2299
+ streamingCtrlRef.current = null;
1953
2300
  setState({
1954
2301
  isStreaming: false,
1955
2302
  content: "",
2303
+ pendingLine: "",
1956
2304
  usage: void 0,
1957
2305
  error: void 0,
1958
2306
  activity: void 0,
1959
2307
  toolCalls: [],
1960
- startTime: void 0
2308
+ startTime: void 0,
2309
+ isPaused: false
1961
2310
  });
1962
2311
  }, []);
1963
2312
  return { state, startStream, cancelStream, reset };
@@ -4427,5 +4776,5 @@ async function runFirstRunSetup() {
4427
4776
  }
4428
4777
 
4429
4778
  export { runFirstRunSetup, startChatSession };
4430
- //# sourceMappingURL=App-JQ622M66.js.map
4431
- //# sourceMappingURL=App-JQ622M66.js.map
4779
+ //# sourceMappingURL=App-NT6MRKQJ.js.map
4780
+ //# sourceMappingURL=App-NT6MRKQJ.js.map