koneck 2.26.0 → 2.27.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 (51) hide show
  1. package/dist/doctor.d.ts +15 -0
  2. package/dist/doctor.d.ts.map +1 -1
  3. package/dist/doctor.js +28 -8
  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 +3 -3
  9. package/dist/ink-chat.d.ts.map +1 -1
  10. package/dist/ink-chat.js +134 -25
  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 +60 -26
  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/history.d.ts +61 -0
  28. package/dist/web/history.d.ts.map +1 -0
  29. package/dist/web/history.js +197 -0
  30. package/dist/web/history.js.map +1 -0
  31. package/dist/web/mark.d.ts +15 -0
  32. package/dist/web/mark.d.ts.map +1 -0
  33. package/dist/web/mark.js +15 -0
  34. package/dist/web/mark.js.map +1 -0
  35. package/dist/web/sessions.d.ts +8 -0
  36. package/dist/web/sessions.d.ts.map +1 -1
  37. package/dist/web/sessions.js +9 -0
  38. package/dist/web/sessions.js.map +1 -1
  39. package/dist/web/ui-client.d.ts +14 -0
  40. package/dist/web/ui-client.d.ts.map +1 -0
  41. package/dist/web/ui-client.js +0 -0
  42. package/dist/web/ui-client.js.map +1 -0
  43. package/dist/web/ui-css.d.ts +15 -0
  44. package/dist/web/ui-css.d.ts.map +1 -0
  45. package/dist/web/ui-css.js +284 -0
  46. package/dist/web/ui-css.js.map +1 -0
  47. package/dist/web/ui.d.ts +6 -9
  48. package/dist/web/ui.d.ts.map +1 -1
  49. package/dist/web/ui.js +72 -542
  50. package/dist/web/ui.js.map +1 -1
  51. package/package.json +1 -1
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
@@ -754,6 +763,7 @@ Slash commands:
754
763
  /endpoint http://192.168.1.50:11434/v1
755
764
  /key Enter an API key for the current provider
756
765
  /status Provider, model, mode, tokens, cost, CWD
766
+ /doctor Diagnose the active provider, workspace, and developer tools
757
767
  /clear /reset /new Clear conversation history
758
768
  /model [name] Show or switch model
759
769
  /provider [name] Show or switch provider
@@ -766,6 +776,9 @@ Slash commands:
766
776
  /cost Show session cost
767
777
  /memory Show .koneck/MEMORY.md
768
778
  /diff Show uncommitted git changes
779
+ /quality Run the detected lint and test checks
780
+ /audit [count] Show recent redacted tool activity
781
+ /policy [init] Show or create workspace safety rules
769
782
  /verify Run project test command
770
783
  /agents How parallel sub-agents are configured
771
784
  /subtask <task> Run a task in an isolated sub-agent
@@ -810,8 +823,14 @@ function App({ config: initialConfig, clearFrame }) {
810
823
  const streamedRef = useRef('');
811
824
  /** When the live region last had something new to show, used to pick the repaint rate. */
812
825
  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);
