koneck 2.25.16 → 2.25.18

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
@@ -6,7 +6,7 @@ import { createAgentSession, buildClient, resolveAgentConcurrency } from './engi
6
6
  import { estimateCost, formatCost } from './pricing.js';
7
7
  import { generateSessionId, saveSession, listSessions, loadSession } from './session.js';
8
8
  import { loadMemory } from './memory.js';
9
- import { loadKoneckConfig, saveKoneckConfig, setConfigKey, configKeyDescriptions, CONFIG_FILE } from './config-store.js';
9
+ import { loadKoneckConfig, saveKoneckConfig, setConfigKey, configKeyDescriptions, CONFIG_FILE, CONFIG_SCHEMA, stepValue, displayValue } from './config-store.js';
10
10
  import { PROVIDERS, resolveProvider, getApiKey, addUserProvider, userProvidersPath } from './providers.js';
11
11
  import { listCheckpoints, revertCheckpoint, snapshotTree, filesTouchedBy } from './checkpoint.js';
12
12
  import { fileDiff, diffSummary } from './diff-view.js';
@@ -418,7 +418,7 @@ const MAX_DIFF_BYTES = 512 * 1024;
418
418
  /** One tool rarely touches more; the cap stops a bulk move from flooding scrollback. */
419
419
  const MAX_DIFF_FILES = 4;
420
420
  /** The tabs of the usage pane, in the order they are shown. */
421
- const USAGE_TABS = ['session', 'context', 'models', 'limits'];
421
+ const USAGE_TABS = ['settings', 'session', 'context', 'models', 'limits'];
422
422
  const MIN_REPLY_WIDTH = 24; // below this a table cannot render legibly anyway
423
423
  const PICKER_ROWS = 9; // visible rows; the list scrolls within this window
424
424
  /**
@@ -508,6 +508,8 @@ function App({ config: initialConfig }) {
508
508
  * token — the animation tick is what paints it, not a re-render per delta.
509
509
  */
510
510
  const thinkingRef = useRef({ chars: 0, at: 0 });
511
+ /** Why the current turn stopped short, when it did. Cleared at the start of each turn. */
512
+ const incompleteRef = useRef(null);
511
513
  /**
512
514
  * Text pasted into the composer, held aside so the prompt stays readable.
513
515
  *
@@ -665,6 +667,7 @@ function App({ config: initialConfig }) {
665
667
  liveToolRef.current.output = line;
666
668
  },
667
669
  onStreamActivity: (frames) => setProviderFrames(frames),
670
+ onIncomplete: (reason) => { incompleteRef.current = reason; },
668
671
  onThinking: (_text, totalChars) => {
669
672
  thinkingRef.current = { chars: totalChars, at: Date.now() };
670
673
  lastActivityRef.current = Date.now();
@@ -702,6 +705,12 @@ function App({ config: initialConfig }) {
702
705
  /** Tree hash captured at startup, so "code changes" is a diff rather than a running tally. */
703
706
  const baseTreeRef = useRef(null);
704
707
  const [usagePane, setUsagePane] = useState(null);
708
+ /** Which setting is highlighted on the Settings tab. */
709
+ const [settingRow, setSettingRow] = useState(0);
710
+ /** Stored config as the editor sees it — read when the pane opens, written as it is edited. */
711
+ const [stored, setStored] = useState({});
712
+ /** Non-null while a value is being typed rather than stepped. */
713
+ const [editing, setEditing] = useState(null);
705
714
  // undefined while the diff is still being computed, null once git has said there is nothing to
706
715
  // compare against. Collapsing the two would flash "not a git repo" at every open.
707
716
  const [usageDiff, setUsageDiff] = useState(undefined);
