koneck 2.25.2 → 2.25.4

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/dist/ink-chat.js CHANGED
@@ -8,11 +8,12 @@ import { generateSessionId, saveSession, listSessions, loadSession } from './ses
8
8
  import { loadMemory } from './memory.js';
9
9
  import { loadKoneckConfig, saveKoneckConfig, setConfigKey, configKeyDescriptions, CONFIG_FILE } from './config-store.js';
10
10
  import { PROVIDERS, resolveProvider, getApiKey, addUserProvider, userProvidersPath } from './providers.js';
11
- import { listCheckpoints, revertCheckpoint, snapshotTree } from './checkpoint.js';
12
- import { bar, humanTokens, humanDuration, diffStatSince, modelInfoFrom, contextWindowFor, recordStatus, classifyResponse, stateLabel, readModelStatus, } from './usage.js';
11
+ import { listCheckpoints, revertCheckpoint, snapshotTree, filesTouchedBy } from './checkpoint.js';
12
+ import { fileDiff, diffSummary } from './diff-view.js';
13
+ import { bar, humanTokens, humanDuration, diffStatSince, modelInfoFrom, contextWindowFor, recordStatus, classifyResponse, stateLabel, readModelStatus, computeUiWidth, } from './usage.js';
13
14
  import { Markdown, Panel } from './markdown-ink.js';
14
15
  import { fetchUpdateStatus, updateCommand } from './update-check.js';
15
- import { readFileSync } from 'fs';
16
+ import { readFileSync, statSync } from 'fs';
16
17
  import { fileURLToPath } from 'url';
17
18
  import path from 'path';
18
19
  /** Read once at load so the running version can always be shown without a lookup per render. */
@@ -33,6 +34,13 @@ const AMBER = '#F6C453';
33
34
  const MUTED = '#9AAAB2';
34
35
  const INK = '#F4F1EA';
35
36
  const DIM = '#5C717A';
37
+ // Diff colours. Deliberately not terminal green/red: those clash with the warm palette and read
38
+ // as errors. These are the existing accents lifted a little, over bands dark enough to sit on a
39
+ // black terminal without glowing.
40
+ const DIFF_ADD = '#7FE3B0';
41
+ const DIFF_ADD_BG = '#10291F';
42
+ const DIFF_DEL = '#F2909B';
43
+ const DIFF_DEL_BG = '#2B1419';
36
44
  // The user's message bar: light-on-dark, high contrast against the terminal ground so an
37
45
  // input is easy to find when scrolling back. Deliberately not the cyan used for KONECK.
38
46
  const USER_BG = '#3A434A';
@@ -257,6 +265,10 @@ const EFFORT_BLURB = {
257
265
  high: 'Thorough — covers edge cases, costs more tokens',
258
266
  max: 'Most rigorous analysis available; slowest and priciest',
259
267
  };
268
+ /** Past this, a file is not read for diffing — the render would be useless anyway. */
269
+ const MAX_DIFF_BYTES = 512 * 1024;
270
+ /** One tool rarely touches more; the cap stops a bulk move from flooding scrollback. */
271
+ const MAX_DIFF_FILES = 4;
260
272
  /** The tabs of the usage pane, in the order they are shown. */
261
273
  const USAGE_TABS = ['session', 'context', 'models', 'limits'];
262
274
  const MIN_REPLY_WIDTH = 24; // below this a table cannot render legibly anyway
