koneck 2.26.0 → 2.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/dist/doctor.d.ts +27 -0
  2. package/dist/doctor.d.ts.map +1 -1
  3. package/dist/doctor.js +70 -9
  4. package/dist/doctor.js.map +1 -1
  5. package/dist/engine.d.ts.map +1 -1
  6. package/dist/engine.js +4 -1
  7. package/dist/engine.js.map +1 -1
  8. package/dist/ink-chat.d.ts +12 -3
  9. package/dist/ink-chat.d.ts.map +1 -1
  10. package/dist/ink-chat.js +246 -30
  11. package/dist/ink-chat.js.map +1 -1
  12. package/dist/quality-gate.d.ts +5 -1
  13. package/dist/quality-gate.d.ts.map +1 -1
  14. package/dist/quality-gate.js +8 -6
  15. package/dist/quality-gate.js.map +1 -1
  16. package/dist/session.d.ts +10 -1
  17. package/dist/session.d.ts.map +1 -1
  18. package/dist/session.js +12 -1
  19. package/dist/session.js.map +1 -1
  20. package/dist/web/api.d.ts.map +1 -1
  21. package/dist/web/api.js +122 -27
  22. package/dist/web/api.js.map +1 -1
  23. package/dist/web/changes.d.ts +55 -0
  24. package/dist/web/changes.d.ts.map +1 -0
  25. package/dist/web/changes.js +215 -0
  26. package/dist/web/changes.js.map +1 -0
  27. package/dist/web/files.d.ts +43 -0
  28. package/dist/web/files.d.ts.map +1 -0
  29. package/dist/web/files.js +131 -0
  30. package/dist/web/files.js.map +1 -0
  31. package/dist/web/history.d.ts +61 -0
  32. package/dist/web/history.d.ts.map +1 -0
  33. package/dist/web/history.js +197 -0
  34. package/dist/web/history.js.map +1 -0
  35. package/dist/web/mark.d.ts +15 -0
  36. package/dist/web/mark.d.ts.map +1 -0
  37. package/dist/web/mark.js +15 -0
  38. package/dist/web/mark.js.map +1 -0
  39. package/dist/web/search.d.ts +45 -0
  40. package/dist/web/search.d.ts.map +1 -0
  41. package/dist/web/search.js +147 -0
  42. package/dist/web/search.js.map +1 -0
  43. package/dist/web/sessions.d.ts +8 -0
  44. package/dist/web/sessions.d.ts.map +1 -1
  45. package/dist/web/sessions.js +9 -0
  46. package/dist/web/sessions.js.map +1 -1
  47. package/dist/web/ui-client.d.ts +14 -0
  48. package/dist/web/ui-client.d.ts.map +1 -0
  49. package/dist/web/ui-client.js +0 -0
  50. package/dist/web/ui-client.js.map +1 -0
  51. package/dist/web/ui-css.d.ts +15 -0
  52. package/dist/web/ui-css.d.ts.map +1 -0
  53. package/dist/web/ui-css.js +329 -0
  54. package/dist/web/ui-css.js.map +1 -0
  55. package/dist/web/ui.d.ts +6 -9
  56. package/dist/web/ui.d.ts.map +1 -1
  57. package/dist/web/ui.js +100 -541
  58. package/dist/web/ui.js.map +1 -1
  59. package/dist/workspace-trust.d.ts +8 -0
  60. package/dist/workspace-trust.d.ts.map +1 -1
  61. package/dist/workspace-trust.js +11 -0
  62. package/dist/workspace-trust.js.map +1 -1
  63. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"ink-chat.d.ts","sourceRoot":"","sources":["../src/ink-chat.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ink-chat.d.ts","sourceRoot":"","sources":["../src/ink-chat.tsx"],"names":[],"mappings":"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"}
package/dist/ink-chat.js CHANGED
@@ -41,6 +41,10 @@ import { describeActivity } from './activity.js';
41
41
  import { redactSecrets, containsSecret } from './redact.js';
42
42
  import { renderTrajectory } from './trajectory.js';
