koneck 2.128.27 → 2.128.29

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
@@ -8,7 +8,7 @@ import { pacedInterval, startingInterval, floorFor, shouldDrawFrame } from './fr
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';
11
- import { loadKoneckConfig, saveKoneckConfig, setConfigKey, configKeyDescriptions, CONFIG_FILE, CONFIG_SCHEMA, stepValue, displayValue } from './config-store.js';
11
+ import { loadKoneckConfig, saveKoneckConfig, setConfigKey, configKeyDescriptions, CONFIG_FILE, CONFIG_SCHEMA, stepValue, displayValue, settingsWindow } from './config-store.js';
12
12
  import { PROVIDERS, resolveProvider, getApiKey, addUserProvider, removeUserProvider, userProvidersPath, keyEnvFor, BUILT_INS as BUILT_IN_PROVIDERS, servesLocally, servesOwnWeights, endpointForProvider } from './providers.js';
13
13
  import { normaliseEndpoint, withEndpoint, savedEndpoints, shortSource, resolveModelChoice, withModel } from './endpoints.js';
14
14
  import { loadProjectConfig, loadGlobalConfig, saveGlobalConfig, withoutStoredApiKey } from './config.js';
@@ -715,6 +715,14 @@ const PANEL_DIFF_LINES = 400;
715
715
  const USAGE_TABS = ['settings', 'session', 'context', 'models', 'limits'];
716
716
  const MIN_REPLY_WIDTH = 24; // below this a table cannot render legibly anyway
717
717
  const PICKER_MAX_ROWS = 16;
718
+ /**
719
+ * Rows kept for everything around a picker in ordinary scrollback.
720
+ *
721
+ * The composer with its border, the status bar, the footer, the picker's own border and hint
722
+ * row, and the blanks between them. Found by measurement rather than by adding those up: at
723
+ * 80x24, nine and twelve both still wiped the terminal and fourteen did not.
724
+ */
725
+ const DEFAULT_PICKER_RESERVE = 14;
718
726
  /**
719
727
  * Number of choices to keep visible in a palette.
720
728
  *
@@ -723,13 +731,24 @@ const PICKER_MAX_ROWS = 16;
723
731
  * the composer and the rest remains available through search and paging.
724
732
  */
725
733
  export function pickerRowLimit(kind, itemCount, terminalRows, reservedRows) {
726
- // In ordinary scrollback this is not a fixed overlay: it can extend into the terminal's normal
727
- // scroll region, so shrinking a command picker just because a pane has ten rows is needless
728
- // friction. Only alternate-screen callers pass a reservation, because that is a fixed frame
729
- // and overflow there makes Ink clear/repaint the entire screen.
730
- const terminalBudget = reservedRows === undefined
731
- ? PICKER_MAX_ROWS
732
- : Math.max(1, (terminalRows && terminalRows > 0 ? terminalRows : 24) - reservedRows);
734
+ /*
735
+ * The window bounds this on every screen, and it used not to.
736
+ *
737
+ * The reasoning here was that ordinary scrollback is not a fixed frame, so a picker may run as
738
+ * long as it likes and let the terminal scroll — and that only the alternate screen needed a
739
+ * reservation, because overflow there makes Ink clear and repaint everything.
740
+ *
741
+ * Ink does not make that distinction. It compares the height of what it is about to draw
742
+ * against `stdout.rows` and wipes the terminal when it does not fit, whichever screen is in
743
+ * use, because it cannot erase lines that have scrolled away. The picker is part of the
744
+ * repainting region in both. Measured at 80x24: typing "/config" wiped the screen three times
745
+ * before the command had even been chosen, so every slash command flashed the whole terminal.
746
+ *
747
+ * The reservation differs — the alternate screen has a transcript above the picker to leave
748
+ * room for — but there is always one.
749
+ */
750
+ const reserved = reservedRows ?? DEFAULT_PICKER_RESERVE;
751
+ const terminalBudget = Math.max(1, (terminalRows && terminalRows > 0 ? terminalRows : 24) - reserved);
733
752
  // Keep kind in the signature: it documents that this is the common policy for *every* picker
734
753
  // and lets a future picker opt into a different policy without duplicating this calculation.
735
754
  void kind;
@@ -2389,6 +2408,14 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
2389
2408
  }, [effort]);
2390
2409
  const [showAnalytics, setShowAnalytics] = useState(false);
2391
2410
  const [updatePane, setUpdatePane] = useState(null);