@@ -331,6 +343,8 @@ function App({ config: initialConfig }) {
331
343
  // instead of calling setState per token keeps a Raspberry Pi from re-rendering on every chunk.
332
344
  const sayRef = useRef(''); // narration not yet committed
333
345
  const liveToolRef = useRef(null); // the one tool still running
346
+ /** Pre-edit contents of the files the running tool said it would touch. */
347
+ const pendingDiffRef = useRef([]);
334
348
  const charsRef = useRef(0); // streamed chars, for the live token estimate
335
349
  const realTokRef = useRef(0); // real output tokens this turn, once the provider reports usage
336
350
  const tokBaseRef = useRef(0); // session output-token count at the start of this turn
@@ -349,25 +363,73 @@ function App({ config: initialConfig }) {
349
363
  if (text !== '')
350
364
  setRows(prev => [...prev, { role: 'steps', steps: [{ kind: 'say', text }] }]);
351
365
  }
366
+ /**
367
+ * Reads a file for diffing, or null if it is absent or too big to be worth showing. Reading is
368
+ * synchronous on purpose: the tool runs the instant this callback returns, so an await here
369
+ * would race the very write it is trying to photograph.
370
+ */
371
+ function readForDiff(rel) {
372
+ try {
373
+ const abs = path.resolve(cfg.cwd, rel);
374
+ if (statSync(abs).size > MAX_DIFF_BYTES)
375
+ return null;
376
+ return readFileSync(abs, 'utf-8');
377
+ }
378
+ catch {
379
+ return null; // absent: a creation, which fileDiff renders as such
380
+ }
381
+ }
352
382
  function pushTool(name, args) {
353
383
  commitSay();
384
+ // Photograph every file this tool has announced it will touch, before it touches it.
385
+ pendingDiffRef.current = filesTouchedBy(name, args).slice(0, MAX_DIFF_FILES)
386
+ .map(rel => ({ rel, before: readForDiff(rel) }));
354
387
  liveToolRef.current = { kind: 'tool', name, detail: toolDetail(name, args), startedAt: Date.now() };
355
388
  }
356
389
  function finishTool(name, ok, ms) {
357
390
  if (!ok)
358
391
  turnRecoveredRef.current = true;
359
392
  const live = liveToolRef.current;
393
+ const pending = pendingDiffRef.current;
360
394
  liveToolRef.current = null;
395
+ pendingDiffRef.current = [];
361
396
  if (!live)
362
397
  return;
363
- const done = { ...live, endedAt: live.startedAt + ms, ok };
398
+ // A failed tool leaves the file as it was, so there is nothing to show.
399
+ const diffs = ok
400
+ ? pending
401
+ .map(({ rel, before }) => fileDiff(rel, before, readForDiff(rel)))
402
+ .filter(d => !d.unchanged)
403
+ : [];
404
+ const done = { ...live, endedAt: live.startedAt + ms, ok, ...(diffs.length ? { diffs } : {}) };
364
405
  setRows(prev => [...prev, { role: 'steps', steps: [done] }]);
365
406
  }
407
+ /**
408
+ * Moves the agent panel into scrollback once the turn is over. The live panel is transient by
409
+ * design — it repaints — so without this the record of what the agents did would scroll away
410
+ * with the last repaint and leave nothing behind.
411
+ */
412
+ function flushAgents() {
413
+ const list = agentsRef.current;
414
+ if (list.length === 0)
415
+ return;
416
+ agentsRef.current = [];
417
+ setAgents([]);
418
+ const done = list.filter(a => a.status === 'done').length;
419
+ const failed = list.filter(a => a.status === 'failed').length;
420
+ const tokens = list.reduce((n, a) => n + a.tokens, 0);
421
+ // The elapsed span is the longest agent, not the sum: they ran at the same time.
422
+ const span = Math.max(...list.map(a => (a.endedAt ?? Date.now()) - a.startedAt));
423
+ addRow({ role: 'steps', steps: [{ kind: 'say', text: `${list.length} parallel agents finished — ${done} succeeded` +
424
+ (failed ? `, ${failed} failed` : '') +
425
+ ` · ${humanTokens(tokens)} tokens · ${fmtElapsed(span)}` }] });
426
+ }
366
427
  // Session Promise is created eagerly at mount so it resolves before the user's first message.
367
428
  const sessionOpts = {
368
429
  onChunk: (text) => pushSay(text),
369
430
  onToolCall: (name, args) => pushTool(name, args),
370
431
  onToolResult: (name, ok, ms) => finishTool(name, ok, ms),
432
+ onAgentProgress: (list) => { agentsRef.current = list; setAgents(list); },
371
433
  onTokens: (s) => {
372
434
  realTokRef.current = Math.max(0, s.completionTokens - tokBaseRef.current);
373
435
  },
@@ -404,6 +466,29 @@ function App({ config: initialConfig }) {
404
466
  // compare against. Collapsing the two would flash "not a git repo" at every open.
405
467
  const [usageDiff, setUsageDiff] = useState(undefined);
406
468
  const [modelCatalog, setModelCatalog] = useState([]);
469
+ // Terminal width, tracked as state rather than read at render. Ink re-renders its own layout on
470
+ // a resize but has no idea a component consulted process.stdout.columns, so without this the UI
471
+ // kept the width it booted with until restarted.
472
+ // `|| 80` not `?? 80`: a pty with no negotiated size reports 0 columns, and 0 would collapse
473
+ // the layout to a single character.
474
+ const [termCols, setTermCols] = useState(() => process.stdout.columns || 80);
475
+ /** Optional user cap on the UI width. Undefined means fill the terminal. */
476
+ const [configMaxWidth, setConfigMaxWidth] = useState(undefined);
477
+ useEffect(() => {
478
+ void loadKoneckConfig().then(c => {
479
+ setConfigMaxWidth(typeof c.maxWidth === 'number' && c.maxWidth >= 40 ? c.maxWidth : undefined);
480
+ });
481
+ }, []);
482
+ useEffect(() => {
483
+ const onResize = () => setTermCols(process.stdout.columns || 80);
484
+ process.stdout.on('resize', onResize);
485
+ return () => { process.stdout.off('resize', onResize); };
486
+ }, []);
487
+ /** Live state of parallel sub-agents. Empty except while spawn_agents is running. */
488
+ const [agents, setAgents] = useState([]);
489
+ // Mirrored in a ref because the turn's exit paths run inside a closure that would otherwise
490
+ // see the state as it was when the turn began.
491
+ const agentsRef = useRef([]);
407
492
  // Masked credential entry. The value is held in component state for the session and passed
408
493
  // to the client as a runtime override — KONECK never writes it to disk, and it is never put
409
494
  // into a transcript row, so it cannot end up in a saved session file.
@@ -1015,6 +1100,7 @@ function App({ config: initialConfig }) {
1015
1100
  const val = parts.slice(1).join(' ');
1016
1101
  if (key === 'reset') {
1017
1102
  await saveKoneckConfig({});
1103
+ setConfigMaxWidth(undefined);
1018
1104
  addSystem('Config reset to defaults.');
1019
1105
  return;
1020
1106
  }
@@ -1031,6 +1117,10 @@ function App({ config: initialConfig }) {
1031
1117
  }
1032
1118
  if (key === 'effort' && ['low', 'medium', 'high', 'max'].includes(val))
1033
1119
  setEffort(val);
1120
+ if (key === 'maxWidth') {
1121
+ const n = Number(val);
1122
+ setConfigMaxWidth(Number.isFinite(n) && n >= 40 ? n : undefined);
1123
+ }
1034
1124
  return;
1035
1125
  }
1036
1126
  if (key) {
@@ -1044,6 +1134,7 @@ function App({ config: initialConfig }) {
1044
1134
  const live = {
1045
1135
  provider: cfg.provider, model: cfg.model, effort, requireApproval: cfg.requireApproval,
1046
1136
  maxTurns: cfg.maxTurns, budget: cfg.budget, baseURL: cfg.baseURL,
1137
+ maxWidth: configMaxWidth ?? `(not set - filling ${termCols} columns)`,
1047
1138
  };
1048
1139
  const lines2 = Object.entries(descs).map(([k, desc]) => {
1049
1140
  const v = live[k] ?? stored[k];
@@ -1576,6 +1667,7 @@ function App({ config: initialConfig }) {
1576
1667
  if (controller.signal.aborted) {
1577
1668
  sayRef.current = '';
1578
1669
  liveToolRef.current = null;
1670
+ flushAgents();
1579
1671
  addRow({ role: 'system', text: `Cancelled after ${fmtElapsed(Date.now() - started)}. Anything already done is above; ` +
1580
1672
  'your composer is untouched.' });
1581
1673
  setAgentState('ready');
@@ -1588,6 +1680,7 @@ function App({ config: initialConfig }) {
1588
1680
  const content = last && typeof last.content === 'string'
1589
1681
  ? last.content : 'Task finished with no written response.';
1590
1682
  setLastAgentText(content);
1683
+ flushAgents();
1591
1684
  const elapsed = Date.now() - started;
1592
1685
  apiMsRef.current += elapsed;
1593
1686
  // A turn that completed is the only proof a model actually works that costs nothing
@@ -1627,6 +1720,7 @@ function App({ config: initialConfig }) {
1627
1720
  const trailing = sayRef.current.trim();
1628
1721
  sayRef.current = '';
1629
1722
  liveToolRef.current = null;
1723
+ flushAgents();
1630
1724
  setRows(prev => [
1631
1725
  ...prev,
1632
1726
  ...(trailing ? [{ role: 'steps', steps: [{ kind: 'say', text: trailing }] }] : []),
@@ -1654,11 +1748,10 @@ function App({ config: initialConfig }) {
1654
1748
  return parts.length > 2 ? '.../' + parts.slice(-2).join('/') : full;
1655
1749
  })();
1656
1750
  const modelShort = cfg.model.split('/').at(-1) ?? cfg.model;
1657
- // `|| 80` not `?? 80`: a pty with no negotiated size reports columns as 0, and 0 would
1658
- // collapse the whole layout to a one-character column. The floor of 40 keeps a very
1659
- // narrow split pane readable instead of wrapping every word.
1660
- const termCols = process.stdout.columns || 80;
1661
- const uiWidth = Math.max(40, Math.min(termCols, 120));
1751
+ // The UI fills the terminal. It used to stop at 120 columns, which left most of a wide
1752
+ // window empty. A cap is still available through `maxWidth` in config for anyone who
1753
+ // prefers a narrow measure on a very wide screen, but nothing is capped by default.
1754
+ const uiWidth = computeUiWidth(termCols, configMaxWidth);
1662
1755
  const barWidth = uiWidth - 4; // uiWidth minus the container's paddingX={2} on both sides
1663
1756
  // Space a rendered reply actually has: the bar, minus the response box border (2), its
1664
1757
  // paddingX={1} (2), and the "ꓘK " prefix plus its gap (3). Getting this even one column
@@ -1678,6 +1771,29 @@ function App({ config: initialConfig }) {
1678
1771
  // Live output token count: real usage when the provider reports it, else a char estimate
1679
1772
  const liveTokens = Math.max(realTokRef.current, Math.round(charsRef.current / 4));
1680
1773
  /** Renders one activity step. Shared by the live view and committed scrollback so they match. */
1774
+ /**
1775
+ * A file change, drawn the way a diff is read: a gutter of line numbers, a sign, and the line
1776
+ * itself on a tinted band. Every rendered line is padded to the same width so the tint forms a
1777
+ * solid block rather than a ragged edge, which is what makes an added run legible at a glance.
1778
+ */
1779
+ const renderDiff = (d, key) => {
1780
+ const numbers = d.hunks.flatMap(h => h.lines.map(l => l.after ?? l.before ?? 0));
1781
+ const gutter = Math.max(2, String(Math.max(0, ...numbers)).length);
1782
+ const width = Math.max(24, barWidth - gutter - 8);
1783
+ const header = d.created ? 'new file' : d.deleted ? 'deleted' : diffSummary(d);
1784
+ return (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: DIM, children: "\u2514" }), _jsx(Text, { color: CYAN, children: d.path }), _jsx(Text, { color: d.created ? DIFF_ADD : d.deleted ? DIFF_DEL : MUTED, children: header })] }), d.binary
1785
+ ? _jsx(Text, { color: DIM, children: ' binary file - not shown' })
1786
+ : d.hunks.map((hunk, hi) => (_jsxs(Box, { flexDirection: "column", children: [hi > 0 && _jsxs(Text, { color: DIM, children: [' '.repeat(gutter), " ..."] }), hunk.lines.map((l, li) => {
1787
+ const shown = l.kind === 'del' ? l.before : l.after;
1788
+ const sign = l.kind === 'add' ? '+' : l.kind === 'del' ? '-' : ' ';
1789
+ const fg = l.kind === 'add' ? DIFF_ADD : l.kind === 'del' ? DIFF_DEL : MUTED;
1790
+ const bg = l.kind === 'add' ? DIFF_ADD_BG : l.kind === 'del' ? DIFF_DEL_BG : undefined;
1791
+ // Tabs would make the tinted block a ragged width, so they are expanded.
1792
+ const body = `${sign} ${l.text}`.replace(/\t/g, ' ');
1793
+ const cell = body.length > width ? body.slice(0, width - 3) + '...' : body.padEnd(width);
1794
+ return (_jsxs(Box, { children: [_jsxs(Text, { color: DIM, children: [String(shown ?? '').padStart(gutter), " "] }), _jsx(Text, { color: fg, ...(bg ? { backgroundColor: bg } : {}), children: cell })] }, li));
1795
+ })] }, hi))), d.hiddenLines > 0 && (_jsxs(Text, { color: DIM, children: [' '.repeat(gutter), " ... ", d.hiddenLines, " more changed lines"] }))] }, key));
1796
+ };
1681
1797
  const renderStep = (step, key) => {
1682
1798
  if (step.kind === 'say') {
1683
1799
  return (_jsxs(Box, { marginTop: 1, gap: 1, children: [_jsx(Text, { color: CYAN, children: "\u25CF" }), _jsx(Text, { color: INK, children: step.text.trim() })] }, key));
@@ -1686,7 +1802,7 @@ function App({ config: initialConfig }) {
1686
1802
  const ms = (step.endedAt ?? Date.now()) - step.startedAt;
1687
1803
  const glyph = running ? SPINNER[spinFrame] : step.ok === false ? '✖' : '✓';
1688
1804
  const color = running ? AMBER : step.ok === false ? CRIMSON : GREEN;
1689
- return (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: color, children: glyph }), _jsx(Text, { color: AMBER, bold: true, children: step.name }), _jsx(Text, { color: DIM, children: "\u00B7" }), _jsx(Text, { color: MUTED, children: fmtElapsed(ms) })] }), step.detail !== '' && (_jsxs(Box, { paddingLeft: 2, gap: 1, children: [_jsx(Text, { color: DIM, children: "\u2514" }), _jsx(Text, { color: MUTED, children: step.detail })] }))] }, key));
1805
+ return (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: color, children: glyph }), _jsx(Text, { color: AMBER, bold: true, children: step.name }), _jsx(Text, { color: DIM, children: "\u00B7" }), _jsx(Text, { color: MUTED, children: fmtElapsed(ms) })] }), step.detail !== '' && (step.diffs ?? []).length === 0 && (_jsxs(Box, { paddingLeft: 2, gap: 1, children: [_jsx(Text, { color: DIM, children: "\u2514" }), _jsx(Text, { color: MUTED, children: step.detail })] })), (step.diffs ?? []).map((d, i) => renderDiff(d, i))] }, key));
1690
1806
  };
