koneck 2.127.1 → 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
@@ -4,7 +4,7 @@ import { Box, Static, Text, render, useApp, useInput, useStdin } from 'ink';
4
4
  import { execa } from 'execa';
5
5
  import { resolveAgentConcurrency, toolsUnsupportedNotice } from './engine-facts.js';
6
6
  import { withSynchronizedFrames, probeSynchronizedOutput } from './frame-sync.js';
7
- import { pacedInterval, startingInterval, floorFor } from './frame-pace.js';
7
+ import { pacedInterval, startingInterval, floorFor, shouldDrawFrame } from './frame-pace.js';
8
8
  import { estimateCost, formatCost, rateText } from './pricing.js';
9
9
  import { generateSessionId, saveSession, listSessions, loadSession, relativeAge, renameSession, forkSession, archiveSession, findSession, } from './session.js';
10
10
  import { loadMemory } from './memory.js';
@@ -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([]);
@@ -1804,6 +1824,17 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
1804
1824
  * So: the wheel is the thing you opt into, not the thing you lose.
1805
1825
  */
1806
1826
  const [mouse, setMouse] = useState(cfg.mouse === true);
1827
+ /*
1828
+ * Whether the working animation runs at all.
1829
+ *
1830
+ * On, because a turn that produces nothing for two minutes needs something saying it is alive.
1831
+ * Off is here because on a terminal that does not composite a redraw the animation is the only
1832
+ * thing repainting when nobody is typing, and somebody who would rather have a still screen than
1833
+ * a moving one should be able to say so once and be done — "i fucken hate the flickering with all
1834
+ * my heart". Nothing is lost but the movement: the elapsed time and the status bar still say what
1835
+ * is happening, they simply say it without flashing.
1836
+ */
1837
+ const [animate, setAnimate] = useState(cfg.animate !== false);
1807
1838
  useEffect(() => {
1808
1839
  if (!mouse || !altScreen || !process.stdout.isTTY)
1809
1840
  return;
@@ -2241,6 +2272,8 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
2241
2272
  const syncedFramesRef = useRef(synchronizedFrames);
2242
2273
  /** Whether the diff panel is showing, so the transcript does not draw the same diff again. */
2243
2274
  const diffPanelOpenRef = useRef(false);
2275
+ /** When a key was last pressed. The animation stands down while somebody is typing. */
2276
+ const lastKeyAtRef = useRef(0);
2244
2277
  const wordTimer = useRef(0);
2245
2278
  // Register on the session bus so other running KONECK instances can reach this one.
2246
2279
  useEffect(() => {
@@ -2320,25 +2353,45 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
2320
2353
  * how a Pi ends up lagging while the same build feels instant on a laptop. See frame-pace.ts.
2321
2354
  */
2322
2355
  const floor = floorFor(syncedFramesRef.current);
2323
- let interval = startingInterval(syncedFramesRef.current);
2356
+ let interval = animate ? startingInterval(syncedFramesRef.current) : 2_000;
2324
2357
  let asked = Date.now();
2325
2358
  const tick = () => {
2326
2359
  const now = Date.now();
2327
- interval = pacedInterval(interval, now - asked, floor);
2360
+ interval = animate ? pacedInterval(interval, now - asked, floor) : 2_000;
2361
+ /*
2362
+ * Nothing is repainted while somebody is typing.
2363
+ *
2364
+ * Every frame erases the live region and writes it again — the activity lines, the composer
2365
+ * and the status bar together. Where the terminal does not composite that, each one is a
2366
+ * visible flash, and it lands on top of the repaint the keystroke itself already caused. So
2367
+ * typing into the box while a turn ran flickered continuously: "when i type in the message
2368
+ * box, it's flickering. Please fix it, i fucken hate the flickering."
2369
+ *
2370
+ * The animation exists to show that work is still happening when nothing else does. Somebody
2371
+ * with their hands on the keys does not need telling, and the status bar says "processing"
2372
+ * without moving. So the clock keeps running and the frame is simply not drawn: the spinner
2373
+ * picks up from where the elapsed time has reached the moment they stop, having lost nothing
2374
+ * but the frames nobody wanted.
2375
+ */
2376
+ if (!shouldDrawFrame(now, lastKeyAtRef.current)) {
2377
+ asked = Date.now();
2378
+ timer = setTimeout(tick, interval);
2379
+ return;
2380
+ }
2328
2381
  // Elapsed is read from the clock rather than counted in ticks, so a frame the event loop
2329
2382
  // delivers late moves the animation on by what actually passed instead of falling behind.
2330
2383
  const elapsed = now - busyStart.current;
2331
- const { spin, shimmer } = animFrames(elapsed, SPINNER.length);
2384
+ const { spin, shimmer } = animate ? animFrames(elapsed, SPINNER.length) : { spin: 0, shimmer: 0 };
2332
2385
  setAnim({ spin, shimmer, elapsed });
2333
2386
  wordTimer.current += 1;
2334
- if (wordTimer.current % wordEvery === 0)
2387
+ if (animate && wordTimer.current % wordEvery === 0)
2335
2388
  setSpinWord(w => w + 1);
2336
2389
  asked = Date.now();
2337
2390
  timer = setTimeout(tick, interval);
2338
2391
  };
2339
2392
  timer = setTimeout(tick, interval);
2340
2393
  return () => clearTimeout(timer);
2341
- }, [busy]);
2394
+ }, [busy, animate]);
2342
2395
  // A queued prompt runs the moment the agent is free again. Keyed on `busy` rather than done