826
+ /**
827
+ * Frames received before any content — proof the provider has the request and is working.
828
+ *
829
+ * Gateways can emit hundreds of these while routing. This must not be React state: doing so
830
+ * repainted the complete terminal for each network frame, which is the primary source of the
831
+ * visible flashing during a response. The already-throttled live refresh reads this ref.
832
+ */
833
+ const providerFramesRef = useRef(0);
815
834
  /**
816
835
  * Reasoning characters this turn, and when the last one arrived.
817
836
  *
@@ -1040,7 +1059,7 @@ function App({ config: initialConfig, clearFrame }) {
1040
1059
  if (liveToolRef.current)
1041
1060
  liveToolRef.current.output = line;
1042
1061
  },
1043
- onStreamActivity: (frames) => setProviderFrames(frames),
1062
+ onStreamActivity: (frames) => { providerFramesRef.current = frames; },
1044
1063
  onIncomplete: (reason) => { incompleteRef.current = reason; },
1045
1064
  onToolsUnsupported: (model) => {
1046
1065
  // Said once per session, not once per turn — it is a property of the model, not an event.
@@ -1496,16 +1515,23 @@ function App({ config: initialConfig, clearFrame }) {
1496
1515
  }
1497
1516
  busyStart.current = Date.now();
1498
1517
  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.
1518
+ // The frame rate is the flicker rate. Every tick makes Ink update the live region, so network
1519
+ // activity must be sampled for a human reader rather than mirrored frame-for-frame. A third
1520
+ // of a second keeps streamed prose feeling live; an idle tool only needs a once-a-second
1521
+ // heartbeat. This is particularly important in terminals that repaint an ANSI line update as
1522
+ // a flash.
1504
1523
  let timer;
1505
1524
  const tick = () => {
1506
1525
  const elapsed = Date.now() - busyStart.current;
1507
1526
  const delay = currentDelay();
1508
- setAnim(a => ({ spin: (a.spin + 1) % SPINNER.length, shimmer: a.shimmer + 1, elapsed }));
1527
+ // Do not animate several cells for every content refresh. The spinner moves once a second
1528
+ // and the shimmer once every two seconds; the intervening refreshes are solely to reveal
1529
+ // new streamed text.
1530
+ setAnim({
1531
+ spin: Math.floor(elapsed / 1_000) % SPINNER.length,
1532
+ shimmer: Math.floor(elapsed / 2_000),
1533
+ elapsed,
1534
+ });
1509
1535
  wordTimer.current += 1;
1510
1536
  // Roughly every 60 seconds, whatever the current rate. Twenty-five felt churned on a long
1511
1537
  // run — the word is there to be read and enjoyed, not to reassure by moving.
@@ -1516,13 +1542,9 @@ function App({ config: initialConfig, clearFrame }) {
1516
1542
  };
1517
1543
  const currentDelay = () => {
1518
1544
  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;
1545
+ return idleFor < 2_000 ? 350 : 1_000;
1524
1546
  };
1525
- timer = setTimeout(tick, 150);
1547
+ timer = setTimeout(tick, 350);
1526
1548
  return () => clearTimeout(timer);
1527
1549
  }, [busy]);
1528
1550
  // A queued prompt runs the moment the agent is free again. Keyed on `busy` rather than done
@@ -1618,7 +1640,9 @@ function App({ config: initialConfig, clearFrame }) {
1618
1640
  id, task, provider: cfg.provider, model: cfg.model,
1619
1641
  cwd: cfg.cwd, startedAt: startedAtRef.current,
1620
1642
  turns: live.turns, totalTokens: live.totalTokens,
1621
- }, s?.messages ?? []);
1643
+ // Timings persisted alongside the conversation: the messages say what was said, only this
1644
+ // says where the time went.
1645
+ }, s?.messages ?? [], s?.trajectory);
1622
1646
  }
1623
1647
  /**
1624
1648
  * Compresses this session's state into something worth sending. One small model call, capped
@@ -2119,6 +2143,22 @@ function App({ config: initialConfig, clearFrame }) {
2119
2143
  `Cost : ${costStr}`);
2120
2144
  return;
2121
2145
  }
2146
+ case '/doctor': {
2147
+ // A session can change provider, endpoint, or workspace after startup. Running the same
2148
+ // checks here, against the *active* provider, means a diagnosis describes this session
2149
+ // rather than a stale global default.
2150
+ addSystem(`**KONECK Doctor** — checking ${cfg.provider} and this workspace…`);
2151
+ try {
2152
+ const checks = await doctorChecks(cfg.cwd, cfg.provider);
2153
+ setRows(prev => prev.slice(0, -1));
2154
+ addSystem(`**KONECK Doctor**\n\n${doctorReport(checks)}`);
2155
+ }
2156
+ catch (e) {
2157
+ setRows(prev => prev.slice(0, -1));
2158
+ addSystem(`Doctor could not finish: ${e instanceof Error ? e.message : String(e)}`);
2159
+ }
2160
+ return;
2161
+ }
2122
2162
  case '/clear':
2123
2163
  case '/reset':
2124
2164
  case '/new': {
@@ -2381,6 +2421,75 @@ function App({ config: initialConfig, clearFrame }) {
2381
2421
  }
2382
2422
  return;
2383
2423
  }
2424
+ case '/quality': {
2425
+ // The engine uses this exact gate after a coding turn. Exposing it in chat gives the user
2426
+ // the same independent proof on demand, without the legacy TUI writing over Ink's frame.
2427
+ addSystem('**Quality gate** — running detected lint and test checks…');
2428
+ try {
2429
+ const result = await runQualityGate(cfg.cwd, { report: false });
2430
+ setRows(prev => prev.slice(0, -1));
2431
+ if (result.checks.length === 0) {
2432
+ addSystem('**Quality gate**\n\nNo lint or test command was detected for this workspace.');
2433
+ return;
2434
+ }
2435
+ const lines = result.checks.flatMap(check => {
2436
+ const state = check.passed ? 'PASS' : 'FAIL';
2437
+ const detail = check.output?.trim();
2438
+ const tail = !check.passed && detail
2439
+ ? `\n\n\`\`\`\n${detail.slice(-2_000)}\n\`\`\``
2440
+ : '';
2441
+ return [`**${state}** ${check.label} — \`${check.command}\` (${fmtElapsed(check.durationMs)})${tail}`];
2442
+ });
2443
+ addSystem(`**Quality gate ${result.passed ? 'passed' : 'needs attention'}**\n\n` +
2444
+ lines.join('\n\n'));
2445
+ }
2446
+ catch (e) {
2447
+ setRows(prev => prev.slice(0, -1));
2448
+ addSystem(`Quality gate could not finish: ${e instanceof Error ? e.message : String(e)}`);
2449
+ }
2450
+ return;
2451
+ }
2452
+ case '/audit': {
2453
+ const requested = Number.parseInt(arg, 10);
2454
+ const limit = Number.isFinite(requested) ? Math.max(1, Math.min(requested, 100)) : 20;
2455
+ const events = await listAuditEvents(cfg.cwd, limit);
2456
+ if (events.length === 0) {
2457
+ addSystem('No recorded tool activity yet. KONECK records redacted actions in `.koneck/audit/`.');
2458
+ return;
2459
+ }
2460
+ const lines = events.map(event => {
2461
+ const when = new Date(event.timestamp).toLocaleString();
2462
+ const state = event.outcome === 'success' ? 'OK' : 'FAIL';
2463
+ const action = Object.entries(event.action)
2464
+ .map(([key, value]) => `${key}=${String(value)}`)
2465
+ .join(' · ');
2466
+ return `${state.padEnd(4)} ${when} ${event.tool} ${action} (${fmtElapsed(event.durationMs)})`;
2467
+ });
2468
+ addSystem(`**Recent tool activity** — ${events.length} event${events.length === 1 ? '' : 's'}\n\n${lines.join('\n')}`);
2469
+ return;
2470
+ }
2471
+ case '/policy': {
2472
+ if (arg.trim().toLowerCase() === 'init') {
2473
+ try {
2474
+ addSystem(await initializePolicy(cfg.cwd));
2475
+ }
2476
+ catch (e) {
2477
+ addSystem(`Could not create ${POLICY_FILE}: ${e instanceof Error ? e.message : String(e)}`);
2478
+ }
2479
+ return;
2480
+ }
2481
+ const policy = await loadPolicy(cfg.cwd);
2482
+ if (!policy) {
2483
+ addSystem(`No workspace policy is active. Run \`/policy init\` to create \`${POLICY_FILE}\`.`);
2484
+ return;
2485
+ }
2486
+ addSystem(`**KONECK policy** — \`${POLICY_FILE}\`\n\n` +
2487
+ `Protected paths : ${policy.protectedPaths?.length ?? 0}\n` +
2488
+ `Write scopes : ${policy.allowedWritePaths?.length ? policy.allowedWritePaths.join(', ') : 'unrestricted'}\n` +
2489
+ `Blocked commands: ${policy.blockedCommands?.length ?? 0}\n` +
2490
+ `Approval rules : ${policy.requireApprovalFor?.join(', ') || 'none'}`);
2491
+ return;
2492
+ }
2384
2493
  case '/verify': {
2385
2494
  const { detectProject } = await import('./project.js');
2386
2495
  const project = await detectProject(cfg.cwd);
@@ -3403,7 +3512,7 @@ function App({ config: initialConfig, clearFrame }) {
3403
3512
  thinkingTextRef.current = '';
3404
3513
  incompleteRef.current = null;
3405
3514
  lastActivityRef.current = Date.now();
3406
- setProviderFrames(0);
3515
+ providerFramesRef.current = 0;
3407
3516
  liveToolRef.current = null;
3408
3517
  charsRef.current = 0;
3409
3518
  realTokRef.current = 0;
@@ -3685,7 +3794,7 @@ function App({ config: initialConfig, clearFrame }) {
3685
3794
  // A gateway routing to several backends sends content-free frames while it
3686
3795
  // finds one. Saying "waiting for the first token" through that reads as a
3687
3796
  // 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`
3797
+ : providerFramesRef.current > 0 ? `${cfg.provider} is holding the line, no output yet`
3689
3798
  : elapsedMs > 8_000 ? `no response from ${cfg.provider} yet`
3690
3799
  : 'starting', thinkingRef.current.chars > 0 && Date.now() - thinkingRef.current.at < 2_000
3691
3800
  ? ` | thinking, ${fmtTokens(Math.round(thinkingRef.current.chars / 4))} reasoning tokens`