1691
1807
  return (_jsxs(Box, { flexDirection: "column", width: uiWidth, paddingX: 2, children: [showAnalytics && (_jsxs(Box, { borderStyle: "round", borderColor: AMBER, paddingX: 1, marginBottom: 1, flexDirection: "column", width: barWidth, children: [_jsx(Text, { color: AMBER, bold: true, children: "\u25C6 Session Analytics" }), _jsxs(Text, { color: MUTED, children: ["Turns : ", _jsx(Text, { color: INK, children: stats.turns })] }), _jsxs(Text, { color: MUTED, children: ["Prompt tok : ", _jsx(Text, { color: INK, children: stats.promptTokens.toLocaleString() })] }), _jsxs(Text, { color: MUTED, children: ["Output tok : ", _jsx(Text, { color: INK, children: stats.completionTokens.toLocaleString() })] }), _jsxs(Text, { color: MUTED, children: ["Total tok : ", _jsx(Text, { color: INK, children: stats.totalTokens.toLocaleString() })] }), _jsxs(Text, { color: MUTED, children: ["Model : ", _jsx(Text, { color: INK, children: modelShort }), " Provider: ", _jsx(Text, { color: INK, children: cfg.provider })] }), _jsx(Text, { color: DIM, children: "Tab to close" })] })), _jsx(Static, { items: rows, children: (row, index) => row.role === 'header' ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "column", alignItems: "center", marginTop: 1, marginBottom: 1, children: [_jsx(Text, { color: CYAN, bold: true, children: "KONECK / SOFTWARE DELIVERY SYSTEM" }), _jsx(Text, { color: MUTED, children: "Deliberate engineering, with control at every change." })] }), _jsx(Panel, { width: barWidth, color: CYAN, title: "KONECK", children: [`MODEL: \`${modelShort}\` PROVIDER: \`${cfg.provider}\` WORKSPACE: \`${workspace}\``] })] }, index)) : row.role === 'user' ? (
1692
1808
  // Painted as a filled bar so the user's own words are the most findable thing
@@ -1696,7 +1812,24 @@ function App({ config: initialConfig }) {
1696
1812
  // content, so the right edge stepped in and out; and a box inside a box left
1697
1813
  // markdown tables needlessly narrow. A marker plus the content reads better and
1698
1814
  // cannot misalign, which is how Claude Code presents its own answers.
1699
- _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: CYAN, bold: true, children: "\uA7B0K" }), _jsx(Box, { flexDirection: "column", children: _jsx(Markdown, { text: row.text ?? '', width: barWidth - 3 }) })] }), row.tokens != null && (_jsxs(Text, { color: MUTED, children: [_jsx(Text, { color: GREEN, children: "* " }), row.done ?? 'Done', " in ", fmtElapsed(row.elapsed ?? 0), ' | ', row.tokens, " tokens", ' | ', cfg.provider, " ", modelShort] }))] }, index)) }), _jsx(Box, { flexDirection: "column", children: busy && (agentState === 'processing' || agentState === 'syncing') && (_jsxs(Box, { flexDirection: "column", children: [sayRef.current.trim() !== '' && renderStep({ kind: 'say', text: tailLines(sayRef.current, 2, replyWidth) }, 0), liveToolRef.current && renderStep(liveToolRef.current, 1), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: [_jsxs(Text, { color: GREEN, children: [SPINNER[spinFrame], " "] }), _jsx(Shimmer, { text: workWord(elapsedMs, spinWord, turnRecoveredRef.current)[0], frame: shimmerFrame, base: MUTED }), _jsx(Text, { color: MUTED, children: "\u2026 " }), _jsxs(Text, { color: DIM, children: ["(", fmtElapsed(elapsedMs), " | ", liveTokens > 0
1815
+ _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: CYAN, bold: true, children: "\uA7B0K" }), _jsx(Box, { flexDirection: "column", children: _jsx(Markdown, { text: row.text ?? '', width: barWidth - 3 }) })] }), row.tokens != null && (_jsxs(Text, { color: MUTED, children: [_jsx(Text, { color: GREEN, children: "* " }), row.done ?? 'Done', " in ", fmtElapsed(row.elapsed ?? 0), ' | ', row.tokens, " tokens", ' | ', cfg.provider, " ", modelShort] }))] }, index)) }), _jsx(Box, { flexDirection: "column", children: busy && (agentState === 'processing' || agentState === 'syncing') && (_jsxs(Box, { flexDirection: "column", children: [sayRef.current.trim() !== '' && renderStep({ kind: 'say', text: tailLines(sayRef.current, 2, replyWidth) }, 0), liveToolRef.current && renderStep(liveToolRef.current, 1), agents.length > 0 && (() => {
1816
+ const done = agents.filter(a => a.status !== 'running').length;
1817
+ const failed = agents.filter(a => a.status === 'failed').length;
1818
+ const tokens = agents.reduce((n, a) => n + a.tokens, 0);
1819
+ const started = Math.min(...agents.map(a => a.startedAt));
1820
+ // The task text is clipped to whatever is left after the fixed columns, so a long
1821
+ // task never wraps and breaks the alignment of the rows under it.
1822
+ const taskWidth = Math.max(16, barWidth - 34);
1823
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: CYAN, children: "\u25C6" }), _jsxs(Text, { color: INK, bold: true, children: [agents.length, " agents"] }), _jsx(Text, { color: DIM, children: "\u00B7" }), _jsxs(Text, { color: done === agents.length ? GREEN : AMBER, children: [done, "/", agents.length, " complete"] }), _jsx(Text, { color: DIM, children: "\u00B7" }), _jsxs(Text, { color: MUTED, children: [humanTokens(tokens), " tok"] }), _jsx(Text, { color: DIM, children: "\u00B7" }), _jsx(Text, { color: MUTED, children: fmtElapsed(Date.now() - started) }), failed > 0 && _jsxs(Text, { color: CRIMSON, children: ["\u00B7 ", failed, " failed"] })] }), agents.map(a => {
1824
+ const glyph = a.status === 'running' ? SPINNER[spinFrame]
1825
+ : a.status === 'failed' ? '✖' : '✓';
1826
+ const color = a.status === 'running' ? AMBER
1827
+ : a.status === 'failed' ? CRIMSON : GREEN;
1828
+ const took = (a.endedAt ?? Date.now()) - a.startedAt;
1829
+ const task = a.task.length > taskWidth ? a.task.slice(0, taskWidth - 3) + '...' : a.task;
1830
+ return (_jsxs(Box, { paddingLeft: 2, gap: 1, children: [_jsx(Text, { color: color, children: glyph }), _jsx(Text, { color: DIM, children: String(a.index + 1).padStart(2) }), _jsx(Text, { color: a.status === 'running' ? INK : MUTED, children: task.padEnd(taskWidth) }), _jsx(Text, { color: DIM, children: humanTokens(a.tokens).padStart(6) }), _jsx(Text, { color: DIM, children: fmtElapsed(took).padStart(6) })] }, a.index));
1831
+ })] }));
1832
+ })(), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: [_jsxs(Text, { color: GREEN, children: [SPINNER[spinFrame], " "] }), _jsx(Shimmer, { text: workWord(elapsedMs, spinWord, turnRecoveredRef.current)[0], frame: shimmerFrame, base: MUTED }), _jsx(Text, { color: MUTED, children: "\u2026 " }), _jsxs(Text, { color: DIM, children: ["(", fmtElapsed(elapsedMs), " | ", liveTokens > 0
1700
1833
  ? `${fmtTokens(liveTokens)} tokens`
1701
1834
  : elapsedMs > 20_000 ? 'waiting for the first token' : 'starting', ")"] })] }) }), _jsx(Box, { paddingLeft: 2, children: _jsx(Text, { color: DIM, children: "esc to cancel and keep your input \u00B7 ctrl+c to exit" }) })] })) }), usagePane && (() => {
1702
1835
  const width = Math.min(barWidth, 66);