2343
2396
  // inside the turn's own exit paths, so every way a turn can end — success, failure, cancel —
2344
2397
  // drains the queue the same way.
@@ -2723,6 +2776,12 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
2723
2776
  case 'mouse':
2724
2777
  setMouse(value === true);
2725
2778
  break;
2779
+ case 'animate':
2780
+ setAnimate(value !== false);
2781
+ break;
2782
+ case 'autoCopy':
2783
+ setCfg(c => ({ ...c, autoCopy: value !== false }));
2784
+ break;
2726
2785
  case 'provider': {
2727
2786
  const name = typeof value === 'string' && value.trim() !== '' ? value.trim() : cfg.provider;
2728
2787
  const def = PROVIDERS[name.toLowerCase()];
@@ -3250,7 +3309,11 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
3250
3309
  const choice = arg.trim().toLowerCase();
3251
3310
  if (!choice) {
3252
3311
  addSystem(`**Approval posture**\n\n` +
3253
- `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` +
3254
3317
  `Mode: ${modeSpec(mode).label} — ${modeSpec(mode).blurb}\n\n` +
3255
3318
  '`/permissions on` keeps you in control of every consequential action.\n' +
3256
3319
  '`/permissions off` removes the extra session gate; workspace policy and plan mode still win.');
@@ -3269,9 +3332,11 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
3269
3332
  // and the rebuilt session makes the new posture durable for every later turn.
3270
3333
  if (!busy)
3271
3334
  await resetSession(merged);
3272
- addSystem(next
3273
- ? 'Approval on. KONECK will ask in this interface before writes and consequential commands. Read-only inspection stays uninterrupted.'
3274
- : '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.');
3275
3340
  return;
3276
3341
  }
3277
3342
  case '/worktree': {
@@ -3622,6 +3687,39 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
3622
3687
  'PageUp and PageDown still scroll.');
3623
3688
  return;
3624
3689
  }