43
43
  import { describeSandbox, resolveSandbox } from './sandbox.js';
44
+ import { doctorChecks, doctorReport } from './doctor.js';
45
+ import { runQualityGate } from './quality-gate.js';
46
+ import { listAuditEvents } from './audit.js';
47
+ import { initializePolicy, loadPolicy, POLICY_FILE } from './policy.js';
44
48
  const CYAN = '#54D7E7';
45
49
  const GREEN = '#9BCB8F';
46
50
  const CRIMSON = '#E8878D';
@@ -506,6 +510,7 @@ const COMMANDS = [
506
510
  { cmd: '/model', desc: 'Show or set the model by name' },
507
511
  { cmd: '/provider', desc: 'Show or switch provider' },
508
512
  { cmd: '/status', desc: 'Provider, model, mode, tokens, cost, workspace' },
513
+ { cmd: '/doctor', desc: 'Check the active provider, workspace, and developer tools' },
509
514
  { cmd: '/revert', desc: 'Undo file edits KONECK made' },
510
515
  { cmd: '/sessions', desc: 'List running KONECK sessions and their state' },
511
516
  { cmd: '/send', desc: 'Hand work to another session: /send <repo> <instruction>' },
@@ -532,6 +537,9 @@ const COMMANDS = [
532
537
  { cmd: '/fork', desc: 'Copy this session and continue on the copy' },
533
538
  { cmd: '/archive', desc: 'Keep a session but hide it from /resume' },
534
539
  { cmd: '/diff', desc: 'Show uncommitted git changes' },
540
+ { cmd: '/quality', desc: 'Run the detected lint and test checks' },
541
+ { cmd: '/audit', desc: 'Show recent redacted tool activity' },
542
+ { cmd: '/policy', desc: 'Show or initialize workspace safety rules' },
535
543
  { cmd: '/verify', desc: 'Run the project test command' },
536
544
  { cmd: '/rewind', desc: 'Drop the last exchange from history' },
537
545
  { cmd: '/copy', desc: 'Copy the last response to the clipboard' },
@@ -580,19 +588,20 @@ const MAX_DIFF_FILES = 4;
580
588
  const USAGE_TABS = ['settings', 'session', 'context', 'models', 'limits'];
581
589
  const MIN_REPLY_WIDTH = 24; // below this a table cannot render legibly anyway
582
590
  const PICKER_MIN_ROWS = 6;
583
- const PICKER_MAX_ROWS = 14;
584
- const PROVIDER_PICKER_MAX_ROWS = 16;
591
+ const PICKER_MAX_ROWS = 16;
585
592
  /**
586
593
  * Number of choices to keep visible in a palette.
587
594
  *
588
- * Provider choice is deliberately roomier than the other palettes: it is a small, finite set
589
- * and seeing every connection at once makes comparing and choosing it feel immediate. Larger
590
- * lists still leave room for the composer in a short terminal and can be paged through.
595
+ * A picker should feel like a real menu, not a letterbox. Sixteen rows keeps commands, models,
596
+ * files, sessions, and providers equally easy to scan; on short terminals it yields space to
597
+ * the composer and the rest remains available through search and paging.
591
598
  */
592
599
  export function pickerRowLimit(kind, itemCount, terminalRows) {
593
600
  const terminalBudget = Math.max(PICKER_MIN_ROWS, terminalRows - 8);
594
- const ideal = kind === 'provider' ? PROVIDER_PICKER_MAX_ROWS : PICKER_MAX_ROWS;
595
- return Math.min(itemCount, ideal, terminalBudget);
601
+ // Keep kind in the signature: it documents that this is the common policy for *every* picker
602
+ // and lets a future picker opt into a different policy without duplicating this calculation.
603
+ void kind;
604
+ return Math.min(itemCount, PICKER_MAX_ROWS, terminalBudget);
596
605
  }
597
606
  /**
598
607
  * Filters picker entries. Prefix matches rank above substring matches so typing "mo" offers
@@ -614,6 +623,61 @@ export function filterPickerItems(items, query) {
614
623
  }
615
624
  return [...starts, ...contains];
616
625
  }
626
+ /** The smallest number of character edits that turns one command into another. */
627
+ function commandDistance(a, b) {
628
+ // A transposed neighbouring pair is the commonest command typo (`/modle`). Treat it as one
629
+ // edit before using the ordinary insertion/deletion/substitution matrix below.
630
+ if (a.length === b.length) {
631
+ const mismatches = [...a].map((char, index) => char === b[index] ? -1 : index).filter(index => index >= 0);
632
+ if (mismatches.length === 2 && mismatches[1] === mismatches[0] + 1
633
+ && a[mismatches[0]] === b[mismatches[1]]
634
+ && a[mismatches[1]] === b[mismatches[0]])
635
+ return 1;
636
+ }
637
+ const previous = Array.from({ length: b.length + 1 }, (_, i) => i);
638
+ for (let i = 1; i <= a.length; i++) {
639
+ const current = [i];
640
+ for (let j = 1; j <= b.length; j++) {
641
+ current[j] = a[i - 1] === b[j - 1]
642
+ ? previous[j - 1]
643
+ : 1 + Math.min(previous[j - 1], previous[j], current[j - 1]);
644
+ }
645
+ for (let j = 0; j <= b.length; j++)
646
+ previous[j] = current[j];
647
+ }
648
+ return previous[b.length];
649
+ }
650
+ /**
651
+ * Turns a near-miss slash command into a direct recovery path.
652
+ *
653
+ * We deliberately do not guess at distant names: a wrong suggestion steals attention, while
654
+ * catching one or two transposed/omitted characters makes command use feel forgiving.
655
+ */
656
+ export function commandSuggestion(command, commands = COMMANDS.map(item => item.cmd)) {
657
+ const wanted = command.trim().toLowerCase();
658
+ if (!wanted.startsWith('/'))
659
+ return undefined;
660
+ const maxDistance = wanted.length <= 5 ? 1 : wanted.length <= 9 ? 2 : 3;
661
+ let best;
662
+ let distance = Number.POSITIVE_INFINITY;
663
+ for (const candidate of commands) {
664
+ const d = commandDistance(wanted, candidate.toLowerCase());
665
+ // At the same edit distance, preserve as much of what the user typed as possible. For
666
+ // `/modle`, `/model` is more likely than the shorter `/mode` even though both are two edits.
667
+ if (d < distance || (d === distance && (!best || Math.abs(candidate.length - wanted.length) < Math.abs(best.length - wanted.length)))) {
668
+ best = candidate;
669
+ distance = d;
670
+ }
671
+ }
672
+ return distance <= maxDistance ? best : undefined;
673
+ }
674
+ /** Keep a small, useful recall buffer without repeating the same submitted task. */
675
+ export function appendDraftHistory(history, draft, limit = 100) {
676
+ const entry = draft.trim();
677
+ if (entry === '' || history.at(-1) === entry)
678
+ return [...history];
679
+ return [...history, entry].slice(-Math.max(1, limit));
680
+ }
617
681
  /**
618
682
  * The choices offered for one waiting tool call.
619
683
  *
@@ -754,6 +818,7 @@ Slash commands:
754
818
  /endpoint http://192.168.1.50:11434/v1
755
819
  /key Enter an API key for the current provider
756
820
  /status Provider, model, mode, tokens, cost, CWD
821
+ /doctor Diagnose the active provider, workspace, and developer tools
757
822
  /clear /reset /new Clear conversation history
758
823
  /model [name] Show or switch model
759
824
  /provider [name] Show or switch provider
@@ -766,6 +831,9 @@ Slash commands:
766
831
  /cost Show session cost
767
832
  /memory Show .koneck/MEMORY.md
768
833
  /diff Show uncommitted git changes
834
+ /quality Run the detected lint and test checks
835
+ /audit [count] Show recent redacted tool activity
836
+ /policy [init] Show or create workspace safety rules
769
837
  /verify Run project test command
770
838
  /agents How parallel sub-agents are configured
771
839
  /subtask <task> Run a task in an isolated sub-agent
@@ -810,8 +878,14 @@ function App({ config: initialConfig, clearFrame }) {
810
878
  const streamedRef = useRef('');
811
879
  /** When the live region last had something new to show, used to pick the repaint rate. */
812
880
  const lastActivityRef = useRef(Date.now());
813
- /** Frames received before any content — proof the provider has the request and is working. */
814
- const [providerFrames, setProviderFrames] = useState(0);
881
+ /**
882
+ * Frames received before any content — proof the provider has the request and is working.
883
+ *
884
+ * Gateways can emit hundreds of these while routing. This must not be React state: doing so
885
+ * repainted the complete terminal for each network frame, which is the primary source of the
886
+ * visible flashing during a response. The already-throttled live refresh reads this ref.
887
+ */
888
+ const providerFramesRef = useRef(0);
815
889
  /**
816
890
  * Reasoning characters this turn, and when the last one arrived.
817
891
  *
@@ -1040,7 +1114,7 @@ function App({ config: initialConfig, clearFrame }) {
1040
1114
  if (liveToolRef.current)
1041
1115
  liveToolRef.current.output = line;
1042
1116
  },
1043
- onStreamActivity: (frames) => setProviderFrames(frames),
1117
+ onStreamActivity: (frames) => { providerFramesRef.current = frames; },
1044
1118
  onIncomplete: (reason) => { incompleteRef.current = reason; },
1045
1119
  onToolsUnsupported: (model) => {
1046
1120
  // Said once per session, not once per turn — it is a property of the model, not an event.
@@ -1097,6 +1171,11 @@ function App({ config: initialConfig, clearFrame }) {
1097
1171
  }))[0];
1098
1172
  const [rows, setRows] = useState([{ role: 'header' }]);
1099
1173
  const [draft, setDraft] = useState('');
1174
+ // Recall is deliberately session-local. It is for quick retries and refinements, not another
1175
+ // persistence channel for prompts (which may contain sensitive project context).
1176
+ const draftHistoryRef = useRef([]);
1177
+ const historyIndexRef = useRef(null);
1178
+ const historyDraftRef = useRef('');
1100
1179
  /** Caret offset into `draft`. Kept beside it so every edit can place the caret deliberately. */
1101
1180
  const [caret, setCaret] = useState(0);
1102
1181
  const [busy, setBusy] = useState(false);
@@ -1496,16 +1575,23 @@ function App({ config: initialConfig, clearFrame }) {
1496
1575
  }
1497
1576
  busyStart.current = Date.now();
1498
1577
  wordTimer.current = 0;
1499
- // The frame rate is the flicker rate. Every tick makes Ink erase and rewrite the whole live
1500
- // region measured at 188 line-erases a second, which on a modest terminal reads as the
1501
- // bottom of the screen flashing. So the rate follows how much is actually happening: quick
1502
- // while output is arriving, and much slower once a single long command is all that is left,
1503
- // where a spinner turning four times a second says as much as one turning seven.
1578
+ // The frame rate is the flicker rate. Every tick makes Ink update the live region, so network
1579
+ // activity must be sampled for a human reader rather than mirrored frame-for-frame. A third
1580
+ // of a second keeps streamed prose feeling live; an idle tool only needs a once-a-second
1581
+ // heartbeat. This is particularly important in terminals that repaint an ANSI line update as
1582
+ // a flash.
1504
1583
  let timer;
1505
1584
  const tick = () => {
1506
1585
  const elapsed = Date.now() - busyStart.current;
1507
1586
  const delay = currentDelay();
1508
- setAnim(a => ({ spin: (a.spin + 1) % SPINNER.length, shimmer: a.shimmer + 1, elapsed }));
1587
+ // Do not animate several cells for every content refresh. The spinner moves once a second
1588
+ // and the shimmer once every two seconds; the intervening refreshes are solely to reveal
1589
+ // new streamed text.
1590
+ setAnim({
1591
+ spin: Math.floor(elapsed / 1_000) % SPINNER.length,
1592
+ shimmer: Math.floor(elapsed / 2_000),
1593
+ elapsed,
1594
+ });
1509
1595
  wordTimer.current += 1;
1510
1596
  // Roughly every 60 seconds, whatever the current rate. Twenty-five felt churned on a long
1511
1597
  // run — the word is there to be read and enjoyed, not to reassure by moving.
@@ -1516,13 +1602,9 @@ function App({ config: initialConfig, clearFrame }) {
1516
1602
  };
1517
1603
  const currentDelay = () => {
1518
1604
  const idleFor = Date.now() - lastActivityRef.current;
1519
- // Two complaints pull against each other here: a fast repaint flickers, a slow one looks
1520
- // like the app has seized. 500ms was too far — at two frames a second the spinner visibly
1521
- // steps and reads as lag. These are the slowest rates at which the animation still reads
1522
- // as motion, which is the property that matters more than the byte count.
1523
- return idleFor < 2_000 ? 120 : idleFor > 15_000 ? 220 : 170;
1605
+ return idleFor < 2_000 ? 350 : 1_000;
1524
1606
  };
1525
- timer = setTimeout(tick, 150);
1607
+ timer = setTimeout(tick, 350);
1526
1608
  return () => clearTimeout(timer);
1527
1609
  }, [busy]);
1528
1610
  // A queued prompt runs the moment the agent is free again. Keyed on `busy` rather than done
@@ -1549,6 +1631,37 @@ function App({ config: initialConfig, clearFrame }) {
1549
1631
  useEffect(() => { void snapshotTree(cfg.cwd).then(t => { baseTreeRef.current = t; }); }, [cfg.cwd]);
1550
1632
  const addRow = (row) => setRows(prev => [...prev, row]);
1551
1633
  const addSystem = (text) => addRow({ role: 'system', text });
1634
+ /** Walk submitted composer entries without losing whatever was being typed now. */
1635
+ function recallDraft(direction) {
1636
+ const history = draftHistoryRef.current;
1637
+ if (history.length === 0)
1638
+ return;
1639
+ const current = historyIndexRef.current;
1640
+ if (direction === -1) {
1641
+ if (current === null)
1642
+ historyDraftRef.current = draft;
1643
+ const next = current === null ? history.length - 1 : Math.max(0, current - 1);
1644
+ historyIndexRef.current = next;
1645
+ const recalled = history[next];
1646
+ setDraft(recalled);
1647
+ setCaret(recalled.length);
1648
+ return;
1649
+ }
1650
+ if (current === null)
1651
+ return;
1652
+ const next = current + 1;
1653
+ if (next >= history.length) {
1654
+ historyIndexRef.current = null;
1655
+ const restored = historyDraftRef.current;
1656
+ setDraft(restored);
1657
+ setCaret(restored.length);
1658
+ return;
1659
+ }
1660
+ historyIndexRef.current = next;
1661
+ const recalled = history[next];
1662
+ setDraft(recalled);
1663
+ setCaret(recalled.length);
1664
+ }
1552
1665
  async function getSession() {
1553
1666
  return activeSession.p;
1554
1667
  }
@@ -1618,7 +1731,9 @@ function App({ config: initialConfig, clearFrame }) {
1618
1731
  id, task, provider: cfg.provider, model: cfg.model,
1619
1732
  cwd: cfg.cwd, startedAt: startedAtRef.current,
1620
1733
  turns: live.turns, totalTokens: live.totalTokens,
1621
- }, s?.messages ?? []);
1734
+ // Timings persisted alongside the conversation: the messages say what was said, only this
1735
+ // says where the time went.
1736
+ }, s?.messages ?? [], s?.trajectory);
1622
1737
  }
