koneck 2.25.60 → 2.25.62
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 +8 -0
- package/dist/config-store.d.ts.map +1 -1
- package/dist/config-store.js +23 -2
- package/dist/config-store.js.map +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/endpoints.d.ts +79 -0
- package/dist/endpoints.d.ts.map +1 -0
- package/dist/endpoints.js +175 -0
- package/dist/endpoints.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -1
- package/dist/index.js.map +1 -1
- package/dist/ink-chat.d.ts.map +1 -1
- package/dist/ink-chat.js +238 -21
- package/dist/ink-chat.js.map +1 -1
- package/dist/logo.d.ts +11 -11
- package/dist/logo.d.ts.map +1 -1
- package/dist/logo.js +23 -29
- package/dist/logo.js.map +1 -1
- package/package.json +1 -1
package/dist/ink-chat.js
CHANGED
|
@@ -8,6 +8,8 @@ import { generateSessionId, saveSession, listSessions, loadSession, relativeAge,
|
|
|
8
8
|
import { loadMemory } from './memory.js';
|
|
9
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
|
+
import { resolveEndpoint, normaliseEndpoint, withEndpoint, savedEndpoints, shortSource } from './endpoints.js';
|
|
12
|
+
import { loadProjectConfig, loadGlobalConfig } from './config.js';
|
|
11
13
|
import { listCheckpoints, revertCheckpoint, snapshotTree, filesTouchedBy } from './checkpoint.js';
|
|
12
14
|
import { fileDiff, diffSummary } from './diff-view.js';
|
|
13
15
|
import { listWorkspaceEntries, filterEntries, activeReference } from './workspace-files.js';
|
|
@@ -498,6 +500,8 @@ const COMMANDS = [
|
|
|
498
500
|
{ cmd: '/help', desc: 'Show help' },
|
|
499
501
|
{ cmd: '/models', desc: 'Browse and switch models for the connected provider' },
|
|
500
502
|
{ cmd: '/connect', desc: 'Connect a provider, or declare a new one with a URL' },
|
|
503
|
+
{ cmd: '/endpoint', desc: "Show or change this provider's URL, e.g. a remote Ollama" },
|
|
504
|
+
{ cmd: '/redraw', desc: 'Repaint the terminal — same as ctrl+L' },
|
|
501
505
|
{ cmd: '/key', desc: 'Enter an API key for the current provider' },
|
|
502
506
|
{ cmd: '/model', desc: 'Show or set the model by name' },
|
|
503
507
|
{ cmd: '/provider', desc: 'Show or switch provider' },
|
|
@@ -706,7 +710,7 @@ export function pickerLabelWidth(labels, descs, width) {
|
|
|
706
710
|
*/
|
|
707
711
|
const COMMANDS_THAT_MUST_WAIT = new Set([
|
|
708
712
|
'/clear', '/reset', '/new', // would wipe the history the turn is using
|
|
709
|
-
'/model', '/provider', '/connect', '/key', // rebuild the session
|
|
713
|
+
'/model', '/provider', '/connect', '/key', '/endpoint', '/baseurl', // rebuild the session
|
|
710
714
|
'/resume', '/fork', // replace it
|
|
711
715
|
'/revert', // undo files the turn may still be writing
|
|
712
716
|
'/init', '/setup',
|
|
@@ -730,6 +734,10 @@ Slash commands:
|
|
|
730
734
|
/connect [provider] Connect a provider (picker when no name given)
|
|
731
735
|
/connect <name> <url> Declare a provider KONECK does not ship with, e.g.
|
|
732
736
|
/connect openrouter https://openrouter.ai/api/v1
|
|
737
|
+
/redraw Repaint the terminal (ctrl+L) if a stale frame is left on screen
|
|
738
|
+
/endpoint Show the endpoint in force and where it came from
|
|
739
|
+
/endpoint <url> Point this provider somewhere else, e.g.
|
|
740
|
+
/endpoint http://192.168.1.50:11434/v1
|
|
733
741
|
/key Enter an API key for the current provider
|
|
734
742
|
/status Provider, model, mode, tokens, cost, CWD
|
|
735
743
|
/clear /reset /new Clear conversation history
|
|
@@ -1069,6 +1077,8 @@ function App({ config: initialConfig }) {
|
|
|
1069
1077
|
* reading something further up is not yanked away by the next tool call.
|
|
1070
1078
|
*/
|
|
1071
1079
|
const [scrollBack, setScrollBack] = useState(0);
|
|
1080
|
+
/** Bumped to make Ink write a frame whose content has not changed. See redrawScreen. */
|
|
1081
|
+
const [repaint, setRepaint] = useState(0);
|
|
1072
1082
|
const [ask, setAsk] = useState(null);
|
|
1073
1083
|
useEffect(() => {
|
|
1074
1084
|
if (!process.stdout.isTTY)
|
|
@@ -1186,6 +1196,28 @@ function App({ config: initialConfig }) {
|
|
|
1186
1196
|
setConfigMaxWidth(typeof c.maxWidth === 'number' && c.maxWidth >= 40 ? c.maxWidth : undefined);
|
|
1187
1197
|
});
|
|
1188
1198
|
}, []);
|
|
1199
|
+
/**
|
|
1200
|
+
* The config layers other than the store, kept so an endpoint can be resolved and, more to the
|
|
1201
|
+
* point, explained. "It still sticks to the already set ollama" is a question about which file
|
|
1202
|
+
* won, and the answer is not available from the store alone.
|
|
1203
|
+
*/
|
|
1204
|
+
const [outerLayers, setOuterLayers] = useState({ project: {}, global: {} });
|
|
1205
|
+
useEffect(() => {
|
|
1206
|
+
void Promise.all([loadProjectConfig(cfg.cwd), loadGlobalConfig()])
|
|
1207
|
+
.then(([project, global]) => setOuterLayers({ project, global }))
|
|
1208
|
+
.catch(() => { });
|
|
1209
|
+
}, [cfg.cwd]);
|
|
1210
|
+
/** Nearest first, which is the order resolveEndpoint reads them in. */
|
|
1211
|
+
const endpointLayers = (store = stored) => [outerLayers.project, store, outerLayers.global];
|
|
1212
|
+
/** The endpoint in force for a provider, with where it came from. */
|
|
1213
|
+
function endpointFor(provider, store) {
|
|
1214
|
+
const def = PROVIDERS[provider.trim().toLowerCase()];
|
|
1215
|
+
return resolveEndpoint(provider, endpointLayers(store), {
|
|
1216
|
+
fallback: def?.baseURL ?? '',
|
|
1217
|
+
baseURLEnv: def?.baseURLEnv,
|
|
1218
|
+
fallbackIsUserDeclared: def?.userDefined === true,
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1189
1221
|
useEffect(() => {
|
|
1190
1222
|
const onResize = () => {
|
|
1191
1223
|
setTermCols(process.stdout.columns || 80);
|
|
@@ -1387,6 +1419,30 @@ function App({ config: initialConfig }) {
|
|
|
1387
1419
|
if (newCfg)
|
|
1388
1420
|
setCfg(newCfg);
|
|
1389
1421
|
}
|
|
1422
|
+
/**
|
|
1423
|
+
* Repaint the terminal from scratch.
|
|
1424
|
+
*
|
|
1425
|
+
* Reported as KONECK looking "split into two parts, two message input box": a previous frame
|
|
1426
|
+
* left on screen above the live one, with the transcript written in between. Ink erases the
|
|
1427
|
+
* lines it believes it wrote, and any line the terminal wrapped occupies two rows while
|
|
1428
|
+
* counting as one — so the erase falls short and the old frame is stranded. It cannot be
|
|
1429
|
+
* unstranded from inside the frame, because Ink is not aware it is there.
|
|
1430
|
+
*
|
|
1431
|
+
* Ctrl+L is what everybody presses when a terminal looks wrong, and KONECK had no binding for
|
|
1432
|
+
* it at all. It wipes the screen and its scrollback; the conversation is untouched, since it
|
|
1433
|
+
* lives in the session rather than in the terminal's memory of it.
|
|
1434
|
+
*/
|
|
1435
|
+
function redrawScreen() {
|
|
1436
|
+
if (!process.stdout.isTTY)
|
|
1437
|
+
return;
|
|
1438
|
+
// Clear the screen, clear the scrollback with it — a stranded frame that has scrolled out of
|
|
1439
|
+
// view is still in the buffer, and leaving it there means it reappears on the next scroll —
|
|
1440
|
+
// then home the cursor so Ink's next frame starts from a known position.
|
|
1441
|
+
process.stdout.write('\u001b[2J\u001b[3J\u001b[H');
|
|
1442
|
+
// Ink writes a frame only when its content differs from the last one, and the content has
|
|
1443
|
+
// not changed — the screen it was drawn on has. Bumping this forces the write.
|
|
1444
|
+
setRepaint(n => n + 1);
|
|
1445
|
+
}
|
|
1390
1446
|
async function doSave() {
|
|
1391
1447
|
const s = await activeSession.p.catch(() => null);
|
|
1392
1448
|
const firstUser = s?.messages.find((m) => m.role === 'user');
|
|
@@ -1529,7 +1585,58 @@ function App({ config: initialConfig }) {
|
|
|
1529
1585
|
* Persisting alone would leave the running session on the old value until restart, which is the
|
|
1530
1586
|
* behaviour that made the old `/config` feel inert even when it had worked.
|
|
1531
1587
|
*/
|
|
1588
|
+
/**
|
|
1589
|
+
* What a setting is worth right now, wherever the value came from.
|
|
1590
|
+
*
|
|
1591
|
+
* The store is only one of three files. Reading a row from it alone showed "(not set)" beside a
|
|
1592
|
+
* session that was plainly configured, and — with endpoints saved per provider — would show
|
|
1593
|
+
* nothing at all for an endpoint that is in force.
|
|
1594
|
+
*/
|
|
1595
|
+
function liveSetting(key) {
|
|
1596
|
+
switch (key) {
|
|
1597
|
+
case 'provider': return cfg.provider;
|
|
1598
|
+
case 'model': return cfg.model;
|
|
1599
|
+
case 'baseURL': return endpointFor(cfg.provider).url;
|
|
1600
|
+
case 'effort': return effort;
|
|
1601
|
+
case 'requireApproval': return cfg.requireApproval;
|
|
1602
|
+
case 'maxTurns': return cfg.maxTurns;
|
|
1603
|
+
case 'agentConcurrency': return cfg.agentConcurrency;
|
|
1604
|
+
case 'maxWidth': return configMaxWidth;
|
|
1605
|
+
case 'budget': return cfg.budget;
|
|
1606
|
+
default: return stored[key];
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
/** The text an edit box opens on: what the row is showing. */
|
|
1610
|
+
function settingText(field) {
|
|
1611
|
+
const key = String(field.key);
|
|
1612
|
+
const value = key === 'baseURL' || stored[key] === undefined ? liveSetting(key) : stored[key];
|
|
1613
|
+
return value === undefined || value === null ? '' : String(value);
|
|
1614
|
+
}
|
|
1532
1615
|
async function applySetting(field, value) {
|
|
1616
|
+
// The endpoint row is not a plain setting: it is one provider's address, so it is saved
|
|
1617
|
+
// against the provider in force rather than into a single slot every provider shares.
|
|
1618
|
+
if (field.key === 'baseURL') {
|
|
1619
|
+
let url;
|
|
1620
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
1621
|
+
const checked = normaliseEndpoint(value);
|
|
1622
|
+
if (!checked.url) {
|
|
1623
|
+
addSystem(checked.problem ?? `"${value}" is not a URL.`);
|
|
1624
|
+
return;
|
|
1625
|
+
}
|
|
1626
|
+
url = checked.url;
|
|
1627
|
+
if (checked.note)
|
|
1628
|
+
addSystem(checked.note);
|
|
1629
|
+
}
|
|
1630
|
+
const saved = withEndpoint(await loadKoneckConfig(), cfg.provider, url);
|
|
1631
|
+
await saveKoneckConfig(saved);
|
|
1632
|
+
setStored(saved);
|
|
1633
|
+
const now = endpointFor(cfg.provider, saved);
|
|
1634
|
+
const merged = { ...cfg, baseURL: now.url };
|
|
1635
|
+
setCfg(merged);
|
|
1636
|
+
await resetSession(merged);
|
|
1637
|
+
addSystem(`**${cfg.provider}** → ${now.url} (${now.source})`);
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1533
1640
|
const next = { ...stored, [field.key]: value };
|
|
1534
1641
|
if (value === undefined)
|
|
1535
1642
|
delete next[field.key];
|
|
@@ -1542,9 +1649,21 @@ function App({ config: initialConfig }) {
|
|
|
1542
1649
|
case 'maxWidth':
|
|
1543
1650
|
setConfigMaxWidth(typeof value === 'number' && value >= 40 ? value : undefined);
|
|
1544
1651
|
break;
|
|
1652
|
+
case 'provider': {
|
|
1653
|
+
// Changing the provider here has to move the endpoint with it, or the new provider is
|
|
1654
|
+
// addressed at the old one's URL — the same fault /provider had.
|
|
1655
|
+
const name = typeof value === 'string' && value.trim() !== '' ? value.trim() : cfg.provider;
|
|
1656
|
+
const def = PROVIDERS[name.toLowerCase()];
|
|
1657
|
+
const merged = {
|
|
1658
|
+
...cfg, provider: name,
|
|
1659
|
+
model: def?.defaultModel ?? cfg.model,
|
|
1660
|
+
baseURL: endpointFor(name, next).url,
|
|
1661
|
+
};
|
|
1662
|
+
setCfg(merged);
|
|
1663
|
+
await resetSession(merged);
|
|
1664
|
+
break;
|
|
1665
|
+
}
|
|
1545
1666
|
case 'model':
|
|
1546
|
-
case 'provider':
|
|
1547
|
-
case 'baseURL':
|
|
1548
1667
|
case 'maxTurns':
|
|
1549
1668
|
case 'requireApproval':
|
|
1550
1669
|
case 'agentConcurrency':
|
|
@@ -1745,7 +1864,11 @@ function App({ config: initialConfig }) {
|
|
|
1745
1864
|
}
|
|
1746
1865
|
if (kind === 'provider') {
|
|
1747
1866
|
const def = PROVIDERS[item.value];
|
|
1748
|
-
|
|
1867
|
+
// The endpoint saved for the provider being switched to, not its built-in address. Passing
|
|
1868
|
+
// it into resolveProvider also settles the credential question honestly: a remote Ollama is
|
|
1869
|
+
// not loopback and does need a key, whatever the provider is called.
|
|
1870
|
+
const endpoint = endpointFor(item.value);
|
|
1871
|
+
const resolved = resolveProvider(item.value, endpoint.url);
|
|
1749
1872
|
const next = {
|
|
1750
1873
|
...cfg, provider: item.value,
|
|
1751
1874
|
model: def?.defaultModel ?? cfg.model,
|
|
@@ -1763,7 +1886,11 @@ function App({ config: initialConfig }) {
|
|
|
1763
1886
|
}
|
|
1764
1887
|
if (usable) {
|
|
1765
1888
|
await resetSession(next);
|
|
1766
|
-
|
|
1889
|
+
const savedCfg = { ...(await loadKoneckConfig()), provider: item.value, model: next.model };
|
|
1890
|
+
await saveKoneckConfig(savedCfg);
|
|
1891
|
+
setStored(savedCfg);
|
|
1892
|
+
addSystem(`Provider → ${item.value} · model → ${next.model}\n` +
|
|
1893
|
+
`Endpoint ${resolved.baseURL} (${endpoint.source}) · session reset`);
|
|
1767
1894
|
return;
|
|
1768
1895
|
}
|
|
1769
1896
|
setCfg(next);
|
|
@@ -1853,10 +1980,78 @@ function App({ config: initialConfig }) {
|
|
|
1853
1980
|
addSystem(`Current provider: ${cfg.provider}\nAvailable: ${Object.keys(PROVIDERS).join(', ')}\nUsage: /provider <name>`);
|
|
1854
1981
|
return;
|
|
1855
1982
|
}
|
|
1856
|
-
const
|
|
1857
|
-
const
|
|
1983
|
+
const name = arg.trim().toLowerCase();
|
|
1984
|
+
const def = PROVIDERS[name];
|
|
1985
|
+
if (!def) {
|
|
1986
|
+
// Switching to a name that does not resolve used to reset the session anyway, leaving it
|
|
1987
|
+
// pointed at nothing and failing on the next message instead of on the command.
|
|
1988
|
+
addSystem(`Unknown provider "${arg}". Known: ${Object.keys(PROVIDERS).join(', ')}\n` +
|
|
1989
|
+
`Declare one: /connect ${name} https://endpoint/v1`);
|
|
1990
|
+
return;
|
|
1991
|
+
}
|
|
1992
|
+
// The endpoint is the provider's own, not a literal default: whatever was saved for it,
|
|
1993
|
+
// or its environment variable, still applies. Naming a provider used to overwrite the
|
|
1994
|
+
// address with PROVIDERS[name].baseURL, which is how a configured Ollama kept coming back
|
|
1995
|
+
// as localhost.
|
|
1996
|
+
const endpoint = endpointFor(name);
|
|
1997
|
+
const newCfg = { ...cfg, provider: name, model: def.defaultModel ?? cfg.model, baseURL: endpoint.url };
|
|
1858
1998
|
await resetSession(newCfg);
|
|
1859
|
-
|
|
1999
|
+
// Persisted, or the switch is undone by the next start and the old provider wins again.
|
|
2000
|
+
const savedCfg = { ...(await loadKoneckConfig()), provider: name, model: newCfg.model };
|
|
2001
|
+
await saveKoneckConfig(savedCfg);
|
|
2002
|
+
setStored(savedCfg);
|
|
2003
|
+
addSystem(`Provider → ${name} model → ${newCfg.model}\n` +
|
|
2004
|
+
`Endpoint ${endpoint.url} (${endpoint.source})\n` +
|
|
2005
|
+
`Session reset. /endpoint <url> changes the address.`);
|
|
2006
|
+
return;
|
|
2007
|
+
}
|
|
2008
|
+
case '/redraw': {
|
|
2009
|
+
redrawScreen();
|
|
2010
|
+
return;
|
|
2011
|
+
}
|
|
2012
|
+
case '/url':
|
|
2013
|
+
case '/baseurl':
|
|
2014
|
+
case '/endpoint': {
|
|
2015
|
+
const provider = cfg.provider;
|
|
2016
|
+
const current = endpointFor(provider);
|
|
2017
|
+
if (!arg.trim()) {
|
|
2018
|
+
// No argument is the diagnostic. The complaint that produced this command was about a
|
|
2019
|
+
// change that appeared not to take, and the only useful reply names the file that won.
|
|
2020
|
+
const all = savedEndpoints(endpointLayers());
|
|
2021
|
+
const others = Object.entries(all).filter(([name]) => name !== provider.toLowerCase());
|
|
2022
|
+
addSystem(`**${provider}** → ${current.url}\n` +
|
|
2023
|
+
`Source: ${current.source}\n\n` +
|
|
2024
|
+
(others.length
|
|
2025
|
+
? 'Also saved:\n' + others.map(([n, u]) => ` ${n.padEnd(12)} ${u}`).join('\n') + '\n\n'
|
|
2026
|
+
: '') +
|
|
2027
|
+
`Change it: /endpoint http://192.168.1.50:11434/v1\n` +
|
|
2028
|
+
`Undo it: /endpoint default`);
|
|
2029
|
+
return;
|
|
2030
|
+
}
|
|
2031
|
+
const wanted = arg.trim();
|
|
2032
|
+
if (/^(default|reset|clear|unset)$/i.test(wanted)) {
|
|
2033
|
+
const saved = withEndpoint(await loadKoneckConfig(), provider, undefined);
|
|
2034
|
+
await saveKoneckConfig(saved);
|
|
2035
|
+
setStored(saved);
|
|
2036
|
+
const back = endpointFor(provider, saved);
|
|
2037
|
+
await resetSession({ ...cfg, baseURL: back.url });
|
|
2038
|
+
addSystem(`**${provider}** → ${back.url} (${back.source})\nSession reset.`);
|
|
2039
|
+
return;
|
|
2040
|
+
}
|
|
2041
|
+
const { url, problem, note } = normaliseEndpoint(wanted);
|
|
2042
|
+
if (!url) {
|
|
2043
|
+
addSystem(`${problem}\nUsage: /endpoint http://host:port/v1`);
|
|
2044
|
+
return;
|
|
2045
|
+
}
|
|
2046
|
+
// Saved against the provider, not as a global address: setting Ollama's URL used to move
|
|
2047
|
+
// OmniRoute's too, and one of the two was then wrong whichever way round it was left.
|
|
2048
|
+
const saved = withEndpoint(await loadKoneckConfig(), provider, url);
|
|
2049
|
+
await saveKoneckConfig(saved);
|
|
2050
|
+
setStored(saved);
|
|
2051
|
+
await resetSession({ ...cfg, baseURL: url });
|
|
2052
|
+
addSystem(`**${provider}** → ${url}\n` +
|
|
2053
|
+
`Saved for ${provider} in ${CONFIG_FILE}; session reset.` +
|
|
2054
|
+
(note ? `\n${note}` : ''));
|
|
1860
2055
|
return;
|
|
1861
2056
|
}
|
|
1862
2057
|
case '/mode':
|
|
@@ -2510,6 +2705,12 @@ function App({ config: initialConfig }) {
|
|
|
2510
2705
|
void doSave().catch(() => { }).then(() => exit());
|
|
2511
2706
|
return;
|
|
2512
2707
|
}
|
|
2708
|
+
// Ctrl+L repaints, as it does everywhere else. The conversation is not touched — only the
|
|
2709
|
+
// terminal's picture of it, which is the thing that was wrong.
|
|
2710
|
+
if (key.ctrl && input === 'l') {
|
|
2711
|
+
redrawScreen();
|
|
2712
|
+
return;
|
|
2713
|
+
}
|
|
2513
2714
|
// ── Credential entry owns the keyboard, and echoes nothing ───────────────
|
|
2514
2715
|
// Scrolling the transcript, which on the alternate screen is the application's job: the
|
|
2515
2716
|
// terminal's own buffer is not there to fall back on.
|
|
@@ -2716,6 +2917,10 @@ function App({ config: initialConfig }) {
|
|
|
2716
2917
|
}
|
|
2717
2918
|
return;
|
|
2718
2919
|
}
|
|
2920
|
+
if (key.ctrl && input === 'u') {
|
|
2921
|
+
setEditing(e => (e ? { ...e, value: '' } : e));
|
|
2922
|
+
return;
|
|
2923
|
+
}
|
|
2719
2924
|
if (key.backspace || key.delete) {
|
|
2720
2925
|
setEditing(e => (e ? { ...e, value: e.value.slice(0, -1) } : e));
|
|
2721
2926
|
return;
|
|
@@ -2757,7 +2962,10 @@ function App({ config: initialConfig }) {
|
|
|
2757
2962
|
void applySetting(field, stepValue(field, stored[field.key], 1));
|
|
2758
2963
|
}
|
|
2759
2964
|
else {
|
|
2760
|
-
|
|
2965
|
+
// Opened on the current value rather than on an empty box. Editing a URL by retyping
|
|
2966
|
+
// all of it is not editing, and an empty box next to a value that is plainly set
|
|
2967
|
+
// reads as "this cannot be changed".
|
|
2968
|
+
setEditing({ key: String(field.key), value: settingText(field) });
|
|
2761
2969
|
}
|
|
2762
2970
|
return;
|
|
2763
2971
|
}
|
|
@@ -3129,7 +3337,16 @@ function App({ config: initialConfig }) {
|
|
|
3129
3337
|
// window empty. A cap is still available through `maxWidth` in config for anyone who
|
|
3130
3338
|
// prefers a narrow measure on a very wide screen, but nothing is capped by default.
|
|
3131
3339
|
const uiWidth = computeUiWidth(termCols, configMaxWidth);
|
|
3132
|
-
|
|
3340
|
+
// uiWidth minus the container's paddingX={2} on both sides, and then one column of slack.
|
|
3341
|
+
//
|
|
3342
|
+
// Without the slack a full-width box draws a line exactly as wide as the terminal. Ink counts
|
|
3343
|
+
// the lines it wrote in order to erase them next frame, and a line that the terminal wrapped
|
|
3344
|
+
// occupies two rows while counting as one — so the erase falls short and the previous frame is
|
|
3345
|
+
// stranded on screen. That is how a screen ends up showing two composers and two status bars.
|
|
3346
|
+
// One character of a different width than Ink assumed is enough to cause it, and East Asian
|
|
3347
|
+
// ambiguous glyphs (·, ↑↓, ⌕, –, all of which the UI uses) are rendered two cells wide by some
|
|
3348
|
+
// terminals and one by others. A spare column costs nothing and removes the whole class.
|
|
3349
|
+
const barWidth = uiWidth - 5;
|
|
3133
3350
|
// Space a rendered reply actually has: the bar, minus the response box border (2), its
|
|
3134
3351
|
// paddingX={1} (2), and the "ꓘK " prefix plus its gap (3). Getting this even one column
|
|
3135
3352
|
// too wide makes Ink reflow table rows and shatter their borders.
|
|
@@ -3268,23 +3485,23 @@ function App({ config: initialConfig }) {
|
|
|
3268
3485
|
// What is actually in force, not just what this file happens to hold. provider and
|
|
3269
3486
|
// baseURL commonly come from ~/.koneckrc, and showing those as "(not set)" made a
|
|
3270
3487
|
// configured session look unconfigured.
|
|
3271
|
-
const
|
|
3272
|
-
provider: cfg.provider, model: cfg.model, baseURL: cfg.baseURL,
|
|
3273
|
-
effort, requireApproval: cfg.requireApproval, maxTurns: cfg.maxTurns,
|
|
3274
|
-
agentConcurrency: cfg.agentConcurrency, maxWidth: configMaxWidth, budget: cfg.budget,
|
|
3275
|
-
};
|
|
3488
|
+
const endpoint = endpointFor(cfg.provider);
|
|
3276
3489
|
const labelW = Math.max(...CONFIG_SCHEMA.map(f => f.label.length));
|
|
3277
3490
|
for (const [i, f] of CONFIG_SCHEMA.entries()) {
|
|
3278
3491
|
const selected = i === settingRow;
|
|
3279
3492
|
const beingTyped = editing?.key === String(f.key);
|
|
3280
|
-
|
|
3281
|
-
|
|
3493
|
+
// An endpoint always has a source worth naming, so it never reads as unset or as
|
|
3494
|
+
// merely inherited: the question it answers is which file won.
|
|
3495
|
+
const isEndpoint = f.key === 'baseURL';
|
|
3496
|
+
const here = !isEndpoint && stored[f.key] !== undefined;
|
|
3497
|
+
const value = here ? stored[f.key] : liveSetting(String(f.key));
|
|
3282
3498
|
const raw = f.key === 'maxTurns' && (value === 0 || value === undefined)
|
|
3283
3499
|
? 'no limit'
|
|
3284
3500
|
: displayValue(value);
|
|
3285
3501
|
const shown = beingTyped
|
|
3286
3502
|
? `${editing.value}_`
|
|
3287
|
-
:
|
|
3503
|
+
: isEndpoint ? `${raw} (${shortSource(endpoint.source)})`
|
|
3504
|
+
: raw + (here || value === undefined ? '' : ' (inherited)');
|
|
3288
3505
|
// The marker is what makes the selection legible without colour, which a panel line
|
|
3289
3506
|
// drawn as one string cannot carry per-segment.
|
|
3290
3507
|
const mark = selected ? '>' : ' ';
|
|
@@ -3292,12 +3509,12 @@ function App({ config: initialConfig }) {
|
|
|
3292
3509
|
: f.type.kind === 'boolean' ? ' enter toggles'
|
|
3293
3510
|
: f.type.kind === 'choice' ? ` enter cycles: ${f.type.choices.join(' / ')}`
|
|
3294
3511
|
: f.type.kind === 'number' ? ` type a number, enter to save${f.clearable ? ', x clears' : ''}`
|
|
3295
|
-
: ` enter to
|
|
3512
|
+
: ` enter to edit${f.clearable ? ', x clears' : ''}`;
|
|
3296
3513
|
lines.push(`${mark} ${f.label.padEnd(labelW)} ${selected ? '**' + shown + '**' : shown}${hint}`);
|
|
3297
3514
|
}
|
|
3298
3515
|
lines.push('');
|
|
3299
3516
|
lines.push(editing
|
|
3300
|
-
? 'Typing. **enter** saves, **esc** abandons.'
|
|
3517
|
+
? 'Typing. **enter** saves, **ctrl+u** empties the field, **esc** abandons.'
|
|
3301
3518
|
: 'Up/down choose · enter change · x clear · left/right tabs');
|
|
3302
3519
|
lines.push(`Saved to ${CONFIG_FILE}`);
|
|
3303
3520
|
}
|
|
@@ -3507,7 +3724,7 @@ function App({ config: initialConfig }) {
|
|
|
3507
3724
|
? _jsx(Text, { color: INK, children: draft.slice(caret) })
|
|
3508
3725
|
: caret < draft.length
|
|
3509
3726
|
? _jsxs(_Fragment, { children: [_jsx(Text, { backgroundColor: CYAN, color: "#10222A", children: draft[caret] }), _jsx(Text, { color: INK, children: draft.slice(caret + 1) })] })
|
|
3510
|
-
: _jsx(Text, { color: CYAN, children: "\u2588" })] }), _jsx(Box, { marginTop: 1, paddingX: 1, children: _jsxs(Text, { wrap: "truncate", backgroundColor: "#24343B", children: [_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 }), _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"] })] }))] }) })] }));
|
|
3727
|
+
: _jsx(Text, { color: CYAN, children: "\u2588" })] }), _jsx(Box, { marginTop: 1, paddingX: 1, 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 }), _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"] })] }))] }) })] }));
|
|
3511
3728
|
}
|
|
3512
3729
|
export async function runInkChatMode(config) {
|
|
3513
3730
|
// Nothing is written before the first frame. Filling the screen to push the prompt to the foot
|