2411
+ /**
2412
+ * The version installed in the background, if one was; see auto-update.ts.
2413
+ *
2414
+ * Only ever says "installed", never "updated": the code this session is running was read into
2415
+ * memory when it started, and replacing the files on disk does not change it. The restart is
2416
+ * the part that matters and so it is the part the line ends on.
2417
+ */
2418
+ const [autoUpdated, setAutoUpdated] = useState(null);
2392
2419
  const [updating, setUpdating] = useState('idle');
2393
2420
  const [updateLog, setUpdateLog] = useState('');
2394
2421
  // Settable, because resuming has to adopt the restored session's id. Without that, every save
@@ -2804,6 +2831,35 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
2804
2831
  timer = setTimeout(tick, interval);
2805
2832
  return () => clearTimeout(timer);
2806
2833
  }, [busy, animate, ask]);
2834
+ /*
2835
+ * Check for a new version and install it, quietly, in the background.
2836
+ *
2837
+ * Deliberately late and deliberately silent. It is network and a subprocess, neither of which
2838
+ * should compete with the first turn, and there is nothing to report unless it succeeds — a
2839
+ * registry that cannot be reached or an npm that cannot write to its global prefix is not the
2840
+ * user's problem to hear about mid-task. See auto-update.ts for what it refuses to do.
2841
+ */
2842
+ useEffect(() => {
2843
+ if (cfg.autoUpdate === false)
2844
+ return;
2845
+ let live = true;
2846
+ const timer = setTimeout(() => {
2847
+ void (async () => {
2848
+ try {
2849
+ const { autoUpdate } = await import('./auto-update.js');
2850
+ const outcome = await autoUpdate({
2851
+ currentVersion: VERSION,
2852
+ modulePath: fileURLToPath(import.meta.url),
2853
+ enabled: cfg.autoUpdate !== false,
2854
+ });
2855
+ if (live && outcome.state === 'installed')
2856
+ setAutoUpdated(outcome.version);
2857
+ }
2858
+ catch { /* an update that cannot happen is not worth interrupting anyone over */ }
2859
+ })();
2860
+ }, 4_000);
2861
+ return () => { live = false; clearTimeout(timer); };
2862
+ }, [cfg.autoUpdate]);
2807
2863
  // A queued prompt runs the moment the agent is free again. Keyed on `busy` rather than done
2808
2864
  // inside the turn's own exit paths, so every way a turn can end — success, failure, cancel —
2809
2865
  // drains the queue the same way.
@@ -4784,6 +4840,12 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
4784
4840
  setCfg(c => ({ ...c, autoCopy: ['true', '1', 'yes'].includes(val.toLowerCase()) }));
4785
4841
  if (key === 'animate')
4786
4842
  setAnimate(['true', '1', 'yes'].includes(val.toLowerCase()));
4843
+ // Applied to the live session, not only written to the file. Every other switch here
4844
+ // takes effect at once, and one that quietly waited for a restart was a setting you
4845
+ // could not tell you had changed.
4846
+ if (key === 'autoUpdate') {
4847
+ setCfg(c => ({ ...c, autoUpdate: ['true', '1', 'yes'].includes(val.toLowerCase()) }));
4848
+ }
4787
4849
  if (key === 'mouse') {
4788
4850
  setMouse(['true', '1', 'yes'].includes(val.toLowerCase()));
4789
4851
  }
@@ -5397,12 +5459,12 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
5397
5459
  return;
5398
5460
  }
5399
5461
  if (key.pageDown) {
5400
- const jump = pickerRowLimit(picker.kind, list.length, ...(altScreen ? [termRows, 13] : []));
5462
+ const jump = pickerRowLimit(picker.kind, list.length, termRows, altScreen ? 13 : undefined);
5401
5463
  setPickIndex(i => Math.min(i + jump, Math.max(0, list.length - 1)));
5402
5464
  return;
5403
5465
  }
5404
5466
  if (key.pageUp) {
5405
- const jump = pickerRowLimit(picker.kind, list.length, ...(altScreen ? [termRows, 13] : []));
5467
+ const jump = pickerRowLimit(picker.kind, list.length, termRows, altScreen ? 13 : undefined);
5406
5468
  setPickIndex(i => Math.max(0, i - jump));
5407
5469
  return;
5408
5470
  }
@@ -6360,13 +6422,40 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
6360
6422
  const meter = (fraction, label) => [`${bar(fraction, BAR_W)} ${Math.round(fraction * 100)}%`, `${label}`, ''];
6361
6423
  const lines = [tabs, '---'];