3690
+ /*
3691
+ * The working animation, on or off.
3692
+ *
3693
+ * Off leaves a still screen: on a terminal that does not composite a redraw, the animation is
3694
+ * the only thing repainting while nobody types, and a still screen is what some people want.
3695
+ */
3696
+ case '/animation':
3697
+ case '/animate': {
3698
+ const want = arg.trim().toLowerCase();
3699
+ if (want !== '' && want !== 'on' && want !== 'off') {
3700
+ addSystem('Usage: /animation on, /animation off, or /animation to see which it is.');
3701
+ return;
3702
+ }
3703
+ if (want === '') {
3704
+ addSystem(`The working animation is **${animate ? 'on' : 'off'}**.\n\n`
3705
+ + (animate
3706
+ ? 'It repaints a few times a second while a turn runs. On a terminal that does not '
3707
+ + 'composite a redraw that is visible as flicker. `/animation off` stops it — the '
3708
+ + 'elapsed time and the status bar still say what is happening.'
3709
+ : 'The spinner is off. Activity and elapsed time refresh every two seconds while you are not typing. `/animation on` brings the spinner back.'));
3710
+ return;
3711
+ }
3712
+ const next = want === 'on';
3713
+ setAnimate(next);
3714
+ const saved = { ...(await loadKoneckConfig()), animate: next };
3715
+ await saveKoneckConfig(saved);
3716
+ setStored(saved);
3717
+ addSystem(next
3718
+ ? 'Working animation **on**.'
3719
+ : 'Working animation **off**. The status bar still shows '
3720
+ + 'progress and elapsed time every two seconds, pausing while you type.');
3721
+ return;
3722
+ }
3625
3723
  case '/redraw': {
3626
3724
  redrawScreen();
3627
3725
  return;
@@ -3675,8 +3773,11 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
3675
3773
  case '/auto':
3676
3774
  case '/plan':
3677
3775
  case '/edits':
3776
+ case '/edit':
3777
+ case '/code':
3678
3778
  case '/careful': {
3679
- 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;
3680
3781
  const names = MODE_SPECS.map(m => m.name).join(', ');
3681
3782
  if (!target) {
3682
3783
  addSystem(`Current mode: ${modeSpec(mode).label} — ${modeSpec(mode).blurb}\n\n` +
@@ -3847,6 +3948,21 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
3847
3948
  }
3848
3949
  return;
3849
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
+ }
3850
3966
  const mem = await loadMemory(cfg.cwd).catch(() => null);
3851
3967
  addSystem(mem?.trim()
3852
3968
  ? mem + '\n\n(/memory clear removes these notes)'
@@ -4268,6 +4384,10 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
4268
4384
  const n = Number(val);
4269
4385
  setConfigMaxWidth(Number.isFinite(n) && n >= 40 ? n : undefined);
4270
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()));
4271
4391
  if (key === 'mouse') {
4272
4392
  setMouse(['true', '1', 'yes'].includes(val.toLowerCase()));
4273
4393
  }
@@ -4598,6 +4718,13 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
4598
4718
  setKeyPrompt(p => (p ? { ...p, value: p.value + clean.replace(/\s+$/, '') } : p));
4599
4719
  return;
4600
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
+ }
4601
4728
  if (!clean.includes('\n') && clean.length <= 200) {
4602
4729
  setDraft(d => d.slice(0, caret) + clean + d.slice(caret));
4603
4730
  setCaret(c => c + clean.length);
@@ -4610,6 +4737,13 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
4610
4737
  setCaret(c => c + token.length);
4611
4738
  }
4612
4739
  useInput((input, key) => {
4740
+ /*
4741
+ * When a key was last pressed, so the animation can get out of the way.
4742
+ *
4743
+ * Set before anything else, on every key including the ones swallowed below: what matters is
4744
+ * that somebody is at the keyboard, not what they typed. See the animation ticker.
4745
+ */
4746
+ lastKeyAtRef.current = Date.now();
4613
4747
  // ── Mouse reports are not typing ────────────────────────────────────────
4614
4748
  //
4615
4749
  // Ink hands any escape sequence it does not recognise to the application as `input`, and a
@@ -5033,6 +5167,21 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
5033
5167
  });
5034
5168
  return;
5035
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
+ }
5036
5185
  // Only submission waits for the agent. Editing stays live throughout, so a thought had
5037
5186
  // while it works can be typed as it arrives rather than held until the turn ends — and
5038
5187
  // cancelling with esc then hands the keyboard back to a composer that still has it.
@@ -5061,7 +5210,7 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
5061
5210
  }
5062
5211
  addSystem(`${cmd} changes the session, so it waits until this turn finishes.`);
5063
5212
  }
5064
- setQueued(q => [...q, expandPastes(text, pastesRef.current)]);
5213
+ setQueued(q => [...q, imageDraftRef.current.expand(expandPastes(text, pastesRef.current))]);
5065
5214
  setDraft('');
5066
5215
  setCaret(0);
5067
5216
  pastesRef.current = [];
@@ -5157,6 +5306,10 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
5157
5306
  const text = input.replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, '');
5158
5307
  if (text === '')
5159
5308
  return;
5309
+ if (text.length > 1 && pastedImagePath(text, cfg.cwd)) {
5310
+ filePaste(text);
5311
+ return;
5312
+ }
5160
5313
  const next = draft.slice(0, caret) + text + draft.slice(caret);
5161
5314
  const nextCaret = caret + text.length;
5162
5315
  // Once the recalled line is edited it is a new draft, not a history cursor position.
@@ -5201,7 +5354,7 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
5201
5354
  const shown = raw.trim();
5202
5355
  if (!shown)
5203
5356
  return;
5204
- const task = expandPastes(shown, pastesRef.current).trim();
5357
+ const task = imageDraftRef.current.expand(expandPastes(shown, pastesRef.current)).trim();
5205
5358
  if (!task)
5206
5359
  return;
5207
5360
  const generated = generatedTaskRef.current?.task === task ? generatedTaskRef.current : null;
