koneck 2.128.0 → 2.128.1

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
@@ -13,7 +13,8 @@ import { PROVIDERS, resolveProvider, getApiKey, addUserProvider, removeUserProvi
13
13
  import { normaliseEndpoint, withEndpoint, savedEndpoints, shortSource, resolveModelChoice, withModel } from './endpoints.js';
14
14
  import { loadProjectConfig, loadGlobalConfig, saveGlobalConfig, withoutStoredApiKey } from './config.js';
15
15
  import { rememberKey, forgetAllKeys } from './credentials.js';
16
- import { readClipboard, cleanPastedSecret } from './clipboard.js';
16
+ import { ImageDraft, pastedImagePath } from './image-draft.js';
17
+ import { readClipboard, readClipboardImage, startSelectionCopy, cleanPastedSecret } from './clipboard.js';
17
18
  import { paceFrom } from './pace.js';
18
19
  import { policyFrom, FAILOVER_OFF } from './failover.js';
19
20
  import { listCheckpoints, revertCheckpoint, previewCheckpoint, snapshotTree, filesTouchedBy, createWorkspaceCheckpoint, checkpointScope } from './checkpoint.js';
@@ -500,7 +501,7 @@ const COMMANDS = [
500
501
  { cmd: '/queue', desc: 'Inspect or clear prompts waiting behind the current task' },
501
502
  { cmd: '/new', desc: 'New session (alias of /clear)' },
502
503
  { cmd: '/reset', desc: 'Reset session (alias of /clear)' },
503
- { cmd: '/mode', desc: 'Set mode: auto, plan or code' },
504
+ { cmd: '/mode', desc: 'Set mode: auto, edits, careful or plan' },
504
505
  { cmd: '/effort', desc: 'Set reasoning effort: low, medium, high, max (picker)' },
505
506
  { cmd: '/config', desc: 'View or set persistent config' },
506
507
  { cmd: '/usage', desc: 'Usage, context, model status and limits (tabbed pane)' },
@@ -1019,6 +1020,11 @@ export function inkApprovalDecision(requireApproval, mode, tool, argsJson, rules
1019
1020
  // silently erase. It remains stronger than this session's convenience posture.
1020
1021
  if (policyRequiresApproval)
1021
1022
  return 'ask';
1023
+ // Auto is the explicit promise to carry a task through. A globally saved approval preference
1024
+ // must not quietly turn it into careful mode; changing into Auto is the newer, more specific
1025
+ // choice. Workspace policies above still win, and plan mode was already denied by `decide`.
1026
+ if (modeSpec(mode).name === 'auto')
1027
+ return 'allow';
1022
1028
  if (!requireApproval)
1023
1029
  return 'allow';
1024
1030
  if (toolRisk(tool) === 'read')
@@ -1095,7 +1101,7 @@ Slash commands:
1095
1101
  /queue [clear|drop n] Inspect, clear, or drop one queued prompt
1096
1102
  /model [name] Show or switch model
1097
1103
  /provider [name] Show or switch provider
1098
- /mode [auto|plan|code] Show or set mode
1104
+ /mode [auto|edits|careful|plan] Show or set mode
1099
1105
  /effort [low|med|high|max] Set reasoning effort
1100
1106
  /config [key] [value] View or set persistent config
1101
1107
  /aside <question> Ask a side question without interrupting the running task
@@ -1486,6 +1492,7 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
1486
1492
  diffBaseRef.current.set(rel, before);
1487
1493
  }
1488
1494
  liveToolRef.current = { kind: 'tool', name, detail: toolDetail(name, args), startedAt: Date.now() };
1495
+ setRepaint(v => v + 1);
1489
1496
  }
1490
1497
  function finishTool(name, ok, ms, detail) {
1491
1498
  if (!ok)
@@ -1715,6 +1722,19 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
1715
1722
  const [showDiffPanel, setShowDiffPanel] = useState(false);
1716
1723
  const diffPanelTouchedRef = useRef(false);
1717
1724
  const [draft, setDraft] = useState('');
1725
+ const imageDraftRef = useRef(new ImageDraft());
1726
+ const [copyNotice, setCopyNotice] = useState('');
1727
+ useEffect(() => {
1728
+ if (!process.stdin.isTTY || cfg.autoCopy === false)
1729
+ return;
1730
+ return startSelectionCopy(count => setCopyNotice(`Copied ${count} chars to clipboard`));
1731
+ }, [cfg.autoCopy]);
1732
+ useEffect(() => {
1733
+ if (!copyNotice)
1734
+ return;
1735
+ const timer = setTimeout(() => setCopyNotice(''), 3000);
1736
+ return () => clearTimeout(timer);
1737
+ }, [copyNotice]);
1718
1738
  // Recall is deliberately session-local. It is for quick retries and refinements, not another
1719
1739
  // persistence channel for prompts (which may contain sensitive project context).
1720
1740
  const draftHistoryRef = useRef([]);
@@ -2315,9 +2335,6 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
2315
2335
  setAnim({ spin: 0, shimmer: 0, elapsed: 0 });
2316
2336
  return;
2317
2337
  }
2318
- // Turned off: no frames at all, so nothing repaints unless something actually changes.
2319
- if (!animate)
2320
- return;
2321
2338
  busyStart.current = Date.now();
2322
2339
  wordTimer.current = 0;
2323
2340
  // One rate, whether or not the stream is saying anything. The spinner earns its place
@@ -2336,11 +2353,11 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
2336
2353
  * how a Pi ends up lagging while the same build feels instant on a laptop. See frame-pace.ts.
2337
2354
  */
2338
2355
  const floor = floorFor(syncedFramesRef.current);
2339
- let interval = startingInterval(syncedFramesRef.current);
2356
+ let interval = animate ? startingInterval(syncedFramesRef.current) : 2_000;
2340
2357
  let asked = Date.now();
2341
2358
  const tick = () => {
2342
2359
  const now = Date.now();
2343
- interval = pacedInterval(interval, now - asked, floor);
2360
+ interval = animate ? pacedInterval(interval, now - asked, floor) : 2_000;
2344
2361
  /*
2345
2362
  * Nothing is repainted while somebody is typing.
2346
2363
  *
@@ -2364,10 +2381,10 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
2364
2381
  // Elapsed is read from the clock rather than counted in ticks, so a frame the event loop
2365
2382
  // delivers late moves the animation on by what actually passed instead of falling behind.
2366
2383
  const elapsed = now - busyStart.current;
2367
- const { spin, shimmer } = animFrames(elapsed, SPINNER.length);
2384
+ const { spin, shimmer } = animate ? animFrames(elapsed, SPINNER.length) : { spin: 0, shimmer: 0 };
2368
2385
  setAnim({ spin, shimmer, elapsed });
2369
2386
  wordTimer.current += 1;
2370
- if (wordTimer.current % wordEvery === 0)
2387
+ if (animate && wordTimer.current % wordEvery === 0)
2371
2388
  setSpinWord(w => w + 1);
2372
2389
  asked = Date.now();
2373
2390
  timer = setTimeout(tick, interval);
@@ -2759,6 +2776,12 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
2759
2776
  case 'mouse':
2760
2777
  setMouse(value === true);
2761
2778
  break;
2779
+ case 'animate':
2780
+ setAnimate(value !== false);
2781
+ break;
2782
+ case 'autoCopy':
2783
+ setCfg(c => ({ ...c, autoCopy: value !== false }));
2784
+ break;
2762
2785
  case 'provider': {
2763
2786
  const name = typeof value === 'string' && value.trim() !== '' ? value.trim() : cfg.provider;
2764
2787
  const def = PROVIDERS[name.toLowerCase()];
@@ -3286,7 +3309,11 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
3286
3309
  const choice = arg.trim().toLowerCase();
3287
3310
  if (!choice) {
3288
3311
  addSystem(`**Approval posture**\n\n` +
3289
- `Session approval: ${cfg.requireApproval ? 'on — KONECK asks before writes and non-read-only commands' : 'off — KONECK follows the selected mode and workspace policy'}\n` +
3312
+ `Session approval: ${mode === 'auto'
3313
+ ? 'Auto mode runs ordinary work without asking; workspace policy can still require approval'
3314
+ : cfg.requireApproval
3315
+ ? 'on — KONECK asks before writes and non-read-only commands'
3316
+ : 'off — KONECK follows the selected mode and workspace policy'}\n` +
3290
3317
  `Mode: ${modeSpec(mode).label} — ${modeSpec(mode).blurb}\n\n` +
3291
3318
  '`/permissions on` keeps you in control of every consequential action.\n' +
3292
3319
  '`/permissions off` removes the extra session gate; workspace policy and plan mode still win.');
@@ -3305,9 +3332,11 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
3305
3332
  // and the rebuilt session makes the new posture durable for every later turn.
3306
3333
  if (!busy)
3307
3334
  await resetSession(merged);
3308
- addSystem(next
3309
- ? 'Approval on. KONECK will ask in this interface before writes and consequential commands. Read-only inspection stays uninterrupted.'
3310
- : 'Approval off. KONECK follows the selected mode and workspace policy; plan mode and policy blocks still cannot be bypassed.');
3335
+ addSystem(mode === 'auto'
3336
+ ? `Approval ${next ? 'on' : 'off'} saved. Auto mode still runs ordinary work without asking; workspace policy remains enforceable.`
3337
+ : next
3338
+ ? 'Approval on. KONECK will ask in this interface before writes and consequential commands. Read-only inspection stays uninterrupted.'
3339
+ : 'Approval off. KONECK follows the selected mode and workspace policy; plan mode and policy blocks still cannot be bypassed.');
3311
3340
  return;
3312
3341
  }
3313
3342
  case '/worktree': {
@@ -3677,7 +3706,7 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
3677
3706
  ? 'It repaints a few times a second while a turn runs. On a terminal that does not '
3678
3707
  + 'composite a redraw that is visible as flicker. `/animation off` stops it — the '
3679
3708
  + 'elapsed time and the status bar still say what is happening.'
3680
- : 'Nothing repaints unless it changes. `/animation on` brings the spinner back.'));
3709
+ : 'The spinner is off. Activity and elapsed time refresh every two seconds while you are not typing. `/animation on` brings the spinner back.'));
3681
3710
  return;
3682
3711
  }
3683
3712
  const next = want === 'on';
@@ -3687,8 +3716,8 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
3687
3716
  setStored(saved);
3688
3717
  addSystem(next
3689
3718
  ? 'Working animation **on**.'
3690
- : 'Working animation **off** — the screen stays still. The status bar still shows '
3691
- + 'progress, and elapsed time updates when something happens.');
3719
+ : 'Working animation **off**. The status bar still shows '
3720
+ + 'progress and elapsed time every two seconds, pausing while you type.');
3692
3721
  return;
3693
3722
  }
3694
3723
  case '/redraw': {
@@ -3744,8 +3773,11 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
3744
3773
  case '/auto':
3745
3774
  case '/plan':
3746
3775
  case '/edits':
3776
+ case '/edit':
3777
+ case '/code':
3747
3778
  case '/careful': {
3748
- const target = cmd === '/mode' ? arg.trim() : cmd.slice(1);
3779
+ const requested = cmd === '/mode' ? arg.trim() : cmd.slice(1);
3780
+ const target = requested === 'edit' || requested === 'code' ? 'edits' : requested;
3749
3781
  const names = MODE_SPECS.map(m => m.name).join(', ');
3750
3782
  if (!target) {
3751
3783
  addSystem(`Current mode: ${modeSpec(mode).label} — ${modeSpec(mode).blurb}\n\n` +
@@ -3916,6 +3948,21 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
3916
3948
  }
3917
3949
  return;
3918
3950
  }
3951
+ const { readAll } = await import('./ledger.js');
3952
+ const notes = await readAll(cfg.cwd);
3953
+ // Older releases shortened an entire shell pipeline to its first two words. That turned
3954
+ // `cd project && slow-test` into a bogus claim that changing directory was slow. Keep the
3955
+ // old line on disk for auditability, but do not present it as knowledge or send it back to
3956
+ // a model.
3957
+ const usableNotes = notes.filter(n => !(n.kind === 'timing'
3958
+ && n.commands?.some(command => /^cd\s+[^;&|]+$/.test(command))));
3959
+ if (usableNotes.length) {
3960
+ addSystem('Saved project notes (.koneck/ledger.jsonl):\n'
3961
+ + usableNotes.slice(-20).map(n => '• ' + n.text).join('\n'));
3962
+ }
3963
+ if (usableNotes.length !== notes.length) {
3964
+ addSystem(`${notes.length - usableNotes.length} old timing note${notes.length - usableNotes.length === 1 ? '' : 's'} ignored: it measured only a directory change, not the command that followed it.`);
3965
+ }
3919
3966
  const mem = await loadMemory(cfg.cwd).catch(() => null);
3920
3967
  addSystem(mem?.trim()
3921
3968
  ? mem + '\n\n(/memory clear removes these notes)'
@@ -4337,6 +4384,10 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
4337
4384
  const n = Number(val);
4338
4385
  setConfigMaxWidth(Number.isFinite(n) && n >= 40 ? n : undefined);
4339
4386
  }
4387
+ if (key === 'autoCopy')
4388
+ setCfg(c => ({ ...c, autoCopy: ['true', '1', 'yes'].includes(val.toLowerCase()) }));
4389
+ if (key === 'animate')
4390
+ setAnimate(['true', '1', 'yes'].includes(val.toLowerCase()));
4340
4391
  if (key === 'mouse') {
4341
4392
  setMouse(['true', '1', 'yes'].includes(val.toLowerCase()));
4342
4393
  }
@@ -4667,6 +4718,13 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
4667
4718
  setKeyPrompt(p => (p ? { ...p, value: p.value + clean.replace(/\s+$/, '') } : p));
4668
4719
  return;
4669
4720
  }
4721
+ const imagePath = pastedImagePath(clean, cfg.cwd);
4722
+ if (imagePath) {
4723
+ const token = imageDraftRef.current.add(imagePath);
4724
+ setDraft(d => d.slice(0, caret) + token + d.slice(caret));
4725
+ setCaret(c => c + token.length);
4726
+ return;
4727
+ }
4670
4728
  if (!clean.includes('\n') && clean.length <= 200) {
4671
4729
  setDraft(d => d.slice(0, caret) + clean + d.slice(caret));
4672
4730
  setCaret(c => c + clean.length);
@@ -5109,6 +5167,21 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
5109
5167
  });
5110
5168
  return;
5111
5169
  }
5170
+ if (key.ctrl && input.toLowerCase() === 'v') {
5171
+ void (async () => {
5172
+ const image = await readClipboardImage();
5173
+ if (image.path) {
5174
+ filePaste(image.path);
5175
+ return;
5176
+ }
5177
+ const text = await readClipboard();
5178
+ if (text.ok && text.text)
5179
+ filePaste(text.text);
5180
+ else
5181
+ addSystem(image.reason || text.reason || 'The clipboard is empty.');
5182
+ })();
5183
+ return;
5184
+ }
5112
5185
  // Only submission waits for the agent. Editing stays live throughout, so a thought had
5113
5186
  // while it works can be typed as it arrives rather than held until the turn ends — and
5114
5187
  // cancelling with esc then hands the keyboard back to a composer that still has it.
@@ -5137,7 +5210,7 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
5137
5210
  }
5138
5211
  addSystem(`${cmd} changes the session, so it waits until this turn finishes.`);
5139
5212
  }
5140
- setQueued(q => [...q, expandPastes(text, pastesRef.current)]);
5213
+ setQueued(q => [...q, imageDraftRef.current.expand(expandPastes(text, pastesRef.current))]);
5141
5214
  setDraft('');
5142
5215
  setCaret(0);
5143
5216
  pastesRef.current = [];
@@ -5233,6 +5306,10 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
5233
5306
  const text = input.replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, '');
5234
5307
  if (text === '')
5235
5308
  return;
5309
+ if (text.length > 1 && pastedImagePath(text, cfg.cwd)) {
5310
+ filePaste(text);
5311
+ return;
5312
+ }
5236
5313
  const next = draft.slice(0, caret) + text + draft.slice(caret);
5237
5314
  const nextCaret = caret + text.length;
5238
5315
  // Once the recalled line is edited it is a new draft, not a history cursor position.
@@ -5277,7 +5354,7 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
5277
5354
  const shown = raw.trim();
5278
5355
  if (!shown)
5279
5356
  return;
5280
- const task = expandPastes(shown, pastesRef.current).trim();
5357
+ const task = imageDraftRef.current.expand(expandPastes(shown, pastesRef.current)).trim();
5281
5358
  if (!task)
5282
5359
  return;
5283
5360
  const generated = generatedTaskRef.current?.task === task ? generatedTaskRef.current : null;
@@ -5307,7 +5384,7 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
5307
5384
  'written into the session log.');
5308
5385
  }
5309
5386
  const mentions = findMentions(task, pathKind);
5310
- const forDisplay = taskTranscriptLabel(pasteForDisplay(mentions.length ? shortenMentions(task, mentions) : task), generated?.label);
5387
+ const forDisplay = taskTranscriptLabel(shown.includes('[Image #') ? shown : pasteForDisplay(mentions.length ? shortenMentions(task, mentions) : task), generated?.label);
5311
5388
  addRow({ role: 'user', text: forDisplay });
5312
5389
  if (isSlashCommand(task)) {
5313
5390
  const spaceIdx = task.indexOf(' ');
@@ -5498,11 +5575,14 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
5498
5575
  * Only on the alternate screen: in inline mode the transcript is committed to the terminal's own
5499
5576
  * scrollback full-width, and there is no frame to put a second column in.
5500
5577
  */
5578
+ const activityPanelOpen = busy && uiWidth >= 100 && termRows >= 24
5579
+ && (diffPanelTouchedRef.current ? showDiffPanel : cfg.diffPanel !== false);
5501
5580
  const diffPanelOpen = showDiffPanel && altScreen && liveDiffs.length > 0;
5581
+ const sidePanelOpen = activityPanelOpen || diffPanelOpen;
5502
5582
  // Read by renderStep, which is defined above this line, so it goes through a ref rather than
5503
5583
  // depending on declaration order.
5504
5584
  diffPanelOpenRef.current = diffPanelOpen;
5505
- const diffPanelWidth = diffPanelOpen
5585
+ const diffPanelWidth = sidePanelOpen
5506
5586
  ? Math.max(34, Math.min(Math.floor(uiWidth * 0.46), 96))
5507
5587
  : 0;
5508
5588
  const contentWidth = uiWidth - diffPanelWidth;
@@ -5625,7 +5705,7 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
5625
5705
  // Both halves matter. Clipping the bottom would hide what just happened; anchoring
5626
5706
  // always to the bottom is what put the banner there.
5627
5707
  _jsx(Box, { flexGrow: 0, flexShrink: 1, flexDirection: "column", justifyContent: "flex-end", overflowY: "hidden", children: visibleRows.map((row, i) => (_jsx(Box, { flexDirection: "column", flexShrink: 0, children: renderRow(row, i) }, i))) })), altScreen && scrollBack > 0 && (_jsx(Text, { color: AMBER, children: `${G.caret} scrolled back ${scrollBack} rows — PageDown, or just type, to follow again` })), 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: altScreen ? [] : rows, children: (row, index) => renderRow(row, index) }), _jsx(Box, { flexDirection: "column", flexShrink: 0, children: busy && (agentState === 'processing' || agentState === 'syncing') && (_jsxs(Box, { flexDirection: "column", children: [!ask && sayRef.current.trim() !== '' && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: CYAN, bold: true, children: replyStartedRef.current ? ' ' : G.brand }), _jsx(Box, { flexDirection: "column", children: tailLines(sayRef.current, liveReplyLines(termRows), replyWidth - 3)
5628
- .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 /aside 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 && (() => {
5708
+ .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 /aside 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)))] })), !activityPanelOpen && agents.length > 0 && (() => {
5629
5709
  const done = agents.filter(a => a.status !== 'running').length;
5630
5710
  const failed = agents.filter(a => a.status === 'failed').length;
5631
5711
  const tokens = agents.reduce((n, a) => n + a.tokens, 0);
@@ -5642,7 +5722,7 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
5642
5722
  const task = a.task.length > taskWidth ? a.task.slice(0, taskWidth - 3) + '...' : a.task;
5643
5723
  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));
5644
5724
  })] }));
