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/config-store.d.ts +40 -0
- package/dist/config-store.d.ts.map +1 -1
- package/dist/config-store.js +56 -0
- package/dist/config-store.js.map +1 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +22 -9
- package/dist/engine.js.map +1 -1
- package/dist/ink-chat.d.ts.map +1 -1
- package/dist/ink-chat.js +174 -3
- package/dist/ink-chat.js.map +1 -1
- package/dist/types.d.ts +9 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
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);
|