6362
6424
  if (usagePane === 'settings') {
6363
- lines.push('**Settings** — written to disk and applied now', '');
6425
+ const compact = termRows < 30;
6426
+ const chrome = compact ? 21 : 21;
6427
+ if (!compact)
6428
+ lines.push('**Settings** — written to disk and applied now', '');
6364
6429
  // What is actually in force, not just what this file happens to hold. provider and
6365
6430
  // baseURL commonly come from ~/.koneckrc, and showing those as "(not set)" made a
6366
6431
  // configured session look unconfigured.
6367
6432
  const endpoint = endpointFor(cfg.provider);
6368
6433
  const labelW = Math.max(...CONFIG_SCHEMA.map(f => f.label.length));
6434
+ /*
6435
+ * Only as many rows as there is room for.
6436
+ *
6437
+ * Everything else in this pane costs rows too — the tabs, the heading, the two footer
6438
+ * lines, the panel's own border — and below it sit the composer and the status bar. Drawn
6439
+ * in full on a twenty-four-row terminal the list ran off the top, leaving what looked
6440
+ * like a stunted four-row panel. It also made the repainting region as tall as the
6441
+ * window, which is what makes Ink wipe the terminal on every render.
6442
+ */
6443
+ /*
6444
+ * How many rows there is room for, and what gets dropped when there is not.
6445
+ *
6446
+ * Measured, not estimated. At 80x24 the pane came to twenty-five lines with the list
6447
+ * already cut to its minimum of three, and twenty-five lines in a twenty-four-row
6448
+ * window is what makes Ink wipe the whole terminal on every render. Shrinking the list
6449
+ * further could not have fixed that — the fixed parts were the problem — so on a short
6450
+ * terminal the pane drops its own furniture first: the heading, the blank beneath it,
6451
+ * and the line naming the config file. They are the three that say least.
6452
+ */
6453
+ const { start, end } = settingsWindow(CONFIG_SCHEMA.length, settingRow, Math.max(3, termRows - chrome));
6454
+ if (start > 0)
6455
+ lines.push(` ${G.caret} ${start} more above`);
6369
6456
  for (const [i, f] of CONFIG_SCHEMA.entries()) {
6457
+ if (i < start || i >= end)
6458
+ continue;
6370
6459
  const selected = i === settingRow;
6371
6460
  const beingTyped = editing?.key === String(f.key);
6372
6461
  // An endpoint always has a source worth naming, so it never reads as unset or as
@@ -6393,11 +6482,15 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
6393
6482
  : ` enter to edit${f.clearable ? ', x clears' : ''}`;
6394
6483
  lines.push(`${mark} ${f.label.padEnd(labelW)} ${selected ? '**' + shown + '**' : shown}${hint}`);
6395
6484
  }
6485
+ if (end < CONFIG_SCHEMA.length) {
6486
+ lines.push(` ${G.caret} ${CONFIG_SCHEMA.length - end} more below`);
6487
+ }
6396
6488
  lines.push('');
6397
6489
  lines.push(editing
6398
6490
  ? 'Typing. **enter** saves, **ctrl+u** empties the field, **esc** abandons.'
6399
6491
  : 'Up/down choose · enter change · x clear · left/right tabs');
6400
- lines.push(`Saved to ${CONFIG_FILE}`);
6492
+ if (!compact)
6493
+ lines.push(`Saved to ${CONFIG_FILE}`);
6401
6494
  }