@@ -5231,7 +5384,7 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
5231
5384
  'written into the session log.');
5232
5385
  }
5233
5386
  const mentions = findMentions(task, pathKind);
5234
- 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);
5235
5388
  addRow({ role: 'user', text: forDisplay });
5236
5389
  if (isSlashCommand(task)) {
5237
5390
  const spaceIdx = task.indexOf(' ');
@@ -5422,11 +5575,14 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
5422
5575
  * Only on the alternate screen: in inline mode the transcript is committed to the terminal's own
5423
5576
  * scrollback full-width, and there is no frame to put a second column in.
5424
5577
  */
5578
+ const activityPanelOpen = busy && uiWidth >= 100 && termRows >= 24
5579
+ && (diffPanelTouchedRef.current ? showDiffPanel : cfg.diffPanel !== false);
5425
5580
  const diffPanelOpen = showDiffPanel && altScreen && liveDiffs.length > 0;
5581
+ const sidePanelOpen = activityPanelOpen || diffPanelOpen;
5426
5582
  // Read by renderStep, which is defined above this line, so it goes through a ref rather than
5427
5583
  // depending on declaration order.
5428
5584
  diffPanelOpenRef.current = diffPanelOpen;
5429
- const diffPanelWidth = diffPanelOpen
5585
+ const diffPanelWidth = sidePanelOpen
5430
5586
  ? Math.max(34, Math.min(Math.floor(uiWidth * 0.46), 96))
5431
5587
  : 0;
5432
5588
  const contentWidth = uiWidth - diffPanelWidth;
@@ -5549,7 +5705,7 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
5549
5705
  // Both halves matter. Clipping the bottom would hide what just happened; anchoring
5550
5706
  // always to the bottom is what put the banner there.
5551
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)
5552
- .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 && (() => {
5553
5709
  const done = agents.filter(a => a.status !== 'running').length;
5554
5710
  const failed = agents.filter(a => a.status === 'failed').length;
5555
5711
  const tokens = agents.reduce((n, a) => n + a.tokens, 0);
@@ -5566,7 +5722,7 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
5566
5722
  const task = a.task.length > taskWidth ? a.task.slice(0, taskWidth - 3) + '...' : a.task;
5567
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));
5568
5724
  })] }));
5569
- })(), _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({
5570
5726
  elapsedMs, tools: turnToolsRef.current,
5571
5727
  tokens: liveTokens, recovered: turnRecoveredRef.current,
5572
5728
  }, spinWord)[0], frame: shimmerFrame, base: MUTED }), _jsx(Text, { color: MUTED, children: "\u2026 " }), _jsxs(Text, { color: DIM, children: ["(", fmtElapsed(elapsedMs), " | ", liveTokens > 0
@@ -5574,9 +5730,10 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
5574
5730
  // A gateway routing to several backends sends content-free frames while it
5575
5731
  // finds one. Saying "waiting for the first token" through that reads as a
5576
5732
  // hang, when in fact the request was accepted and is being worked on.
5577
- : providerFramesRef.current > 0 ? `${cfg.provider} is holding the line, no output yet`
5578
- : elapsedMs > 8_000 ? `no response from ${cfg.provider} yet`
5579
- : '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
5580
5737
  ? ` | thinking, ${fmtTokens(Math.round(thinkingRef.current.chars / 4))} reasoning tokens`
5581
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 && (() => {
5582
5739
  const width = Math.min(barWidth, usagePane === 'settings' ? 96 : 66);
@@ -5896,11 +6053,13 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
5896
6053
  : '*type the number, or arrows and enter · esc refuses*',
5897
6054
  ];
5898
6055
  return (_jsx(Box, { marginTop: 1, children: _jsx(Panel, { width: panelWidth(lines, barWidth, 46), color: AMBER, title: `Permission — ${modeSpec(mode).label} mode`, children: lines }) }));
5899
- })()] }), 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
5900
6059
  ? _jsx(Text, { color: INK, children: draft.slice(caret) })
5901
6060
  : caret < draft.length
5902
6061
  ? _jsxs(_Fragment, { children: [_jsx(Text, { backgroundColor: CYAN, color: "#10222A", children: draft[caret] }), _jsx(Text, { color: INK, children: draft.slice(caret + 1) })] })
5903
- : _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"] })] }))] }) })] }));
5904
6063
  }
5905
6064
  export async function runInkChatMode(config) {
5906
6065
  // Nothing is written before the first frame. Filling the screen to push the prompt to the foot