koneck 2.25.8 → 2.25.10

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
@@ -194,6 +194,15 @@ function Shimmer({ text, frame, base }) {
194
194
  }) }));
195
195
  }
196
196
  export const WORK_WORDS = {
197
+ // No tool ran: it thought and answered. A cooking metaphor is wrong here — nothing was made.
198
+ reply: [
199
+ ['Thinking', 'Thought it through'], ['Weighing it up', 'Weighed it up'],
200
+ ['Talking it through', 'Talked it through'], ['Laying it out', 'Laid it out'],
201
+ ['Reading you', 'Read you'], ['Working it out', 'Worked it out'],
202
+ ['Turning it over', 'Turned it over'], ['Sizing it up', 'Sized it up'],
203
+ ['Consulting', 'Consulted'], ['Advising', 'Advised'],
204
+ ['Counselling', 'Counselled'], ['Explaining', 'Explained'],
205
+ ],
197
206
  // Under ~3s. Snappy, almost dismissive — it barely counted as work.
198
207
  quick: [
199
208
  ['Sharp-sharp', 'Sharp-sharp'], ['Doing it now-now', 'Done now-now'],
@@ -203,6 +212,13 @@ export const WORK_WORDS = {
203
212
  ['Zapping', 'Zapped'], ['Sorting', 'Sorted'],
204
213
  ['Hot-reloading', 'Hot-reloaded'], ['JIT-compiling', 'JIT-compiled'],
205
214
  ['Instant-noodling', 'Instant-noodled'], ['Wazza-ing', "Wazza'd"],
215
+ // Counter work: over before you have put your wallet away.
216
+ ['Ringing it up', 'Rung up'], ['Swiping the card', 'Card swiped'],
217
+ ['Stamping it', 'Stamped'], ['Checking you in', 'Checked in'],
218
+ ['Taking the register', 'Register taken'], ['Triaging', 'Triaged'],
219
+ ['Waving it through', 'Waved through'], ['Blowing the whistle', 'Whistle blown'],
220
+ ['Ticking the box', 'Box ticked'], ['Handing over the key', 'Key handed over'],
221
+ ['Weighing it', 'Weighed'], ['Bagging it', 'Bagged'],
206
222
  ],
207
223
  // The everyday middle. General flex and completion.
208
224
  normal: [
@@ -220,6 +236,25 @@ export const WORK_WORDS = {
220
236
  ['Noodling', 'Noodled'], ['Rubber-ducking', 'Rubber-ducked'],
221
237
  ['Tree-shaking', 'Tree-shook'], ['Reticulating', 'Reticulated'],
222
238
  ['Letting it cook', 'Let it cook'], ['Getting it oga-certified', 'Oga-certified'],
239
+ // A shift's honest work: clinic, classroom, shop floor, counting house.
240
+ ['Doing the rounds', 'Rounds done'], ['Dispensing', 'Dispensed'],
241
+ ['Dressing the wound', 'Wound dressed'], ['Taking the pulse', 'Pulse taken'],
242
+ ['Marking the papers', 'Papers marked'], ['Setting the homework', 'Homework set'],
243
+ ['Taking the lesson', 'Lesson taken'], ['Turning down the beds', 'Beds turned down'],
244
+ ['Working the front desk', 'Front desk worked'], ['Laying the tables', 'Tables laid'],
245
+ ['Balancing the books', 'Books balanced'], ['Reconciling', 'Reconciled'],
246
+ ['Counting the till', 'Till counted'], ['Clearing the cheque', 'Cheque cleared'],
247
+ ['Running the line', 'Line ran'], ['Tooling up', 'Tooled up'],
248
+ ['Cutting the pattern', 'Pattern cut'], ['Stitching', 'Stitched'],
249
+ ['Pressing and finishing', 'Pressed and finished'], ['Grading the sizes', 'Sizes graded'],
250
+ ['Filing the papers', 'Papers filed'], ['Taking statements', 'Statements taken'],
251
+ ['Serving the summons', 'Summons served'], ['Standing guard', 'Guard stood'],
252
+ ['Walking the beat', 'Beat walked'], ['Canvassing', 'Canvassed'],
253
+ ['Working the room', 'Room worked'], ['Tabling the motion', 'Motion tabled'],
254
+ ['Haggling', 'Haggled'], ['Setting out the stall', 'Stall set out'],
255
+ ['Restocking the shelves', 'Shelves restocked'], ['Cashing up', 'Cashed up'],
256
+ ['Pounding the mealies', 'Mealies pounded'], ['Beating the drum', 'Drum beaten'],
257
+ ['Sitting with the elders', 'Elders consulted'], ['Weaving the mat', 'Mat woven'],
223
258
  ],
224
259
  // Over ~10s. Slow heat and visible effort — it earned the wait.
225
260
  heavy: [
@@ -234,6 +269,18 @@ export const WORK_WORDS = {
234
269
  ['Locking all the way in', 'Locked all the way in'],
235
270
  ['Negotiating with malloc', 'Negotiated with malloc'],
236
271
  ['Arguing with the type checker', 'Out-argued the type checker'],
272
+ // The long jobs: a night shift, a whole ward, a case that ran all day.
273
+ ['Doing the ward round', 'Ward round done'], ['Scrubbing in', 'Scrubbed out'],
274
+ ['Sitting the long clinic', 'Clinic finished'], ['Marking the whole class', 'Whole class marked'],
275
+ ['Sitting the exam', 'Exam sat'], ['Running the night shift', 'Night shift done'],
276
+ ['Working the full run', 'Full run done'], ['Cutting the whole marker', 'Marker cut'],
277
+ ['Closing the books', 'Books closed'], ['Auditing', 'Audited'],
278
+ ['Doing the stocktake', 'Stock taken'], ['Arguing the case', 'Case argued'],
279
+ ['Sitting in chambers', 'Judgment handed down'], ['Building the docket', 'Docket built'],
280
+ ['Doing the night patrol', 'Patrol done'], ['Standing the long watch', 'Watch stood'],
281
+ ['Holding the indaba', 'Indaba held'], ['Filibustering', 'Filibustered'],
282
+ ['Brewing umqombothi', 'Umqombothi brewed'], ['Keeping the fire going', 'Fire kept going'],
283
+ ['Walking it to market', 'Walked it to market'],
237
284
  ['Herding promises', 'Herded the promises'],
238
285
  ['Bribing the scheduler', 'Bribed the scheduler'],
239
286
  ['Smashing wazobia', 'Smashed wazobia'],
@@ -254,20 +301,49 @@ export const WORK_WORDS = {
254
301
  ['Duct-taping', 'Duct-taped'],
255
302
  ],
256
303
  };
257
- /** Which vocabulary the wait deserves. */
258
- export function tierFor(elapsedMs, recovered = false) {
259
- if (recovered)
304
+ /**
305
+ * Whether the turn earned a verb at all.
306
+ *
307
+ * "Cooked lekker in 5.0s" for answering "hi" is a boast about nothing: no tool ran, ten tokens
308
+ * came back, and the only reason it took five seconds is that the provider is slow. Elapsed time
309
+ * alone cannot tell effort from latency, so a turn that ran no tools and said almost nothing gets
310
+ * its timing reported plainly and no verb.
311
+ */
312
+ export function shouldName(effort) {
313
+ if (effort.recovered)
314
+ return true; // recovering from a failure is real work
315
+ if (effort.tools > 0)
316
+ return true; // it did something to the world
317
+ return effort.tokens >= 60; // a substantial answer is work; "hi" is not
318
+ }
319
+ /**
320
+ * Which family of words fits.
321
+ *
322
+ * Time still decides between quick, normal and heavy, but only once the turn has done something.
323
+ * A turn with no tools belongs to `reply` however long it took, because nothing was built and a
324
+ * cooking verb would be describing work that never happened.
325
+ */
326
+ export function tierFor(effort) {
327
+ if (effort.recovered)
260
328
  return 'recovery';
261
- if (elapsedMs < 3_000)
329
+ if (effort.tools === 0)
330
+ return 'reply';
331
+ if (effort.elapsedMs < 3_000)
262
332
  return 'quick';
263
- if (elapsedMs > 10_000)
333
+ if (effort.elapsedMs > 10_000)
264
334
  return 'heavy';
265
335
  return 'normal';
266
336
  }
267
- /** The gerund to show while working, and the past tense to report when the turn lands. */
268
- export function workWord(elapsedMs, seed, recovered = false) {
269
- const bucket = WORK_WORDS[tierFor(elapsedMs, recovered)];
270
- return bucket[Math.abs(seed) % bucket.length];
337
+ /**
338
+ * The gerund to show while working, and the past tense to report when the turn lands.
339
+ *
340
+ * The past tense is empty when the turn did not earn one, which the caller renders as timing
341
+ * alone.
342
+ */
343
+ export function workWord(effort, seed) {
344
+ const bucket = WORK_WORDS[tierFor(effort)];
345
+ const [gerund, past] = bucket[Math.abs(seed) % bucket.length];
346
+ return [gerund, shouldName(effort) ? past : ''];
271
347
  }
272
348
  /** Every slash command, shown in the `/` palette and filtered as you type. */
273
349
  const COMMANDS = [
@@ -284,6 +360,7 @@ const COMMANDS = [
284
360
  { cmd: '/ask', desc: 'Ask another session and wait: /ask <repo> <question>' },
285
361
  { cmd: '/agents', desc: 'How parallel sub-agents are configured' },
286
362
  { cmd: '/subtask', desc: 'Run a task in an isolated sub-agent' },
363
+ { cmd: '/btw', desc: 'Ask a side question without interrupting the running task' },
287
364
  { cmd: '/clear', desc: 'Clear conversation history' },
288
365
  { cmd: '/new', desc: 'New session (alias of /clear)' },
289
366
  { cmd: '/reset', desc: 'Reset session (alias of /clear)' },
@@ -387,6 +464,7 @@ Slash commands:
387
464
  /mode [auto|plan|code] Show or set mode
388
465
  /effort [low|med|high|max] Set reasoning effort
389
466
  /config [key] [value] View or set persistent config
467
+ /btw <question> Ask a side question without interrupting the running task
390
468
  /usage Usage pane: session, context, models, limits
391
469
  /tokens Show token usage breakdown
392
470
  /cost Show session cost
@@ -417,6 +495,8 @@ function App({ config: initialConfig }) {
417
495
  const replyStartedRef = useRef(false);
418
496
  /** Everything streamed this turn. Empty means the provider did not stream at all. */
419
497
  const streamedRef = useRef('');
498
+ /** When the live region last had something new to show, used to pick the repaint rate. */
499
+ const lastActivityRef = useRef(Date.now());
420
500
  /** Frames received before any content — proof the provider has the request and is working. */
421
501
  const [providerFrames, setProviderFrames] = useState(0);
422
502
  /**
@@ -435,6 +515,13 @@ function App({ config: initialConfig }) {
435
515
  const entriesRef = useRef(null);
436
516
  /** Where the `@` being completed starts in the draft. */
437
517
  const atStartRef = useRef(-1);
518
+ /**
519
+ * Prompts typed while a turn was running.
520
+ *
521
+ * Enter used to be swallowed outright, so a thought had to be held until the agent finished or
522
+ * typed again from memory. Queued prompts run in order as soon as the current turn ends.
523
+ */
524
+ const [queued, setQueued] = useState([]);
438
525
  /** Pre-edit contents of the files the running tool said it would touch. */
439
526
  const pendingDiffRef = useRef([]);
440
527
  const charsRef = useRef(0); // streamed chars, for the live token estimate
@@ -456,6 +543,7 @@ function App({ config: initialConfig }) {
456
543
  * whatever is left in the buffer.
457
544
  */
458
545
  function pushSay(text) {
546
+ lastActivityRef.current = Date.now();
459
547
  sayRef.current += text;
460
548
  streamedRef.current += text;
461
549
  charsRef.current += text.length;
@@ -500,6 +588,8 @@ function App({ config: initialConfig }) {
500
588
  function pushTool(name, args) {
501
589
  commitSay();
502
590
  // Photograph every file this tool has announced it will touch, before it touches it.
591
+ lastActivityRef.current = Date.now();
592
+ turnToolsRef.current += 1;
503
593
  pendingDiffRef.current = filesTouchedBy(name, args).slice(0, MAX_DIFF_FILES)
504
594
  .map(rel => ({ rel, before: readForDiff(rel) }));
505
595
  liveToolRef.current = { kind: 'tool', name, detail: toolDetail(name, args), startedAt: Date.now() };
@@ -547,6 +637,11 @@ function App({ config: initialConfig }) {
547
637
  onChunk: (text) => pushSay(text),
548
638
  onToolCall: (name, args) => pushTool(name, args),
549
639
  onToolResult: (name, ok, ms) => finishTool(name, ok, ms),
640
+ onToolOutput: (_name, line) => {
641
+ lastActivityRef.current = Date.now();
642
+ if (liveToolRef.current)
643
+ liveToolRef.current.output = line;
644
+ },
550
645
  onStreamActivity: (frames) => setProviderFrames(frames),
551
646
  onAgentProgress: (list) => { agentsRef.current = list; setAgents(list); },
552
647
  onTokens: (s) => {
@@ -672,7 +767,9 @@ function App({ config: initialConfig }) {
672
767
  * killing the session or losing what is already in the composer. */
673
768
  const abortRef = useRef(null);
674
769
  const turnWordRef = useRef(0); // rotation seed, so consecutive turns differ
675
- const turnRecoveredRef = useRef(false); // a tool failed and was worked around
770
+ const turnRecoveredRef = useRef(false);
771
+ /** Tools this turn has run, which is what separates work from a plain answer. */
772
+ const turnToolsRef = useRef(0); // a tool failed and was worked around
676
773
  const busyStart = useRef(0);
677
774
  const wordTimer = useRef(0);
678
775
  // Register on the session bus so other running KONECK instances can reach this one.
@@ -732,20 +829,47 @@ function App({ config: initialConfig }) {
732
829
  }
733
830
  busyStart.current = Date.now();
734
831
  wordTimer.current = 0;
735
- // 150ms, not 100ms: every tick repaints the whole live region, so the animation frame rate
736
- // is also the flicker rate. Six-and-a-bit frames a second still reads as a smooth sweep.
737
- const id = setInterval(() => {
832
+ // The frame rate is the flicker rate. Every tick makes Ink erase and rewrite the whole live
833
+ // region — measured at 188 line-erases a second, which on a modest terminal reads as the
834
+ // bottom of the screen flashing. So the rate follows how much is actually happening: quick
835
+ // while output is arriving, and much slower once a single long command is all that is left,
836
+ // where a spinner turning four times a second says as much as one turning seven.
837
+ let timer;
838
+ const tick = () => {
839
+ const elapsed = Date.now() - busyStart.current;
738
840
  setSpinFrame(f => (f + 1) % SPINNER.length);
739
841
  setShimmerFrame(f => f + 1);
740
- setElapsedMs(Date.now() - busyStart.current);
842
+ setElapsedMs(elapsed);
741
843
  wordTimer.current += 1;
742
- // 150 ticks = 15s. Long enough to read and settle on, short enough that a slow turn
743
- // still shows signs of life.
744
- if (wordTimer.current % 100 === 0)
745
- setSpinWord(w => w + 1); // ~15s per word
746
- }, 150);
747
- return () => clearInterval(id);
844
+ // Roughly every 25 seconds, whatever the current rate. Long enough to read the word,
845
+ // notice it, and still see it change before the wait starts feeling stuck.
846
+ if (wordTimer.current % Math.max(20, Math.round(25_000 / currentDelay())) === 0) {
847
+ setSpinWord(w => w + 1);
848
+ }
849
+ timer = setTimeout(tick, currentDelay());
850
+ };
851
+ const currentDelay = () => {
852
+ const idleFor = Date.now() - lastActivityRef.current;
853
+ // Something is still arriving, so keep it lively.
854
+ if (idleFor < 2_000)
855
+ return 150;
856
+ // A long wait with nothing to show: slow right down.
857
+ return idleFor > 15_000 ? 500 : 250;
858
+ };
859
+ timer = setTimeout(tick, 150);
860
+ return () => clearTimeout(timer);
748
861
  }, [busy]);
862
+ // A queued prompt runs the moment the agent is free again. Keyed on `busy` rather than done
863
+ // inside the turn's own exit paths, so every way a turn can end — success, failure, cancel —
864
+ // drains the queue the same way.
865
+ useEffect(() => {
866
+ if (busy || queued.length === 0)
867
+ return;
868
+ const [next, ...rest] = queued;
869
+ setQueued(rest);
870
+ if (next)
871
+ submitDraft(next);
872
+ }, [busy, queued]);
749
873
  // Ask the terminal to bracket pasted text. Without this a paste is indistinguishable from
750
874
  // typing, and there is no way to keep it out of the composer or preserve its line breaks.
751
875
  useEffect(() => {
@@ -878,6 +1002,35 @@ function App({ config: initialConfig }) {
878
1002
  return []; // the pane says so rather than showing zeroes
879
1003
  }
880
1004
  }
1005
+ /**
1006
+ * Answers a question without disturbing the turn in flight.
1007
+ *
1008
+ * The agent's own conversation is left untouched: the question runs in a separate, isolated
1009
+ * session, so asking "which model am I on?" mid-build neither interrupts the build nor pollutes
1010
+ * the history the agent is reasoning over. It is the one thing that may jump the queue.
1011
+ */
1012
+ async function askAside(question) {
1013
+ if (!question.trim()) {
1014
+ addSystem('Usage: /btw <question> — answers alongside the running task, without interrupting it.');
1015
+ return;
1016
+ }
1017
+ addRow({ role: 'user', text: `/btw ${question}` });
1018
+ try {
1019
+ const { runAgent } = await import('./engine.js');
1020
+ const result = await runAgent(question, {
1021
+ ...cfg, ci: true, silent: true,
1022
+ // A side question answers from what it is told; it must not edit files behind the
1023
+ // running task's back, so it is given no reason to and kept to a couple of turns.
1024
+ maxTurns: Math.min(cfg.maxTurns, 3),
1025
+ });
1026
+ const last = [...result.messages].reverse()
1027
+ .find(m => m.role === 'assistant' && typeof m.content === 'string' && m.content.trim());
1028
+ addRow({ role: 'agent', text: typeof last?.content === 'string' ? last.content : '(no answer)' });
1029
+ }
1030
+ catch (e) {
1031
+ addSystem(`/btw failed: ${e instanceof Error ? e.message : String(e)}`);
1032
+ }
1033
+ }
881
1034
  /** Opens the update panel, checking the registry each time it is opened deliberately. */
882
1035
  async function openUpdatePane() {
883
1036
  setUpdatePane({ current: VERSION, latest: null, behind: false, manager: 'npm' });
@@ -1151,6 +1304,11 @@ function App({ config: initialConfig }) {
1151
1304
  addSystem(`Effort → ${arg}`);
1152
1305
  return;
1153
1306
  }
1307
+ case '/btw': {
1308
+ // Also available when nothing is running, where it simply answers in a clean context.
1309
+ await askAside(arg);
1310
+ return;
1311
+ }
1154
1312
  case '/usage': {
1155
1313
  const wanted = USAGE_TABS.find(t => t.startsWith(arg.trim().toLowerCase()));
1156
1314
  await openUsagePane(wanted ?? 'session');
@@ -1769,8 +1927,24 @@ function App({ config: initialConfig }) {
1769
1927
  // while it works can be typed as it arrives rather than held until the turn ends — and
1770
1928
  // cancelling with esc then hands the keyboard back to a composer that still has it.
1771
1929
  if (key.return) {
1772
- if (busy)
1930
+ if (busy) {
1931
+ const text = draft.trim();
1932
+ if (!text)
1933
+ return;
1934
+ // A side question is answered straight away and separately; everything else waits its turn.
1935
+ if (/^\/btw(\s|$)/.test(text)) {
1936
+ setDraft('');
1937
+ setCaret(0);
1938
+ void askAside(text.replace(/^\/btw\s*/, ''));
1939
+ return;
1940
+ }
1941
+ setQueued(q => [...q, expandPastes(text, pastesRef.current)]);
1942
+ setDraft('');
1943
+ setCaret(0);
1944
+ pastesRef.current = [];
1945
+ setPasteCount(0);
1773
1946
  return;
1947
+ }
1774
1948
  submitDraft(draft);
1775
1949
  return;
1776
1950
  }
@@ -1917,6 +2091,7 @@ function App({ config: initialConfig }) {
1917
2091
  sayRef.current = '';
1918
2092
  streamedRef.current = '';
1919
2093
  replyStartedRef.current = false;
2094
+ lastActivityRef.current = Date.now();
1920
2095
  setProviderFrames(0);
1921
2096
  liveToolRef.current = null;
1922
2097
  charsRef.current = 0;
@@ -1924,6 +2099,7 @@ function App({ config: initialConfig }) {
1924
2099
  tokBaseRef.current = stats.completionTokens;
1925
2100
  turnWordRef.current = spinWord;
1926
2101
  turnRecoveredRef.current = false;
2102
+ turnToolsRef.current = 0;
1927
2103
  void (async () => {
1928
2104
  const outTokBefore = stats.completionTokens;
1929
2105
  try {
@@ -1978,7 +2154,12 @@ function App({ config: initialConfig }) {
1978
2154
  { role: 'agent', text: body,
1979
2155
  ...(streamed !== '' && replyStartedRef.current ? { continued: true } : {}),
1980
2156
  tokens: Math.max(0, active.stats.completionTokens - outTokBefore), elapsed,
1981
- done: workWord(elapsed, turnWordRef.current, turnRecoveredRef.current)[1] },
2157
+ done: workWord({
2158
+ elapsedMs: elapsed,
2159
+ tools: turnToolsRef.current,
2160
+ tokens: Math.max(0, active.stats.completionTokens - outTokBefore),
2161
+ recovered: turnRecoveredRef.current,
2162
+ }, turnWordRef.current)[1] },
1982
2163
  ]);
1983
2164
  setAgentState('completed');
1984
2165
  setTimeout(() => setAgentState('ready'), 700);
@@ -2080,7 +2261,7 @@ function App({ config: initialConfig }) {
2080
2261
  const ms = (step.endedAt ?? Date.now()) - step.startedAt;
2081
2262
  const glyph = running ? SPINNER[spinFrame] : step.ok === false ? '✖' : '✓';
2082
2263
  const color = running ? AMBER : step.ok === false ? CRIMSON : GREEN;
2083
- 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));
2264
+ 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 })] })), running && step.output && (_jsx(Box, { paddingLeft: 4, children: _jsx(Text, { color: DIM, children: fitCells(step.output, Math.max(20, barWidth - 6)) }) })), (step.diffs ?? []).map((d, i) => renderDiff(d, i))] }, key));
2084
2265
  };
2085
2266
  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' ? (
2086
2267
  // Painted as a filled bar so the user's own words are the most findable thing
@@ -2096,8 +2277,8 @@ function App({ config: initialConfig }) {
2096
2277
  // content, so the right edge stepped in and out; and a box inside a box left
2097
2278
  // markdown tables needlessly narrow. A marker plus the content reads better and
2098
2279
  // cannot misalign, which is how Claude Code presents its own answers.
2099
- _jsxs(Box, { flexDirection: "column", marginTop: row.continued ? 0 : 1, children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: CYAN, bold: true, children: row.continued ? ' ' : 'ꞰK' }), _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() !== '' && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: CYAN, bold: true, children: replyStartedRef.current ? ' ' : 'ꞰK' }), _jsx(Box, { flexDirection: "column", children: tailLines(sayRef.current, liveReplyLines(termRows), replyWidth - 3)
2100
- .split('\n').map((line, i) => _jsx(Text, { color: INK, children: line }, i)) })] })), liveToolRef.current && renderStep(liveToolRef.current, 1), agents.length > 0 && (() => {
2280
+ _jsxs(Box, { flexDirection: "column", marginTop: row.continued ? 0 : 1, children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: CYAN, bold: true, children: row.continued ? ' ' : 'ꞰK' }), _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 ? `${row.done} in ${fmtElapsed(row.elapsed ?? 0)}` : 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() !== '' && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: CYAN, bold: true, children: replyStartedRef.current ? ' ' : 'ꞰK' }), _jsx(Box, { flexDirection: "column", children: tailLines(sayRef.current, liveReplyLines(termRows), replyWidth - 3)
2281
+ .split('\n').map((line, i) => _jsx(Text, { color: INK, children: line }, i)) })] })), liveToolRef.current && renderStep(liveToolRef.current, 1), queued.length > 0 && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: AMBER, children: "\u25AA" }), _jsxs(Text, { color: MUTED, children: [queued.length, " queued, will run when this finishes \u00B7 /btw to ask without waiting"] })] }), queued.map((q, i) => (_jsx(Box, { paddingLeft: 2, children: _jsx(Text, { color: DIM, children: fitCells(q.split('\n')[0] ?? '', Math.max(20, barWidth - 6), true) }) }, i)))] })), agents.length > 0 && (() => {
2101
2282
  const done = agents.filter(a => a.status !== 'running').length;
2102
2283
  const failed = agents.filter(a => a.status === 'failed').length;
2103
2284
  const tokens = agents.reduce((n, a) => n + a.tokens, 0);
@@ -2114,7 +2295,10 @@ function App({ config: initialConfig }) {
2114
2295
  const task = a.task.length > taskWidth ? a.task.slice(0, taskWidth - 3) + '...' : a.task;
2115
2296
  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: fitCells(task, taskWidth) }), _jsx(Text, { color: DIM, children: humanTokens(a.tokens).padStart(6) }), _jsx(Text, { color: DIM, children: fmtElapsed(took).padStart(6) })] }, a.index));
2116
2297
  })] }));
2117
- })(), _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
2298
+ })(), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: [_jsxs(Text, { color: GREEN, children: [SPINNER[spinFrame], " "] }), _jsx(Shimmer, { text: workWord({
2299
+ elapsedMs, tools: turnToolsRef.current,
2300
+ tokens: liveTokens, recovered: turnRecoveredRef.current,
2301
+ }, spinWord)[0], frame: shimmerFrame, base: MUTED }), _jsx(Text, { color: MUTED, children: "\u2026 " }), _jsxs(Text, { color: DIM, children: ["(", fmtElapsed(elapsedMs), " | ", liveTokens > 0
2118
2302
  ? `${fmtTokens(liveTokens)} tokens`
2119
2303
  // A gateway routing to several backends sends content-free frames while it
2120
2304
  // finds one. Saying "waiting for the first token" through that reads as a