@@ -1013,10 +1022,48 @@ function App({ config: initialConfig }) {
1013
1022
  */
1014
1023
  async function openUsagePane(tab) {
1015
1024
  setUsagePane(tab);
1025
+ setEditing(null);
1026
+ void loadKoneckConfig().then(c => setStored(c));
1016
1027
  void diffStatSince(cfg.cwd, baseTreeRef.current).then(setUsageDiff);
1017
1028
  if (modelCatalog.length === 0)
1018
1029
  void loadModelCatalog();
1019
1030
  }
1031
+ /**
1032
+ * Writes one setting and makes it live.
1033
+ *
1034
+ * Persisting alone would leave the running session on the old value until restart, which is the
1035
+ * behaviour that made the old `/config` feel inert even when it had worked.
1036
+ */
1037
+ async function applySetting(field, value) {
1038
+ const next = { ...stored, [field.key]: value };
1039
+ if (value === undefined)
1040
+ delete next[field.key];
1041
+ setStored(next);
1042
+ await saveKoneckConfig(next);
1043
+ switch (field.key) {
1044
+ case 'effort':
1045
+ setEffort((typeof value === 'string' ? value : 'medium'));
1046
+ break;
1047
+ case 'maxWidth':
1048
+ setConfigMaxWidth(typeof value === 'number' && value >= 40 ? value : undefined);
1049
+ break;
1050
+ case 'model':
1051
+ case 'provider':
1052
+ case 'baseURL':
1053
+ case 'maxTurns':
1054
+ case 'requireApproval':
1055
+ case 'agentConcurrency':
1056
+ case 'budget': {
1057
+ // These shape the session itself, so it is rebuilt on the new values.
1058
+ const merged = { ...cfg, [field.key]: value };
1059
+ setCfg(merged);
1060
+ await resetSession(merged);
1061
+ break;
1062
+ }
1063
+ default:
1064
+ break;
1065
+ }
1066
+ }
1020
1067
  /** The provider's own model listing, kept so the models tab and picker agree on the facts. */
1021
1068
  async function loadModelCatalog() {
1022
1069
  try {
@@ -1464,6 +1511,12 @@ function App({ config: initialConfig }) {
1464
1511
  }
1465
1512
  case '/config': {
1466
1513
  const parts = arg.split(/\s+/).filter(Boolean);
1514
+ // Bare `/config` opens the editor. The `key value` form still works, because it is what
1515
+ // scripts and muscle memory use — printing a table was never the useful half.
1516
+ if (parts.length === 0) {
1517
+ await openUsagePane('settings');
1518
+ return;
1519
+ }
1467
1520
  const key = parts[0];
1468
1521
  const val = parts.slice(1).join(' ');
1469
1522
  if (key === 'reset') {
@@ -1924,6 +1977,49 @@ function App({ config: initialConfig }) {
1924
1977
  // Inside the usage pane: the arrows walk the tabs and escape closes it. This sits ahead of
1925
1978
  // the composer's own arrow handling, which would otherwise move the caret behind the pane.
1926
1979
  if (usagePane) {
1980
+ // Typing a value owns the keyboard entirely, so a stray arrow cannot change tabs midway
1981
+ // through an edit and lose what was typed.
1982
+ if (editing) {
1983
+ const field = CONFIG_SCHEMA.find(f => f.key === editing.key);
1984
+ if (key.escape) {
1985
+ setEditing(null);
1986
+ return;
1987
+ }
1988
+ if (key.return) {
1989
+ const raw = editing.value.trim();
1990
+ setEditing(null);
1991
+ if (!field)
1992
+ return;
1993
+ if (raw === '') {
1994
+ if (field.clearable)
1995
+ void applySetting(field, undefined);
1996
+ return;
1997
+ }
1998
+ if (field.type.kind === 'number') {
1999
+ const n = Number(raw);
2000
+ if (!Number.isFinite(n))
2001
+ return; // ignore rather than store junk
2002
+ const { min, max } = field.type;
2003
+ const clamped = Math.min(max ?? Infinity, Math.max(min ?? -Infinity, n));
2004
+ void applySetting(field, clamped);
2005
+ }
2006
+ else {
2007
+ void applySetting(field, raw);
2008
+ }
2009
+ return;
2010
+ }
2011
+ if (key.backspace || key.delete) {
2012
+ setEditing(e => (e ? { ...e, value: e.value.slice(0, -1) } : e));
2013
+ return;
2014
+ }
2015
+ if (!key.ctrl && !key.meta && input) {
2016
+ // eslint-disable-next-line no-control-regex
2017
+ const text = input.replace(/[\u0000-\u001f\u007f]/g, '');
2018
+ if (text)
2019
+ setEditing(e => (e ? { ...e, value: e.value + text } : e));
2020
+ }
2021
+ return;
2022
+ }
1927
2023
  if (key.escape) {
1928
2024
  setUsagePane(null);
1929
2025
  return;
@@ -1931,9 +2027,43 @@ function App({ config: initialConfig }) {
1931
2027
  if (key.leftArrow || key.rightArrow) {
1932
2028
  const at = USAGE_TABS.indexOf(usagePane);
1933
2029
  const next = (at + (key.rightArrow ? 1 : USAGE_TABS.length - 1)) % USAGE_TABS.length;
2030
+ setSettingRow(0);
1934
2031
  void openUsagePane(USAGE_TABS[next]);
1935
2032
  return;
1936
2033
  }
2034
+ if (usagePane === 'settings') {
2035
+ const field = CONFIG_SCHEMA[settingRow];
2036
+ if (key.downArrow) {
2037
+ setSettingRow(r => Math.min(r + 1, CONFIG_SCHEMA.length - 1));
2038
+ return;
2039
+ }
2040
+ if (key.upArrow) {
2041
+ setSettingRow(r => Math.max(r - 1, 0));
2042
+ return;
2043
+ }
2044
+ if (!field)
2045
+ return;
2046
+ // Enter acts: a boolean flips, a choice advances, anything typed opens for typing.
2047
+ if (key.return || input === ' ') {
2048
+ if (field.type.kind === 'boolean' || field.type.kind === 'choice') {
2049
+ void applySetting(field, stepValue(field, stored[field.key], 1));
2050
+ }
2051
+ else {
2052
+ setEditing({ key: String(field.key), value: '' });
2053
+ }
2054
+ return;
2055
+ }
2056
+ // A digit starts typing a number straight away, without a separate "edit" gesture.
2057
+ if (field.type.kind === 'number' && /^[0-9]$/.test(input)) {
2058
+ setEditing({ key: String(field.key), value: input });
2059
+ return;
2060
+ }
2061
+ if (input === 'x' && field.clearable) {
2062
+ void applySetting(field, undefined);
2063
+ return;
2064
+ }
2065
+ return;
2066
+ }
1937
2067
  }
1938
2068
  // Shift+U → the update and guide panel.
1939
2069
  if (key.shift && (input === 'U' || input === 'u')) {
@@ -2142,6 +2272,7 @@ function App({ config: initialConfig }) {
2142
2272
  streamedRef.current = '';
2143
2273
  replyStartedRef.current = false;
2144
2274
  thinkingRef.current = { chars: 0, at: 0 };
2275
+ incompleteRef.current = null;
2145
2276
  lastActivityRef.current = Date.now();
2146
2277
  setProviderFrames(0);
2147
2278
  liveToolRef.current = null;
@@ -2212,6 +2343,11 @@ function App({ config: initialConfig }) {
2212
2343
  recovered: turnRecoveredRef.current,
2213
2344
  }, turnWordRef.current)[1] },
2214
2345
  ]);
2346
+ // A run that stopped short says so, rather than letting the footer imply it finished.
2347
+ if (incompleteRef.current) {
2348
+ addSystem(incompleteRef.current);
2349
+ incompleteRef.current = null;
2350
+ }
2215
2351
  setAgentState('completed');
2216
2352
  setTimeout(() => setAgentState('ready'), 700);
2217
2353
  }
@@ -2359,13 +2495,48 @@ function App({ config: initialConfig }) {
2359
2495
  : 'starting', thinkingRef.current.chars > 0 && Date.now() - thinkingRef.current.at < 2_000
2360
2496
  ? ` | thinking, ${fmtTokens(Math.round(thinkingRef.current.chars / 4))} reasoning tokens`
2361
2497
  : '', ")"] })] }) }), _jsx(Box, { paddingLeft: 2, children: _jsx(Text, { color: DIM, children: "esc to cancel and keep your input \u00B7 ctrl+c to exit" }) })] })) }), usagePane && (() => {
2362
- const width = Math.min(barWidth, 66);
2498
+ const width = Math.min(barWidth, usagePane === 'settings' ? 96 : 66);
2363
2499
  const inner = width - 4;
2364
2500
  const BAR_W = Math.max(12, inner - 14);
2365
2501
  const tabs = USAGE_TABS.map(t => t === usagePane ? `**${t[0].toUpperCase() + t.slice(1)}**` : t[0].toUpperCase() + t.slice(1)).join(' ');
2366
2502
  /** A labelled bar with its percentage, the shape used for every proportion here. */
2367
2503
  const meter = (fraction, label) => [`${bar(fraction, BAR_W)} ${Math.round(fraction * 100)}%`, `${label}`, ''];
2368
2504
  const lines = [tabs, '---'];
2505
+ if (usagePane === 'settings') {
2506
+ lines.push('**Settings** — written to disk and applied now', '');
2507
+ // What is actually in force, not just what this file happens to hold. provider and
2508
+ // baseURL commonly come from ~/.koneckrc, and showing those as "(not set)" made a
2509
+ // configured session look unconfigured.
2510
+ const live = {
2511
+ provider: cfg.provider, model: cfg.model, baseURL: cfg.baseURL,
2512
+ effort, requireApproval: cfg.requireApproval, maxTurns: cfg.maxTurns,
2513
+ agentConcurrency: cfg.agentConcurrency, maxWidth: configMaxWidth, budget: cfg.budget,
2514
+ };
2515
+ const labelW = Math.max(...CONFIG_SCHEMA.map(f => f.label.length));
2516
+ for (const [i, f] of CONFIG_SCHEMA.entries()) {
2517
+ const selected = i === settingRow;
2518
+ const beingTyped = editing?.key === String(f.key);
2519
+ const here = stored[f.key] !== undefined;
2520
+ const value = here ? stored[f.key] : live[f.key];
2521
+ const shown = beingTyped
2522
+ ? `${editing.value}_`
2523
+ : displayValue(value) + (here || value === undefined ? '' : ' (inherited)');
2524
+ // The marker is what makes the selection legible without colour, which a panel line
2525
+ // drawn as one string cannot carry per-segment.
2526
+ const mark = selected ? '>' : ' ';
2527
+ const hint = !selected ? ''
2528
+ : f.type.kind === 'boolean' ? ' enter toggles'
2529
+ : f.type.kind === 'choice' ? ` enter cycles: ${f.type.choices.join(' / ')}`
2530
+ : f.type.kind === 'number' ? ` type a number, enter to save${f.clearable ? ', x clears' : ''}`
2531
+ : ` enter to type a value${f.clearable ? ', x clears' : ''}`;
2532
+ lines.push(`${mark} ${f.label.padEnd(labelW)} ${selected ? '**' + shown + '**' : shown}${hint}`);
2533
+ }
2534
+ lines.push('');
2535
+ lines.push(editing
2536
+ ? 'Typing. **enter** saves, **esc** abandons.'
2537
+ : 'Up/down choose · enter change · x clear · left/right tabs');
2538
+ lines.push(`Saved to ${CONFIG_FILE}`);
2539
+ }
2369
2540
  if (usagePane === 'session') {
2370
2541
  const wall = Date.now() - sessionStartRef.current;
2371
2542
  const cost = estimateCost(cfg.model, stats.promptTokens, stats.completionTokens);