1623
1738
  /**
1624
1739
  * Compresses this session's state into something worth sending. One small model call, capped
@@ -2119,6 +2234,22 @@ function App({ config: initialConfig, clearFrame }) {
2119
2234
  `Cost : ${costStr}`);
2120
2235
  return;
2121
2236
  }
2237
+ case '/doctor': {
2238
+ // A session can change provider, endpoint, or workspace after startup. Running the same
2239
+ // checks here, against the *active* provider, means a diagnosis describes this session
2240
+ // rather than a stale global default.
2241
+ addSystem(`**KONECK Doctor** — checking ${cfg.provider} and this workspace…`);
2242
+ try {
2243
+ const checks = await doctorChecks(cfg.cwd, cfg.provider, cfg.baseURL, cfg.apiKey);
2244
+ setRows(prev => prev.slice(0, -1));
2245
+ addSystem(`**KONECK Doctor**\n\n${doctorReport(checks)}`);
2246
+ }
2247
+ catch (e) {
2248
+ setRows(prev => prev.slice(0, -1));
2249
+ addSystem(`Doctor could not finish: ${e instanceof Error ? e.message : String(e)}`);
2250
+ }
2251
+ return;
2252
+ }
2122
2253
  case '/clear':
2123
2254
  case '/reset':
2124
2255
  case '/new': {
@@ -2381,6 +2512,75 @@ function App({ config: initialConfig, clearFrame }) {
2381
2512
  }
2382
2513
  return;
2383
2514
  }
2515
+ case '/quality': {
2516
+ // The engine uses this exact gate after a coding turn. Exposing it in chat gives the user
2517
+ // the same independent proof on demand, without the legacy TUI writing over Ink's frame.
2518
+ addSystem('**Quality gate** — running detected lint and test checks…');
2519
+ try {
2520
+ const result = await runQualityGate(cfg.cwd, { report: false });
2521
+ setRows(prev => prev.slice(0, -1));
2522
+ if (result.checks.length === 0) {
2523
+ addSystem('**Quality gate**\n\nNo lint or test command was detected for this workspace.');
2524
+ return;
2525
+ }
2526
+ const lines = result.checks.flatMap(check => {
2527
+ const state = check.passed ? 'PASS' : 'FAIL';
2528
+ const detail = check.output?.trim();
2529
+ const tail = !check.passed && detail
2530
+ ? `\n\n\`\`\`\n${detail.slice(-2_000)}\n\`\`\``
2531
+ : '';
2532
+ return [`**${state}** ${check.label} — \`${check.command}\` (${fmtElapsed(check.durationMs)})${tail}`];
2533
+ });
2534
+ addSystem(`**Quality gate ${result.passed ? 'passed' : 'needs attention'}**\n\n` +
2535
+ lines.join('\n\n'));
2536
+ }
2537
+ catch (e) {
2538
+ setRows(prev => prev.slice(0, -1));
2539
+ addSystem(`Quality gate could not finish: ${e instanceof Error ? e.message : String(e)}`);
2540
+ }
2541
+ return;
2542
+ }
2543
+ case '/audit': {
2544
+ const requested = Number.parseInt(arg, 10);
2545
+ const limit = Number.isFinite(requested) ? Math.max(1, Math.min(requested, 100)) : 20;
2546
+ const events = await listAuditEvents(cfg.cwd, limit);
2547
+ if (events.length === 0) {
2548
+ addSystem('No recorded tool activity yet. KONECK records redacted actions in `.koneck/audit/`.');
2549
+ return;
2550
+ }
2551
+ const lines = events.map(event => {
2552
+ const when = new Date(event.timestamp).toLocaleString();
2553
+ const state = event.outcome === 'success' ? 'OK' : 'FAIL';
2554
+ const action = Object.entries(event.action)
2555
+ .map(([key, value]) => `${key}=${String(value)}`)
2556
+ .join(' · ');
2557
+ return `${state.padEnd(4)} ${when} ${event.tool} ${action} (${fmtElapsed(event.durationMs)})`;
2558
+ });
2559
+ addSystem(`**Recent tool activity** — ${events.length} event${events.length === 1 ? '' : 's'}\n\n${lines.join('\n')}`);
2560
+ return;
2561
+ }
2562
+ case '/policy': {
2563
+ if (arg.trim().toLowerCase() === 'init') {
2564
+ try {
2565
+ addSystem(await initializePolicy(cfg.cwd));
2566
+ }
2567
+ catch (e) {
2568
+ addSystem(`Could not create ${POLICY_FILE}: ${e instanceof Error ? e.message : String(e)}`);
2569
+ }
2570
+ return;
2571
+ }
2572
+ const policy = await loadPolicy(cfg.cwd);
2573
+ if (!policy) {
2574
+ addSystem(`No workspace policy is active. Run \`/policy init\` to create \`${POLICY_FILE}\`.`);
2575
+ return;
2576
+ }
2577
+ addSystem(`**KONECK policy** — \`${POLICY_FILE}\`\n\n` +
2578
+ `Protected paths : ${policy.protectedPaths?.length ?? 0}\n` +
2579
+ `Write scopes : ${policy.allowedWritePaths?.length ? policy.allowedWritePaths.join(', ') : 'unrestricted'}\n` +
2580
+ `Blocked commands: ${policy.blockedCommands?.length ?? 0}\n` +
2581
+ `Approval rules : ${policy.requireApprovalFor?.join(', ') || 'none'}`);
2582
+ return;
2583
+ }
2384
2584
  case '/verify': {
2385
2585
  const { detectProject } = await import('./project.js');
2386
2586
  const project = await detectProject(cfg.cwd);
@@ -2803,7 +3003,12 @@ function App({ config: initialConfig, clearFrame }) {
2803
3003
  return;
2804
3004
  }
2805
3005
  default:
2806
- addSystem(`Unknown command: ${cmd}. Type /help for the full list.`);
3006
+ {
3007
+ const suggestion = commandSuggestion(cmd);
3008
+ addSystem(suggestion
3009
+ ? `Unknown command: ${cmd}. Did you mean \`${suggestion}\`?`
3010
+ : `Unknown command: ${cmd}. Type /help for the full list.`);
3011
+ }
2807
3012
  }
2808
3013
  }
2809
3014
  /**
@@ -3249,6 +3454,14 @@ function App({ config: initialConfig, clearFrame }) {
3249
3454
  // The composer used to be append-and-backspace only, so fixing a typo near the start of a
3250
3455
  // long prompt meant deleting everything after it. Everything below is the readline set a
3251
3456
  // shell gives you, so an edit costs one keystroke rather than a retype.
3457
+ if (key.upArrow) {
3458
+ recallDraft(-1);
3459
+ return;
3460
+ }
3461
+ if (key.downArrow) {
3462
+ recallDraft(1);
3463
+ return;
3464
+ }
3252
3465
  if (key.leftArrow) {
3253
3466
  const step = 1 + takeBurst('left');
3254
3467
  setCaret(c => (key.ctrl || key.meta ? wordStart(draft, c) : Math.max(0, c - step)));
@@ -3403,7 +3616,7 @@ function App({ config: initialConfig, clearFrame }) {
3403
3616
  thinkingTextRef.current = '';
3404
3617
  incompleteRef.current = null;
3405
3618
  lastActivityRef.current = Date.now();
3406
- setProviderFrames(0);
3619
+ providerFramesRef.current = 0;
3407
3620
  liveToolRef.current = null;
3408
3621
  charsRef.current = 0;
3409
3622
  realTokRef.current = 0;
@@ -3685,7 +3898,7 @@ function App({ config: initialConfig, clearFrame }) {
3685
3898
  // A gateway routing to several backends sends content-free frames while it
3686
3899
  // finds one. Saying "waiting for the first token" through that reads as a
3687
3900
  // hang, when in fact the request was accepted and is being worked on.
3688
- : providerFrames > 0 ? `${cfg.provider} is holding the line, no output yet`
3901
+ : providerFramesRef.current > 0 ? `${cfg.provider} is holding the line, no output yet`
3689
3902
  : elapsedMs > 8_000 ? `no response from ${cfg.provider} yet`
3690
3903
  : 'starting', thinkingRef.current.chars > 0 && Date.now() - thinkingRef.current.at < 2_000
3691
3904
  ? ` | thinking, ${fmtTokens(Math.round(thinkingRef.current.chars / 4))} reasoning tokens`
@@ -3905,12 +4118,15 @@ function App({ config: initialConfig, clearFrame }) {
3905
4118
  // right-hand side at a constant made "Reference a file or directory" wrap onto two rows.
3906
4119
  const countText = pickerCountText(pickIndex, list.length, picker.items.length);
3907
4120
  const headWidth = title.length + countText.length + 3;
3908
- const rowWidth = Math.max(headWidth, ...shown.map(it => 38 + (it.current ? 10 : 0) + it.desc.length));
4121
+ // Measure the filtered list, not just its current scroll window. A palette whose right
4122
+ // edge breathes as the highlight crosses a long description feels broken even when the
4123
+ // selection itself is correct.
4124
+ const rowWidth = Math.max(headWidth, ...list.map(it => 38 + (it.current ? 10 : 0) + it.desc.length + (it.group?.length ?? 0) + 3));
3909
4125
  const boxWidth = Math.max(40, Math.min(barWidth, rowWidth + 4));
3910
4126
  const width = boxWidth - 4;
3911
4127
  // Room for the longest label the list actually holds, rather than a constant. A session
3912
4128
  // title is what identifies the row, and 36 cells cut most of them in half.
3913
- const labelWidth = pickerLabelWidth(shown.map(it => it.label), shown.map(it => it.desc), width);
4129
+ const labelWidth = pickerLabelWidth(list.map(it => it.label), list.map(it => it.desc), width);
3914
4130
  let lastGroup;
3915
4131
  return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: CYAN, paddingX: 1, marginTop: 1, width: boxWidth, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { color: CYAN, bold: true, children: title }), _jsx(Text, { color: DIM, children: countText })] }), _jsxs(Box, { children: [_jsxs(Text, { color: DIM, children: [G.search, " "] }), pickQuery === ''
3916
4132
  ? _jsx(Text, { color: DIM, children: "type to search" })
@@ -3920,8 +4136,8 @@ function App({ config: initialConfig, clearFrame }) {
3920
4136
  const header = item.group && item.group !== lastGroup ? item.group : null;
3921
4137
  lastGroup = item.group;
3922
4138
  const label = item.label.length > 34 ? item.label.slice(0, 31) + '...' : item.label;
3923
- return (_jsxs(Box, { flexDirection: "column", children: [header && _jsx(Text, { color: DIM, children: header }), _jsx(Text, { backgroundColor: selected ? CYAN : undefined, color: selected ? '#10222A' : INK, bold: selected, children: fitCells(`${selected ? G.caret + ' ' : ' '}${fitCells(label, labelWidth)}` +
3924
- `${item.current ? '(current) ' : ''}${item.desc}`, width) })] }, item.value + absolute));
4139
+ return (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { backgroundColor: selected ? CYAN : undefined, color: selected ? '#10222A' : INK, bold: selected, children: fitCells(`${selected ? G.caret + ' ' : ' '}${fitCells(label, labelWidth)}` +
4140
+ `${item.current ? '(current) ' : ''}${header ? `${header} · ` : ''}${item.desc}`, width) }) }, item.value + absolute));
3925
4141
  }), list.length > pickerRows && (_jsxs(Text, { color: DIM, children: ["\u2191\u2193 navigate \u00B7 pgup/pgdn jump \u00B7 enter select \u00B7 showing ", Math.max(0, start) + 1, "\u2013", Math.max(0, start) + shown.length, " of ", list.length] }))] }));
3926
4142
  })(), keyPrompt && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: AMBER, paddingX: 1, marginTop: 1, width: barWidth, children: [_jsxs(Text, { color: AMBER, bold: true, children: ["API key for ", keyPrompt.provider] }), _jsx(Text, { color: MUTED, children: "Paste it and press enter. Held in memory for this session only; esc to cancel." }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: MUTED, children: [keyPrompt.env, ": "] }), _jsx(Text, { color: INK, children: '*'.repeat(Math.min(keyPrompt.value.length, 48)) }), _jsx(Text, { color: CYAN, children: "\u2588" })] })] })), ask && (() => {
3927
4143
  let detail = '';