koneck 2.25.59 → 2.25.61
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 +72 -0
- package/dist/endpoints.d.ts.map +1 -0
- package/dist/endpoints.js +148 -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 +2 -0
- package/dist/ink-chat.d.ts.map +1 -1
- package/dist/ink-chat.js +310 -22
- package/dist/ink-chat.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 } 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';
|
|
@@ -270,6 +272,16 @@ function Shimmer({ text, frame, base }) {
|
|
|
270
272
|
export const WORK_WORDS = {
|
|
271
273
|
// No tool ran: it thought and answered. A cooking metaphor is wrong here — nothing was made.
|
|
272
274
|
reply: [
|
|
275
|
+
["Thinking it through", "Thought it through, sharp"],
|
|
276
|
+
["Consulting the elders", "Elders consulted"],
|
|
277
|
+
["Reading the room", "Room read"],
|
|
278
|
+
["Chewing it over", "Chewed it over"],
|
|
279
|
+
["Doing the maths", "Maths done"],
|
|
280
|
+
["Asking the rubber duck", "Duck consulted"],
|
|
281
|
+
["Squinting at it", "Squinted at it"],
|
|
282
|
+
["Joining the dots", "Dots joined"],
|
|
283
|
+
["Sleeping on it", "Slept on it"],
|
|
284
|
+
["Weighing both sides", "Both sides weighed"],
|
|
273
285
|
['Thinking', 'Thought it through'], ['Weighing it up', 'Weighed it up'],
|
|
274
286
|
['Talking it through', 'Talked it through'], ['Laying it out', 'Laid it out'],
|
|
275
287
|
['Reading you', 'Read you'], ['Working it out', 'Worked it out'],
|
|
@@ -279,6 +291,18 @@ export const WORK_WORDS = {
|
|
|
279
291
|
],
|
|
280
292
|
// Under ~3s. Snappy, almost dismissive — it barely counted as work.
|
|
281
293
|
quick: [
|
|
294
|
+
["Now-now", "Done now-now"],
|
|
295
|
+
["Ayoba", "Made it ayoba"],
|
|
296
|
+
["Kicking it", "Kicked"],
|
|
297
|
+
["Zap-zapping", "Zap-zapped"],
|
|
298
|
+
["Tapping the card", "Card tapped"],
|
|
299
|
+
["Popping the kettle on", "Kettle popped on"],
|
|
300
|
+
["Pulling an espresso", "Espresso pulled"],
|
|
301
|
+
["Catching the kombi", "Kombi caught"],
|
|
302
|
+
["Beating the robot", "Robot beaten"],
|
|
303
|
+
["Slipping it in", "Slipped in"],
|
|
304
|
+
["Grabbing a vetkoek", "Vetkoek grabbed"],
|
|
305
|
+
["Skipping the queue", "Queue skipped"],
|
|
282
306
|
['Sharp-sharp', 'Sharp-sharp'], ['Doing it now-now', 'Done now-now'],
|
|
283
307
|
['One-tapping', 'One-tapped'], ['No-scoping', 'No-scoped'],
|
|
284
308
|
['Speedrunning', 'Speedran'], ['Microwaving', 'Microwaved'],
|
|
@@ -296,6 +320,32 @@ export const WORK_WORDS = {
|
|
|
296
320
|
],
|
|
297
321
|
// The everyday middle. General flex and completion.
|
|
298
322
|
normal: [
|
|
323
|
+
["Stirring the sishwala", "Sishwala stirred"],
|
|
324
|
+
["Pounding tinkhobe", "Tinkhobe pounded"],
|
|
325
|
+
["Souring the emasi", "Emasi soured"],
|
|
326
|
+
["Brewing emahewu", "Emahewu brewed"],
|
|
327
|
+
["Frying magwinya", "Magwinya fried"],
|
|
328
|
+
["Rolling a bunny chow", "Bunny chow rolled"],
|
|
329
|
+
["Firing the shisanyama", "Shisanyama fired"],
|
|
330
|
+
["Stirring the potjie", "Potjie stirred"],
|
|
331
|
+
["Turning the pap", "Pap turned"],
|
|
332
|
+
["Dropping the log drum", "Log drum dropped"],
|
|
333
|
+
["Stewing jollof", "Jollof stewed"],
|
|
334
|
+
["Pounding fufu", "Fufu pounded"],
|
|
335
|
+
["Rolling injera", "Injera rolled"],
|
|
336
|
+
["Grinding berbere", "Berbere ground"],
|
|
337
|
+
["Wrapping dolmades", "Dolmades wrapped"],
|
|
338
|
+
["Folding dumplings", "Dumplings folded"],
|
|
339
|
+
["Rolling sushi", "Sushi rolled"],
|
|
340
|
+
["Proving the sourdough", "Sourdough proved"],
|
|
341
|
+
["Tempering chocolate", "Chocolate tempered"],
|
|
342
|
+
["Layering the lasagne", "Lasagne layered"],
|
|
343
|
+
["Negotiating with TypeScript", "TypeScript talked round"],
|
|
344
|
+
["Bribing the linter", "Linter bribed"],
|
|
345
|
+
["Blaming the cache", "Cache blamed"],
|
|
346
|
+
["Threading the needle", "Needle threaded"],
|
|
347
|
+
["Loading the kombi", "Kombi loaded"],
|
|
348
|
+
["Beating loadshedding", "Loadshedding beaten"],
|
|
299
349
|
['Cooking', 'Cooked lekker'], ['Baking', 'Baked'],
|
|
300
350
|
['Grilling', 'Grilled'], ['Sizzling', 'Sizzled'],
|
|
301
351
|
['Brewing', 'Brewed'], ['Chowing', 'Chowed'],
|
|
@@ -332,6 +382,24 @@ export const WORK_WORDS = {
|
|
|
332
382
|
],
|
|
333
383
|
// Over ~10s. Slow heat and visible effort — it earned the wait.
|
|
334
384
|
heavy: [
|
|
385
|
+
["Slow-braaiing", "Slow-braaied"],
|
|
386
|
+
["Burying the potjie", "Potjie unearthed"],
|
|
387
|
+
["Drying umncweba", "Umncweba dried"],
|
|
388
|
+
["Curing biltong", "Biltong cured"],
|
|
389
|
+
["Waiting for the kombi to fill", "Kombi finally filled"],
|
|
390
|
+
["Smoking brisket", "Brisket smoked"],
|
|
391
|
+
["Curing prosciutto", "Prosciutto cured"],
|
|
392
|
+
["Ageing the parmesan", "Parmesan aged"],
|
|
393
|
+
["Reducing the stock", "Stock reduced"],
|
|
394
|
+
["Confiting the duck", "Duck confited"],
|
|
395
|
+
["Fermenting the mageu", "Mageu fermented"],
|
|
396
|
+
["Digging the imbawula", "Imbawula stoked"],
|
|
397
|
+
["Walking to Mbabane", "Walked to Mbabane"],
|
|
398
|
+
["Crossing the Lubombo", "Lubombo crossed"],
|
|
399
|
+
["Counting the cattle", "Cattle counted"],
|
|
400
|
+
["Thatching the roof", "Roof thatched"],
|
|
401
|
+
["Waiting out stage six", "Stage six outlasted"],
|
|
402
|
+
["Arguing with the compiler", "Compiler talked down"],
|
|
335
403
|
['Braaiing', "Braai'd"], ['Deep-frying', 'Deep-fried'],
|
|
336
404
|
['Pressure-cooking', 'Pressure-cooked'], ['Slow-smoking', 'Smoked'],
|
|
337
405
|
['Simmering', 'Simmered'], ['Marinating', 'Marinated'],
|
|
@@ -361,6 +429,14 @@ export const WORK_WORDS = {
|
|
|
361
429
|
],
|
|
362
430
|
// Something failed on the way and had to be worked around.
|
|
363
431
|
recovery: [
|
|
432
|
+
["Patching the pothole", "Pothole patched"],
|
|
433
|
+
["Jump-starting it", "Jump-started"],
|
|
434
|
+
["Duct-taping it", "Duct-taped"],
|
|
435
|
+
["Calling the mechanic", "Mechanic called"],
|
|
436
|
+
["Rerouting round the roadworks", "Roadworks rerouted"],
|
|
437
|
+
["Putting it back on the rails", "Back on the rails"],
|
|
438
|
+
["Unblocking the drain", "Drain unblocked"],
|
|
439
|
+
["Re-braaiing the burnt bit", "Burnt bit re-braaied"],
|
|
364
440
|
['Sorting it sharp-sharp', 'Sorted sharp-sharp'],
|
|
365
441
|
['De-spaghettifying', 'De-spaghettified'],
|
|
366
442
|
['Clutching it back', 'Clutched it'],
|
|
@@ -424,6 +500,7 @@ const COMMANDS = [
|
|
|
424
500
|
{ cmd: '/help', desc: 'Show help' },
|
|
425
501
|
{ cmd: '/models', desc: 'Browse and switch models for the connected provider' },
|
|
426
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" },
|
|
427
504
|
{ cmd: '/key', desc: 'Enter an API key for the current provider' },
|
|
428
505
|
{ cmd: '/model', desc: 'Show or set the model by name' },
|
|
429
506
|
{ cmd: '/provider', desc: 'Show or switch provider' },
|
|
@@ -619,6 +696,32 @@ export function pickerLabelWidth(labels, descs, width) {
|
|
|
619
696
|
const longestLabel = Math.max(18, ...labels.map(l => l.length));
|
|
620
697
|
return Math.max(18, Math.min(longestLabel, width - longestDesc - 4));
|
|
621
698
|
}
|
|
699
|
+
/**
|
|
700
|
+
* Commands that must wait for a running turn, because they change what it is running against.
|
|
701
|
+
*
|
|
702
|
+
* Everything else runs immediately. Asking what the agent has done, or where the time went, is a
|
|
703
|
+
* question about the run in progress — queueing it until the run finishes answers it about a
|
|
704
|
+
* different moment, and typing /activity to watch a long turn and being told "1 queued, will run
|
|
705
|
+
* when this finishes" is the opposite of what was wanted.
|
|
706
|
+
*
|
|
707
|
+
* A deny list rather than an allow list, because the informational commands are the majority and
|
|
708
|
+
* a new one should work without being registered twice. The dangerous ones are few and named.
|
|
709
|
+
*/
|
|
710
|
+
const COMMANDS_THAT_MUST_WAIT = new Set([
|
|
711
|
+
'/clear', '/reset', '/new', // would wipe the history the turn is using
|
|
712
|
+
'/model', '/provider', '/connect', '/key', '/endpoint', '/baseurl', // rebuild the session
|
|
713
|
+
'/resume', '/fork', // replace it
|
|
714
|
+
'/revert', // undo files the turn may still be writing
|
|
715
|
+
'/init', '/setup',
|
|
716
|
+
]);
|
|
717
|
+
/** Commands that cannot do anything useful without an argument, so they are offered for editing. */
|
|
718
|
+
const COMMANDS_NEEDING_ARGUMENT = new Set(['/send', '/ask', '/aside']);
|
|
719
|
+
export function commandRunsWhileBusy(cmd) {
|
|
720
|
+
return !COMMANDS_THAT_MUST_WAIT.has(cmd.toLowerCase());
|
|
721
|
+
}
|
|
722
|
+
export function commandNeedsArgument(cmd) {
|
|
723
|
+
return COMMANDS_NEEDING_ARGUMENT.has(cmd.toLowerCase());
|
|
724
|
+
}
|
|
622
725
|
const MODES = MODE_SPECS.map(m => m.name);
|
|
623
726
|
const HELP_TEXT = `
|
|
624
727
|
Type / on an empty line to browse every command; keep typing to filter.
|
|
@@ -630,6 +733,9 @@ Slash commands:
|
|
|
630
733
|
/connect [provider] Connect a provider (picker when no name given)
|
|
631
734
|
/connect <name> <url> Declare a provider KONECK does not ship with, e.g.
|
|
632
735
|
/connect openrouter https://openrouter.ai/api/v1
|
|
736
|
+
/endpoint Show the endpoint in force and where it came from
|
|
737
|
+
/endpoint <url> Point this provider somewhere else, e.g.
|
|
738
|
+
/endpoint http://192.168.1.50:11434/v1
|
|
633
739
|
/key Enter an API key for the current provider
|
|
634
740
|
/status Provider, model, mode, tokens, cost, CWD
|
|
635
741
|
/clear /reset /new Clear conversation history
|
|
@@ -1086,6 +1192,28 @@ function App({ config: initialConfig }) {
|
|
|
1086
1192
|
setConfigMaxWidth(typeof c.maxWidth === 'number' && c.maxWidth >= 40 ? c.maxWidth : undefined);
|
|
1087
1193
|
});
|
|
1088
1194
|
}, []);
|
|
1195
|
+
/**
|
|
1196
|
+
* The config layers other than the store, kept so an endpoint can be resolved and, more to the
|
|
1197
|
+
* point, explained. "It still sticks to the already set ollama" is a question about which file
|
|
1198
|
+
* won, and the answer is not available from the store alone.
|
|
1199
|
+
*/
|
|
1200
|
+
const [outerLayers, setOuterLayers] = useState({ project: {}, global: {} });
|
|
1201
|
+
useEffect(() => {
|
|
1202
|
+
void Promise.all([loadProjectConfig(cfg.cwd), loadGlobalConfig()])
|
|
1203
|
+
.then(([project, global]) => setOuterLayers({ project, global }))
|
|
1204
|
+
.catch(() => { });
|
|
1205
|
+
}, [cfg.cwd]);
|
|
1206
|
+
/** Nearest first, which is the order resolveEndpoint reads them in. */
|
|
1207
|
+
const endpointLayers = (store = stored) => [outerLayers.project, store, outerLayers.global];
|
|
1208
|
+
/** The endpoint in force for a provider, with where it came from. */
|
|
1209
|
+
function endpointFor(provider, store) {
|
|
1210
|
+
const def = PROVIDERS[provider.trim().toLowerCase()];
|
|
1211
|
+
return resolveEndpoint(provider, endpointLayers(store), {
|
|
1212
|
+
fallback: def?.baseURL ?? '',
|
|
1213
|
+
baseURLEnv: def?.baseURLEnv,
|
|
1214
|
+
fallbackIsUserDeclared: def?.userDefined === true,
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1089
1217
|
useEffect(() => {
|
|
1090
1218
|
const onResize = () => {
|
|
1091
1219
|
setTermCols(process.stdout.columns || 80);
|
|
@@ -1236,9 +1364,9 @@ function App({ config: initialConfig }) {
|
|
|
1236
1364
|
const delay = currentDelay();
|
|
1237
1365
|
setAnim(a => ({ spin: (a.spin + 1) % SPINNER.length, shimmer: a.shimmer + 1, elapsed }));
|
|
1238
1366
|
wordTimer.current += 1;
|
|
1239
|
-
// Roughly every
|
|
1240
|
-
//
|
|
1241
|
-
if (wordTimer.current % Math.max(20, Math.round(
|
|
1367
|
+
// Roughly every 60 seconds, whatever the current rate. Twenty-five felt churned on a long
|
|
1368
|
+
// run — the word is there to be read and enjoyed, not to reassure by moving.
|
|
1369
|
+
if (wordTimer.current % Math.max(20, Math.round(60_000 / delay)) === 0) {
|
|
1242
1370
|
setSpinWord(w => w + 1);
|
|
1243
1371
|
}
|
|
1244
1372
|
timer = setTimeout(tick, delay);
|
|
@@ -1429,7 +1557,58 @@ function App({ config: initialConfig }) {
|
|
|
1429
1557
|
* Persisting alone would leave the running session on the old value until restart, which is the
|
|
1430
1558
|
* behaviour that made the old `/config` feel inert even when it had worked.
|
|
1431
1559
|
*/
|
|
1560
|
+
/**
|
|
1561
|
+
* What a setting is worth right now, wherever the value came from.
|
|
1562
|
+
*
|
|
1563
|
+
* The store is only one of three files. Reading a row from it alone showed "(not set)" beside a
|
|
1564
|
+
* session that was plainly configured, and — with endpoints saved per provider — would show
|
|
1565
|
+
* nothing at all for an endpoint that is in force.
|
|
1566
|
+
*/
|
|
1567
|
+
function liveSetting(key) {
|
|
1568
|
+
switch (key) {
|
|
1569
|
+
case 'provider': return cfg.provider;
|
|
1570
|
+
case 'model': return cfg.model;
|
|
1571
|
+
case 'baseURL': return endpointFor(cfg.provider).url;
|
|
1572
|
+
case 'effort': return effort;
|
|
1573
|
+
case 'requireApproval': return cfg.requireApproval;
|
|
1574
|
+
case 'maxTurns': return cfg.maxTurns;
|
|
1575
|
+
case 'agentConcurrency': return cfg.agentConcurrency;
|
|
1576
|
+
case 'maxWidth': return configMaxWidth;
|
|
1577
|
+
case 'budget': return cfg.budget;
|
|
1578
|
+
default: return stored[key];
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
/** The text an edit box opens on: what the row is showing. */
|
|
1582
|
+
function settingText(field) {
|
|
1583
|
+
const key = String(field.key);
|
|
1584
|
+
const value = key === 'baseURL' || stored[key] === undefined ? liveSetting(key) : stored[key];
|
|
1585
|
+
return value === undefined || value === null ? '' : String(value);
|
|
1586
|
+
}
|
|
1432
1587
|
async function applySetting(field, value) {
|
|
1588
|
+
// The endpoint row is not a plain setting: it is one provider's address, so it is saved
|
|
1589
|
+
// against the provider in force rather than into a single slot every provider shares.
|
|
1590
|
+
if (field.key === 'baseURL') {
|
|
1591
|
+
let url;
|
|
1592
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
1593
|
+
const checked = normaliseEndpoint(value);
|
|
1594
|
+
if (!checked.url) {
|
|
1595
|
+
addSystem(checked.problem ?? `"${value}" is not a URL.`);
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1598
|
+
url = checked.url;
|
|
1599
|
+
if (checked.note)
|
|
1600
|
+
addSystem(checked.note);
|
|
1601
|
+
}
|
|
1602
|
+
const saved = withEndpoint(await loadKoneckConfig(), cfg.provider, url);
|
|
1603
|
+
await saveKoneckConfig(saved);
|
|
1604
|
+
setStored(saved);
|
|
1605
|
+
const now = endpointFor(cfg.provider, saved);
|
|
1606
|
+
const merged = { ...cfg, baseURL: now.url };
|
|
1607
|
+
setCfg(merged);
|
|
1608
|
+
await resetSession(merged);
|
|
1609
|
+
addSystem(`**${cfg.provider}** → ${now.url} (${now.source})`);
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1433
1612
|
const next = { ...stored, [field.key]: value };
|
|
1434
1613
|
if (value === undefined)
|
|
1435
1614
|
delete next[field.key];
|
|
@@ -1442,9 +1621,21 @@ function App({ config: initialConfig }) {
|
|
|
1442
1621
|
case 'maxWidth':
|
|
1443
1622
|
setConfigMaxWidth(typeof value === 'number' && value >= 40 ? value : undefined);
|
|
1444
1623
|
break;
|
|
1624
|
+
case 'provider': {
|
|
1625
|
+
// Changing the provider here has to move the endpoint with it, or the new provider is
|
|
1626
|
+
// addressed at the old one's URL — the same fault /provider had.
|
|
1627
|
+
const name = typeof value === 'string' && value.trim() !== '' ? value.trim() : cfg.provider;
|
|
1628
|
+
const def = PROVIDERS[name.toLowerCase()];
|
|
1629
|
+
const merged = {
|
|
1630
|
+
...cfg, provider: name,
|
|
1631
|
+
model: def?.defaultModel ?? cfg.model,
|
|
1632
|
+
baseURL: endpointFor(name, next).url,
|
|
1633
|
+
};
|
|
1634
|
+
setCfg(merged);
|
|
1635
|
+
await resetSession(merged);
|
|
1636
|
+
break;
|
|
1637
|
+
}
|
|
1445
1638
|
case 'model':
|
|
1446
|
-
case 'provider':
|
|
1447
|
-
case 'baseURL':
|
|
1448
1639
|
case 'maxTurns':
|
|
1449
1640
|
case 'requireApproval':
|
|
1450
1641
|
case 'agentConcurrency':
|
|
@@ -1614,9 +1805,18 @@ function App({ config: initialConfig }) {
|
|
|
1614
1805
|
return;
|
|
1615
1806
|
}
|
|
1616
1807
|
if (kind === 'command') {
|
|
1617
|
-
//
|
|
1618
|
-
|
|
1619
|
-
|
|
1808
|
+
// Run it. Picking a command out of the palette and pressing enter is choosing to run it;
|
|
1809
|
+
// leaving it in the composer meant a second enter for every command, which is a keystroke
|
|
1810
|
+
// that only ever existed to serve the few commands that need an argument.
|
|
1811
|
+
if (commandNeedsArgument(item.value)) {
|
|
1812
|
+
setDraft(item.value + ' ');
|
|
1813
|
+
setCaret(item.value.length + 1);
|
|
1814
|
+
return;
|
|
1815
|
+
}
|
|
1816
|
+
setDraft('');
|
|
1817
|
+
setCaret(0);
|
|
1818
|
+
setBusy(true);
|
|
1819
|
+
await handleCommand(item.value, '').finally(() => setBusy(false));
|
|
1620
1820
|
return;
|
|
1621
1821
|
}
|
|
1622
1822
|
if (kind === 'resume') {
|
|
@@ -1636,7 +1836,11 @@ function App({ config: initialConfig }) {
|
|
|
1636
1836
|
}
|
|
1637
1837
|
if (kind === 'provider') {
|
|
1638
1838
|
const def = PROVIDERS[item.value];
|
|
1639
|
-
|
|
1839
|
+
// The endpoint saved for the provider being switched to, not its built-in address. Passing
|
|
1840
|
+
// it into resolveProvider also settles the credential question honestly: a remote Ollama is
|
|
1841
|
+
// not loopback and does need a key, whatever the provider is called.
|
|
1842
|
+
const endpoint = endpointFor(item.value);
|
|
1843
|
+
const resolved = resolveProvider(item.value, endpoint.url);
|
|
1640
1844
|
const next = {
|
|
1641
1845
|
...cfg, provider: item.value,
|
|
1642
1846
|
model: def?.defaultModel ?? cfg.model,
|
|
@@ -1654,7 +1858,11 @@ function App({ config: initialConfig }) {
|
|
|
1654
1858
|
}
|
|
1655
1859
|
if (usable) {
|
|
1656
1860
|
await resetSession(next);
|
|
1657
|
-
|
|
1861
|
+
const savedCfg = { ...(await loadKoneckConfig()), provider: item.value, model: next.model };
|
|
1862
|
+
await saveKoneckConfig(savedCfg);
|
|
1863
|
+
setStored(savedCfg);
|
|
1864
|
+
addSystem(`Provider → ${item.value} · model → ${next.model}\n` +
|
|
1865
|
+
`Endpoint ${resolved.baseURL} (${endpoint.source}) · session reset`);
|
|
1658
1866
|
return;
|
|
1659
1867
|
}
|
|
1660
1868
|
setCfg(next);
|
|
@@ -1744,10 +1952,74 @@ function App({ config: initialConfig }) {
|
|
|
1744
1952
|
addSystem(`Current provider: ${cfg.provider}\nAvailable: ${Object.keys(PROVIDERS).join(', ')}\nUsage: /provider <name>`);
|
|
1745
1953
|
return;
|
|
1746
1954
|
}
|
|
1747
|
-
const
|
|
1748
|
-
const
|
|
1955
|
+
const name = arg.trim().toLowerCase();
|
|
1956
|
+
const def = PROVIDERS[name];
|
|
1957
|
+
if (!def) {
|
|
1958
|
+
// Switching to a name that does not resolve used to reset the session anyway, leaving it
|
|
1959
|
+
// pointed at nothing and failing on the next message instead of on the command.
|
|
1960
|
+
addSystem(`Unknown provider "${arg}". Known: ${Object.keys(PROVIDERS).join(', ')}\n` +
|
|
1961
|
+
`Declare one: /connect ${name} https://endpoint/v1`);
|
|
1962
|
+
return;
|
|
1963
|
+
}
|
|
1964
|
+
// The endpoint is the provider's own, not a literal default: whatever was saved for it,
|
|
1965
|
+
// or its environment variable, still applies. Naming a provider used to overwrite the
|
|
1966
|
+
// address with PROVIDERS[name].baseURL, which is how a configured Ollama kept coming back
|
|
1967
|
+
// as localhost.
|
|
1968
|
+
const endpoint = endpointFor(name);
|
|
1969
|
+
const newCfg = { ...cfg, provider: name, model: def.defaultModel ?? cfg.model, baseURL: endpoint.url };
|
|
1749
1970
|
await resetSession(newCfg);
|
|
1750
|
-
|
|
1971
|
+
// Persisted, or the switch is undone by the next start and the old provider wins again.
|
|
1972
|
+
const savedCfg = { ...(await loadKoneckConfig()), provider: name, model: newCfg.model };
|
|
1973
|
+
await saveKoneckConfig(savedCfg);
|
|
1974
|
+
setStored(savedCfg);
|
|
1975
|
+
addSystem(`Provider → ${name} model → ${newCfg.model}\n` +
|
|
1976
|
+
`Endpoint ${endpoint.url} (${endpoint.source})\n` +
|
|
1977
|
+
`Session reset. /endpoint <url> changes the address.`);
|
|
1978
|
+
return;
|
|
1979
|
+
}
|
|
1980
|
+
case '/url':
|
|
1981
|
+
case '/baseurl':
|
|
1982
|
+
case '/endpoint': {
|
|
1983
|
+
const provider = cfg.provider;
|
|
1984
|
+
const current = endpointFor(provider);
|
|
1985
|
+
if (!arg.trim()) {
|
|
1986
|
+
// No argument is the diagnostic. The complaint that produced this command was about a
|
|
1987
|
+
// change that appeared not to take, and the only useful reply names the file that won.
|
|
1988
|
+
const all = savedEndpoints(endpointLayers());
|
|
1989
|
+
const others = Object.entries(all).filter(([name]) => name !== provider.toLowerCase());
|
|
1990
|
+
addSystem(`**${provider}** → ${current.url}\n` +
|
|
1991
|
+
`Source: ${current.source}\n\n` +
|
|
1992
|
+
(others.length
|
|
1993
|
+
? 'Also saved:\n' + others.map(([n, u]) => ` ${n.padEnd(12)} ${u}`).join('\n') + '\n\n'
|
|
1994
|
+
: '') +
|
|
1995
|
+
`Change it: /endpoint http://192.168.1.50:11434/v1\n` +
|
|
1996
|
+
`Undo it: /endpoint default`);
|
|
1997
|
+
return;
|
|
1998
|
+
}
|
|
1999
|
+
const wanted = arg.trim();
|
|
2000
|
+
if (/^(default|reset|clear|unset)$/i.test(wanted)) {
|
|
2001
|
+
const saved = withEndpoint(await loadKoneckConfig(), provider, undefined);
|
|
2002
|
+
await saveKoneckConfig(saved);
|
|
2003
|
+
setStored(saved);
|
|
2004
|
+
const back = endpointFor(provider, saved);
|
|
2005
|
+
await resetSession({ ...cfg, baseURL: back.url });
|
|
2006
|
+
addSystem(`**${provider}** → ${back.url} (${back.source})\nSession reset.`);
|
|
2007
|
+
return;
|
|
2008
|
+
}
|
|
2009
|
+
const { url, problem, note } = normaliseEndpoint(wanted);
|
|
2010
|
+
if (!url) {
|
|
2011
|
+
addSystem(`${problem}\nUsage: /endpoint http://host:port/v1`);
|
|
2012
|
+
return;
|
|
2013
|
+
}
|
|
2014
|
+
// Saved against the provider, not as a global address: setting Ollama's URL used to move
|
|
2015
|
+
// OmniRoute's too, and one of the two was then wrong whichever way round it was left.
|
|
2016
|
+
const saved = withEndpoint(await loadKoneckConfig(), provider, url);
|
|
2017
|
+
await saveKoneckConfig(saved);
|
|
2018
|
+
setStored(saved);
|
|
2019
|
+
await resetSession({ ...cfg, baseURL: url });
|
|
2020
|
+
addSystem(`**${provider}** → ${url}\n` +
|
|
2021
|
+
`Saved for ${provider} in ${CONFIG_FILE}; session reset.` +
|
|
2022
|
+
(note ? `\n${note}` : ''));
|
|
1751
2023
|
return;
|
|
1752
2024
|
}
|
|
1753
2025
|
case '/mode':
|
|
@@ -2648,7 +2920,10 @@ function App({ config: initialConfig }) {
|
|
|
2648
2920
|
void applySetting(field, stepValue(field, stored[field.key], 1));
|
|
2649
2921
|
}
|
|
2650
2922
|
else {
|
|
2651
|
-
|
|
2923
|
+
// Opened on the current value rather than on an empty box. Editing a URL by retyping
|
|
2924
|
+
// all of it is not editing, and an empty box next to a value that is plainly set
|
|
2925
|
+
// reads as "this cannot be changed".
|
|
2926
|
+
setEditing({ key: String(field.key), value: settingText(field) });
|
|
2652
2927
|
}
|
|
2653
2928
|
return;
|
|
2654
2929
|
}
|
|
@@ -2710,6 +2985,19 @@ function App({ config: initialConfig }) {
|
|
|
2710
2985
|
void askAside(text.replace(/^\/aside\s*/, ''));
|
|
2711
2986
|
return;
|
|
2712
2987
|
}
|
|
2988
|
+
// A local command runs now. It asks about the run in progress, and answering it after the
|
|
2989
|
+
// run has finished answers it about a different moment.
|
|
2990
|
+
if (isSlashCommand(text)) {
|
|
2991
|
+
const space = text.indexOf(' ');
|
|
2992
|
+
const cmd = space === -1 ? text : text.slice(0, space);
|
|
2993
|
+
if (commandRunsWhileBusy(cmd)) {
|
|
2994
|
+
setDraft('');
|
|
2995
|
+
setCaret(0);
|
|
2996
|
+
void handleCommand(cmd, space === -1 ? '' : text.slice(space + 1).trim());
|
|
2997
|
+
return;
|
|
2998
|
+
}
|
|
2999
|
+
addSystem(`${cmd} changes the session, so it waits until this turn finishes.`);
|
|
3000
|
+
}
|
|
2713
3001
|
setQueued(q => [...q, expandPastes(text, pastesRef.current)]);
|
|
2714
3002
|
setDraft('');
|
|
2715
3003
|
setCaret(0);
|
|
@@ -3146,23 +3434,23 @@ function App({ config: initialConfig }) {
|
|
|
3146
3434
|
// What is actually in force, not just what this file happens to hold. provider and
|
|
3147
3435
|
// baseURL commonly come from ~/.koneckrc, and showing those as "(not set)" made a
|
|
3148
3436
|
// configured session look unconfigured.
|
|
3149
|
-
const
|
|
3150
|
-
provider: cfg.provider, model: cfg.model, baseURL: cfg.baseURL,
|
|
3151
|
-
effort, requireApproval: cfg.requireApproval, maxTurns: cfg.maxTurns,
|
|
3152
|
-
agentConcurrency: cfg.agentConcurrency, maxWidth: configMaxWidth, budget: cfg.budget,
|
|
3153
|
-
};
|
|
3437
|
+
const endpoint = endpointFor(cfg.provider);
|
|
3154
3438
|
const labelW = Math.max(...CONFIG_SCHEMA.map(f => f.label.length));
|
|
3155
3439
|
for (const [i, f] of CONFIG_SCHEMA.entries()) {
|
|
3156
3440
|
const selected = i === settingRow;
|
|
3157
3441
|
const beingTyped = editing?.key === String(f.key);
|
|
3158
|
-
|
|
3159
|
-
|
|
3442
|
+
// An endpoint always has a source worth naming, so it never reads as unset or as
|
|
3443
|
+
// merely inherited: the question it answers is which file won.
|
|
3444
|
+
const isEndpoint = f.key === 'baseURL';
|
|
3445
|
+
const here = !isEndpoint && stored[f.key] !== undefined;
|
|
3446
|
+
const value = here ? stored[f.key] : liveSetting(String(f.key));
|
|
3160
3447
|
const raw = f.key === 'maxTurns' && (value === 0 || value === undefined)
|
|
3161
3448
|
? 'no limit'
|
|
3162
3449
|
: displayValue(value);
|
|
3163
3450
|
const shown = beingTyped
|
|
3164
3451
|
? `${editing.value}_`
|
|
3165
|
-
:
|
|
3452
|
+
: isEndpoint ? `${raw} (${endpoint.source})`
|
|
3453
|
+
: raw + (here || value === undefined ? '' : ' (inherited)');
|
|
3166
3454
|
// The marker is what makes the selection legible without colour, which a panel line
|
|
3167
3455
|
// drawn as one string cannot carry per-segment.
|
|
3168
3456
|
const mark = selected ? '>' : ' ';
|