5645
- })(), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: [_jsxs(Text, { color: GREEN, children: [SPINNER[spinFrame], " "] }), _jsx(Shimmer, { text: workWord({
5725
+ })(), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: [_jsxs(Text, { color: GREEN, children: [animate ? SPINNER[spinFrame] : '●', " "] }), _jsx(Shimmer, { text: workWord({
5646
5726
  elapsedMs, tools: turnToolsRef.current,
5647
5727
  tokens: liveTokens, recovered: turnRecoveredRef.current,
5648
5728
  }, spinWord)[0], frame: shimmerFrame, base: MUTED }), _jsx(Text, { color: MUTED, children: "\u2026 " }), _jsxs(Text, { color: DIM, children: ["(", fmtElapsed(elapsedMs), " | ", liveTokens > 0
@@ -5650,9 +5730,10 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
5650
5730
  // A gateway routing to several backends sends content-free frames while it
5651
5731
  // finds one. Saying "waiting for the first token" through that reads as a
5652
5732
  // hang, when in fact the request was accepted and is being worked on.
5653
- : providerFramesRef.current > 0 ? `${cfg.provider} is holding the line, no output yet`
5654
- : elapsedMs > 8_000 ? `no response from ${cfg.provider} yet`
5655
- : 'starting', thinkingRef.current.chars > 0 && Date.now() - thinkingRef.current.at < 2_000
5733
+ : liveToolRef.current ? 'running ' + liveToolRef.current.name
5734
+ : providerFramesRef.current > 0 ? `${cfg.provider} is holding the line, no output yet`
5735
+ : elapsedMs > 8_000 ? `no response from ${cfg.provider} yet`
5736
+ : 'starting', thinkingRef.current.chars > 0 && Date.now() - thinkingRef.current.at < 2_000
5656
5737
  ? ` | thinking, ${fmtTokens(Math.round(thinkingRef.current.chars / 4))} reasoning tokens`
5657
5738
  : '', ")"] })] }) }), showThinking && !ask && thinkingTextRef.current.trim() !== '' && (_jsx(Box, { flexDirection: "column", paddingLeft: 2, children: thinkingTail(thinkingTextRef.current, THINKING_LINES, barWidth - 6).map((line, i) => (_jsx(Text, { color: DIM, italic: true, children: line }, i))) })), _jsx(Box, { paddingLeft: 2, children: _jsx(Text, { color: DIM, children: "esc to cancel and keep your input \u00B7 ctrl+c to exit" }) })] })) }), usagePane && (() => {
5658
5739
  const width = Math.min(barWidth, usagePane === 'settings' ? 96 : 66);
@@ -5972,11 +6053,13 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
5972
6053
  : '*type the number, or arrows and enter · esc refuses*',
5973
6054
  ];
5974
6055
  return (_jsx(Box, { marginTop: 1, children: _jsx(Panel, { width: panelWidth(lines, barWidth, 46), color: AMBER, title: `Permission — ${modeSpec(mode).label} mode`, children: lines }) }));
5975
- })()] }), diffPanelOpen && (_jsx(DiffPanel, { files: liveDiffs, width: diffPanelWidth, height: Math.max(4, frameRows - 6) }))] }), _jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: busy ? DIM : CYAN, paddingX: 1, width: barWidth, flexShrink: 0, children: [_jsxs(Text, { color: CYAN, bold: true, children: [G.caret, " "] }), _jsx(Text, { color: INK, children: draft.slice(0, caret) }), busy
6056
+ })()] }), sidePanelOpen && (_jsxs(Box, { flexDirection: "column", width: diffPanelWidth, flexShrink: 0, children: [activityPanelOpen && (_jsxs(Box, { borderStyle: "round", borderColor: CYAN, flexDirection: "column", paddingX: 1, height: Math.min(18, Math.max(8, termRows - 7)), overflowY: "hidden", children: [_jsxs(Text, { color: CYAN, bold: true, children: ["Activity \u00B7 ", fmtElapsed(elapsedMs)] }), _jsx(Text, { color: AMBER, wrap: "truncate", children: ask ? 'Waiting for your approval' : liveToolRef.current
6057
+ ? 'Running ' + liveToolRef.current.name : thinkingRef.current.chars > 0
6058
+ ? 'Waiting for model · reasoning received' : 'Waiting for model' }), liveToolRef.current && _jsx(Text, { color: INK, wrap: "truncate", children: liveToolRef.current.detail }), liveToolRef.current && _jsxs(Text, { color: DIM, children: ["Running for ", fmtElapsed(Date.now() - liveToolRef.current.startedAt)] }), _jsx(Text, { color: MUTED, wrap: "truncate", children: liveToolRef.current?.output || 'Status updates every 2s; esc to interrupt' }), plan && _jsx(Text, { color: MUTED, wrap: "truncate", children: plan.steps.find(s => s.state !== 'done')?.text || 'Plan complete' }), agents.length > 0 && _jsxs(Text, { color: CYAN, bold: true, children: ["Agents \u00B7 ", agents.filter(a => a.status !== 'running').length, "/", agents.length, " finished"] }), agents.map(a => _jsx(Box, { flexDirection: "column", flexShrink: 0, children: _jsxs(Text, { color: a.status === 'failed' ? CRIMSON : a.status === 'done' ? GREEN : AMBER, wrap: "truncate", children: [a.index + 1, ". ", a.status === 'running' ? (a.activity === 'Queued' ? 'queued' : 'working') : a.status, " \u00B7 ", a.activity, " \u00B7 ", a.task] }) }, a.index))] })), diffPanelOpen && _jsx(DiffPanel, { files: liveDiffs, width: diffPanelWidth, height: Math.max(4, frameRows - (activityPanelOpen ? 24 : 6)) })] }))] }), _jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: busy ? DIM : CYAN, paddingX: 1, width: uiWidth - 5, flexShrink: 0, children: [_jsxs(Text, { color: CYAN, bold: true, children: [G.caret, " "] }), _jsx(Text, { color: INK, children: draft.slice(0, caret) }), busy
5976
6059
  ? _jsx(Text, { color: INK, children: draft.slice(caret) })
5977
6060
  : caret < draft.length
5978
6061
  ? _jsxs(_Fragment, { children: [_jsx(Text, { backgroundColor: CYAN, color: "#10222A", children: draft[caret] }), _jsx(Text, { color: INK, children: draft.slice(caret + 1) })] })
5979
- : _jsx(Text, { color: CYAN, children: "\u2588" })] }), _jsx(Box, { marginTop: 1, paddingX: 1, flexShrink: 0, children: _jsxs(Text, { wrap: "truncate", backgroundColor: "#24343B", children: [repaint % 2 === 1 ? '\u200b' : '', _jsx(Text, { color: MUTED, children: "TAB " }), _jsx(Text, { color: INK, children: "Analytics" }), _jsx(Text, { color: MUTED, children: " / " }), _jsx(Text, { color: CYAN, children: "/HELP" }), _jsx(Text, { color: INK, children: " Commands" }), _jsx(Text, { color: MUTED, children: " / SHIFT+TAB " }), _jsx(Text, { color: INK, children: "Mode" }), _jsx(Text, { color: MUTED, children: " / " }), _jsx(Text, { color: statusColor, children: statusLabel.toLowerCase() }), plan && planSummary(plan) && (_jsxs(_Fragment, { children: [_jsx(Text, { color: MUTED, children: " / PLAN: " }), _jsx(Text, { color: CYAN, children: planSummary(plan) })] })), _jsx(Text, { color: MUTED, children: " / MODE: " }), _jsx(Text, { color: mode === 'auto' ? INK : AMBER, children: modeSpec(mode).label }), _jsx(Text, { color: MUTED, children: " / EFFORT: " }), _jsx(Text, { color: effort === 'medium' ? INK : AMBER, children: effort }), mouse && (_jsxs(_Fragment, { children: [_jsx(Text, { color: MUTED, children: " / " }), _jsx(Text, { color: AMBER, children: "WHEEL: on \u2014 shift to select \u00B7 /mouse off" })] })), _jsx(Text, { color: MUTED, children: " / MODEL: " }), _jsx(Text, { color: INK, children: modelShort }), _jsx(Text, { color: MUTED, children: " / v" }), _jsx(Text, { color: updatePane?.behind ? AMBER : INK, children: VERSION }), updatePane?.behind && _jsx(Text, { color: AMBER, children: " \u2191" }), pasteCount > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { color: MUTED, children: " / " }), _jsxs(Text, { color: AMBER, children: [pasteCount, " paste", pasteCount === 1 ? '' : 's', " held, sent in full"] })] }))] }) })] }));
6062
+ : _jsx(Text, { color: CYAN, children: "\u2588" })] }), copyNotice && _jsx(Text, { color: CYAN, children: copyNotice }), _jsx(Box, { marginTop: 1, paddingX: 1, flexShrink: 0, children: _jsxs(Text, { wrap: "truncate", backgroundColor: "#24343B", children: [repaint % 2 === 1 ? '\u200b' : '', _jsx(Text, { color: MUTED, children: "TAB " }), _jsx(Text, { color: INK, children: "Analytics" }), _jsx(Text, { color: MUTED, children: " / " }), _jsx(Text, { color: CYAN, children: "/HELP" }), _jsx(Text, { color: INK, children: " Commands" }), _jsx(Text, { color: MUTED, children: " / SHIFT+TAB " }), _jsx(Text, { color: INK, children: "Mode" }), _jsx(Text, { color: MUTED, children: " / " }), _jsx(Text, { color: statusColor, children: statusLabel.toLowerCase() }), plan && planSummary(plan) && (_jsxs(_Fragment, { children: [_jsx(Text, { color: MUTED, children: " / PLAN: " }), _jsx(Text, { color: CYAN, children: planSummary(plan) })] })), _jsx(Text, { color: MUTED, children: " / MODE: " }), _jsx(Text, { color: mode === 'auto' ? INK : AMBER, children: modeSpec(mode).label }), _jsx(Text, { color: MUTED, children: " / EFFORT: " }), _jsx(Text, { color: effort === 'medium' ? INK : AMBER, children: effort }), mouse && (_jsxs(_Fragment, { children: [_jsx(Text, { color: MUTED, children: " / " }), _jsx(Text, { color: AMBER, children: "WHEEL: on \u2014 shift to select \u00B7 /mouse off" })] })), _jsx(Text, { color: MUTED, children: " / MODEL: " }), _jsx(Text, { color: INK, children: modelShort }), _jsx(Text, { color: MUTED, children: " / v" }), _jsx(Text, { color: updatePane?.behind ? AMBER : INK, children: VERSION }), updatePane?.behind && _jsx(Text, { color: AMBER, children: " \u2191" }), pasteCount > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { color: MUTED, children: " / " }), _jsxs(Text, { color: AMBER, children: [pasteCount, " paste", pasteCount === 1 ? '' : 's', " held, sent in full"] })] }))] }) })] }));
5980
6063
  }
5981
6064
  export async function runInkChatMode(config) {
5982
6065
  // Nothing is written before the first frame. Filling the screen to push the prompt to the foot