6402
6495
  if (usagePane === 'session') {
6403
6496
  const wall = Date.now() - sessionStartRef.current;
@@ -6579,7 +6672,7 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
6579
6672
  // footer. They consume six rows and the picker itself has six non-choice rows (margin,
6580
6673
  // border, title/search and pager), so reserve thirteen in total. Letting 16 choices into
6581
6674
  // a 24-row frame made Ink reflow the live region and visibly blink on every keypress.
6582
- const pickerRows = pickerRowLimit(picker.kind, list.length, ...(altScreen ? [termRows, 13] : []));
6675
+ const pickerRows = pickerRowLimit(picker.kind, list.length, termRows, altScreen ? 13 : undefined);
6583
6676
  // Keep the highlighted row inside the visible window as the selection moves.
6584
6677
  const start = Math.max(0, Math.min(pickIndex - Math.floor(pickerRows / 2), list.length - pickerRows));
6585
6678
  const shown = list.slice(Math.max(0, start), Math.max(0, start) + pickerRows);
@@ -6671,17 +6764,17 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
6671
6764
  return (_jsx(Box, { marginTop: 1, children: _jsx(Panel, { width: panelWidth(lines, barWidth, 46), color: AMBER, title: `Permission — ${modeSpec(mode).label} mode`, children: lines }) }));
6672
6765
  })()] }), sidePanelOpen && (_jsx(Box, { flexDirection: "column", width: diffPanelWidth, flexShrink: 0, children: diffPanelOpen && _jsx(DiffPanel, { files: liveDiffs, width: diffPanelWidth, height: Math.max(6, frameRows - 6), activityRows: activityPanelOpen ? 4 : 0, activity: activityPanelOpen ? _jsxs(_Fragment, { children: [_jsxs(Text, { color: CYAN, bold: true, children: ["Activity \u00B7 ", fmtElapsed(busyStart.current ? Date.now() - busyStart.current : 0)] }), _jsx(Text, { color: AMBER, wrap: "truncate", children: ask ? 'Waiting for your approval' : liveToolRef.current
6673
6766
  ? 'Running ' + liveToolRef.current.name : thinkingRef.current.chars > 0
6674
- ? 'Waiting for model · reasoning received' : 'Waiting for model' }), _jsx(Text, { color: MUTED, wrap: "truncate", children: liveToolRef.current?.detail || plan?.steps.find(s => s.state !== 'done')?.text || 'esc to interrupt' })] }) : undefined }) }))] }), _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, " "] }), (() => {
6675
- const sel = selectionRange(selAnchor, caret, draft.length);
6676
- if (sel) {
6677
- return (_jsxs(_Fragment, { children: [_jsx(Text, { color: INK, children: draft.slice(0, sel[0]) }), _jsx(Text, { backgroundColor: CYAN, color: "#10222A", children: draft.slice(sel[0], sel[1]) }), _jsx(Text, { color: INK, children: draft.slice(sel[1]) })] }));
6678
- }
6679
- return (_jsxs(_Fragment, { children: [_jsx(Text, { color: INK, children: draft.slice(0, caret) }), busy
6680
- ? _jsx(Text, { color: INK, children: draft.slice(caret) })
6681
- : caret < draft.length
6682
- ? _jsxs(_Fragment, { children: [_jsx(Text, { backgroundColor: CYAN, color: "#10222A", children: draft[caret] }), _jsx(Text, { color: INK, children: draft.slice(caret + 1) })] })
6683
- : _jsx(Text, { color: CYAN, children: "\u2588" })] }));
6684
- })()] }), copyNotice && _jsx(Text, { color: CYAN, children: copyNotice }), _jsx(Box, { marginTop: 1, paddingX: 1, width: uiWidth - 4, 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"] })] })), ' '.repeat(uiWidth)] }) })] }));
6767
+ ? 'Waiting for model · reasoning received' : 'Waiting for model' }), _jsx(Text, { color: MUTED, wrap: "truncate", children: liveToolRef.current?.detail || plan?.steps.find(s => s.state !== 'done')?.text || 'esc to interrupt' })] }) : undefined }) }))] }), _jsx(Box, { marginTop: 1, borderStyle: "round", borderColor: busy ? DIM : CYAN, paddingX: 1, width: uiWidth - 5, flexShrink: 0, children: _jsxs(Text, { wrap: "wrap", children: [_jsxs(Text, { color: CYAN, bold: true, children: [G.caret, " "] }), (() => {
6768
+ const sel = selectionRange(selAnchor, caret, draft.length);
6769
+ if (sel) {
6770
+ return (_jsxs(_Fragment, { children: [_jsx(Text, { color: INK, children: draft.slice(0, sel[0]) }), _jsx(Text, { backgroundColor: CYAN, color: "#10222A", children: draft.slice(sel[0], sel[1]) }), _jsx(Text, { color: INK, children: draft.slice(sel[1]) })] }));
6771
+ }
6772
+ return (_jsxs(_Fragment, { children: [_jsx(Text, { color: INK, children: draft.slice(0, caret) }), busy
6773
+ ? _jsx(Text, { color: INK, children: draft.slice(caret) })
6774
+ : caret < draft.length
6775
+ ? _jsxs(_Fragment, { children: [_jsx(Text, { backgroundColor: CYAN, color: "#10222A", children: draft[caret] }), _jsx(Text, { color: INK, children: draft.slice(caret + 1) })] })
6776
+ : _jsx(Text, { color: CYAN, children: "\u2588" })] }));
6777
+ })()] }) }), copyNotice && _jsx(Text, { color: CYAN, children: copyNotice }), _jsx(Box, { marginTop: 1, paddingX: 1, width: uiWidth - 4, 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 }), autoUpdated && (_jsxs(_Fragment, { children: [_jsx(Text, { color: MUTED, children: " / " }), _jsxs(Text, { color: GREEN, children: [G.ok, " Update installed \u00B7 restart to use it"] })] })), 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"] })] })), ' '.repeat(uiWidth)] }) })] }));
6685
6778
  }
6686
6779
  export async function runInkChatMode(config) {
6687
6780
  // Nothing is written before the first frame. Filling the screen to push the prompt to the foot