koneck 2.31.0 → 2.32.0
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.d.ts +1 -50
- package/dist/ink-chat.d.ts.map +1 -1
- package/dist/ink-chat.js +23 -226
- package/dist/ink-chat.js.map +1 -1
- package/dist/web/api.d.ts.map +1 -1
- package/dist/web/api.js +44 -0
- package/dist/web/api.js.map +1 -1
- package/dist/web/ui-client.d.ts.map +1 -1
- package/dist/web/ui-client.js +280 -34
- package/dist/web/ui-client.js.map +1 -1
- package/dist/web/ui-css.d.ts.map +1 -1
- package/dist/web/ui-css.js +29 -0
- package/dist/web/ui-css.js.map +1 -1
- package/dist/web/ui.d.ts.map +1 -1
- package/dist/web/ui.js +19 -1
- package/dist/web/ui.js.map +1 -1
- package/dist/work-words.d.ts +57 -0
- package/dist/work-words.d.ts.map +1 -0
- package/dist/work-words.js +234 -0
- package/dist/work-words.js.map +1 -0
- package/package.json +1 -1
package/dist/web/ui-client.js
CHANGED
|
@@ -62,9 +62,14 @@ const api = async (path, opts) => {
|
|
|
62
62
|
});
|
|
63
63
|
if (res.status === 401) { $('tokdlg').showModal(); throw new Error('token required'); }
|
|
64
64
|
if (!res.ok) {
|
|
65
|
-
let msg = res.statusText;
|
|
66
|
-
try {
|
|
67
|
-
|
|
65
|
+
let msg = res.statusText, body = null;
|
|
66
|
+
try { body = await res.json(); msg = body.error || msg; } catch (e) {}
|
|
67
|
+
const err = new Error(msg);
|
|
68
|
+
// The status and the body travel with the error: a refusal that can be confirmed and retried
|
|
69
|
+
// is not the same as one that cannot, and the caller is what knows the difference.
|
|
70
|
+
err.status = res.status;
|
|
71
|
+
err.body = body;
|
|
72
|
+
throw err;
|
|
68
73
|
}
|
|
69
74
|
return res.status === 204 ? null : res.json();
|
|
70
75
|
};
|
|
@@ -274,6 +279,39 @@ function renderAsk(e) {
|
|
|
274
279
|
*/
|
|
275
280
|
let tick = null;
|
|
276
281
|
let startedAt = 0;
|
|
282
|
+
/** What this turn has amounted to so far, which is what decides the wording. */
|
|
283
|
+
const turn = { tools: 0, recovered: false, seed: 0, word: '' };
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Which family of words fits, by the same rule the terminal uses.
|
|
287
|
+
*
|
|
288
|
+
* Time alone cannot tell effort from latency, so a turn that ran no tools stays in the reply
|
|
289
|
+
* vocabulary however long it took: a cooking verb would be describing work that never happened.
|
|
290
|
+
*/
|
|
291
|
+
function tierFor(elapsedMs, tools, recovered) {
|
|
292
|
+
if (recovered) return 'recovery';
|
|
293
|
+
if (tools === 0) return 'reply';
|
|
294
|
+
if (elapsedMs < 3000) return 'quick';
|
|
295
|
+
if (elapsedMs > 10000) return 'heavy';
|
|
296
|
+
return 'normal';
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** The gerund to show while working, and the past tense to report when the turn lands. */
|
|
300
|
+
function workWord(elapsedMs, tools, recovered, seed) {
|
|
301
|
+
const bucket = WORK_WORDS[tierFor(elapsedMs, tools, recovered)];
|
|
302
|
+
return bucket[Math.abs(seed) % bucket.length];
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Whether the turn earned a verb at all.
|
|
307
|
+
*
|
|
308
|
+
* "Cooked lekker in 5.0s" for answering "hi" is a boast about nothing: no tool ran, and the only
|
|
309
|
+
* reason it took five seconds is that the provider is slow.
|
|
310
|
+
*/
|
|
311
|
+
function earnedAVerb(tools, recovered, chars) {
|
|
312
|
+
return recovered || tools > 0 || chars >= 240;
|
|
313
|
+
}
|
|
314
|
+
|
|
277
315
|
function setBusy(on, what) {
|
|
278
316
|
state.busy = on;
|
|
279
317
|
$('send').disabled = on || !$('ta').value.trim();
|
|
@@ -283,8 +321,20 @@ function setBusy(on, what) {
|
|
|
283
321
|
if (on) {
|
|
284
322
|
if (!tick) {
|
|
285
323
|
startedAt = Date.now();
|
|
324
|
+
turn.tools = 0; turn.recovered = false; turn.word = '';
|
|
325
|
+
// One seed per turn, so the phrase is stable while the turn runs and different next time.
|
|
326
|
+
// Math.random is fine here: nothing depends on it being reproducible.
|
|
327
|
+
turn.seed = Math.floor(Math.random() * 100000);
|
|
286
328
|
tick = setInterval(() => {
|
|
287
|
-
|
|
329
|
+
const elapsed = Date.now() - startedAt;
|
|
330
|
+
$('liveelapsed').textContent = ms(elapsed);
|
|
331
|
+
// The word escalates as the wait grows: what began as "Sharp-sharp" should not still be
|
|
332
|
+
// claiming that two minutes later.
|
|
333
|
+
const pair = workWord(elapsed, turn.tools, turn.recovered, turn.seed);
|
|
334
|
+
if (pair && pair[0] !== turn.word) {
|
|
335
|
+
turn.word = pair[0];
|
|
336
|
+
$('livewhat').textContent = pair[0];
|
|
337
|
+
}
|
|
288
338
|
}, 250);
|
|
289
339
|
}
|
|
290
340
|
} else if (tick) {
|
|
@@ -349,8 +399,11 @@ function onEvent(e) {
|
|
|
349
399
|
// shape:" as one paragraph.
|
|
350
400
|
state.replyEl = null;
|
|
351
401
|
state.thinkEl = null;
|
|
402
|
+
turn.tools += 1;
|
|
352
403
|
$('livetool').textContent = e.name + (e.detail ? ' ' + e.detail : '');
|
|
353
404
|
renderTool(e);
|
|
405
|
+
// Follow it: the path a tool names is the file to be looking at.
|
|
406
|
+
watchPath(pathFromTool(e), e.name, false);
|
|
354
407
|
break;
|
|
355
408
|
case 'tool-output':
|
|
356
409
|
if (state.lastTool) {
|
|
@@ -358,11 +411,15 @@ function onEvent(e) {
|
|
|
358
411
|
(state.lastTool.body.textContent + e.line + '\n').slice(-5000);
|
|
359
412
|
}
|
|
360
413
|
break;
|
|
361
|
-
case 'tool-done':
|
|
414
|
+
case 'tool-done': {
|
|
362
415
|
finishTool(e);
|
|
363
416
|
setBusy(true, 'thinking');
|
|
364
417
|
$('livetool').textContent = '';
|
|
418
|
+
// Once it has finished writing, the diff is what there is to see.
|
|
419
|
+
const wrotePath = pathFromTool(e);
|
|
420
|
+
if (e.ok && toolWrites(e.name)) watchPath(wrotePath || follow.path, e.name, true);
|
|
365
421
|
break;
|
|
422
|
+
}
|
|
366
423
|
case 'plan': renderPlan(e.steps); break;
|
|
367
424
|
case 'agents': renderAgents(e.agents); break;
|
|
368
425
|
case 'tokens':
|
|
@@ -378,7 +435,16 @@ function onEvent(e) {
|
|
|
378
435
|
// The server says a turn touched files, so the badge needs no polling to be right.
|
|
379
436
|
$('tab-changes').innerHTML = 'Changes <span class="n">' + e.files + '</span>';
|
|
380
437
|
break;
|
|
381
|
-
case 'turn-end':
|
|
438
|
+
case 'turn-end': {
|
|
439
|
+
// What it came to, in the tense that matches. The terminal has always reported this; the
|
|
440
|
+
// browser used to just stop showing a spinner, which reads as though nothing happened.
|
|
441
|
+
const took = Date.now() - startedAt;
|
|
442
|
+
const said = state.replyEl ? (state.replyEl.textContent || '').length : 0;
|
|
443
|
+
const pair = workWord(took, turn.tools, turn.recovered, turn.seed);
|
|
444
|
+
const verb = earnedAVerb(turn.tools, turn.recovered, said) && pair ? pair[1] : '';
|
|
445
|
+
const line = el('div', 'landed', (verb ? verb + ' · ' : '') + ms(took)
|
|
446
|
+
+ (turn.tools ? ' · ' + turn.tools + (turn.tools === 1 ? ' tool' : ' tools') : ''));
|
|
447
|
+
if (startedAt) stream().appendChild(line);
|
|
382
448
|
setBusy(false);
|
|
383
449
|
state.replyEl = null; state.thinkEl = null;
|
|
384
450
|
if (state.view === 'changes') loadChanges();
|
|
@@ -388,6 +454,7 @@ function onEvent(e) {
|
|
|
388
454
|
if (mine) setPosture(mine);
|
|
389
455
|
}).catch(() => {});
|
|
390
456
|
break;
|
|
457
|
+
}
|
|
391
458
|
}
|
|
392
459
|
}
|
|
393
460
|
|
|
@@ -1745,10 +1812,136 @@ $('pmode').onclick = async () => {
|
|
|
1745
1812
|
}
|
|
1746
1813
|
$('modedlg').showModal();
|
|
1747
1814
|
};
|
|
1815
|
+
/* ── following the file being worked on ───────────────────────────────────────────────────── */
|
|
1816
|
+
|
|
1817
|
+
/**
|
|
1818
|
+
* The panel beside the conversation, showing whatever the agent is touching.
|
|
1819
|
+
*
|
|
1820
|
+
* On by default, because the interesting part of watching an agent work is watching files change,
|
|
1821
|
+
* and a view you have to go and click is one you find out about after the fact. Remembered per
|
|
1822
|
+
* browser: a preference about a layout belongs to whoever is looking at it.
|
|
1823
|
+
*/
|
|
1824
|
+
const follow = {
|
|
1825
|
+
on: (() => { try { return localStorage.getItem('koneck.follow') !== 'off'; } catch (e) { return true; } })(),
|
|
1826
|
+
path: null,
|
|
1827
|
+
/** Set while a fetch is in flight, so a burst of tool calls does not queue a burst of requests. */
|
|
1828
|
+
busy: false,
|
|
1829
|
+
/** The path a newer tool named while a fetch was running. */
|
|
1830
|
+
pending: null,
|
|
1831
|
+
};
|
|
1832
|
+
|
|
1833
|
+
/** The file a tool is acting on, from its arguments. Null when it is not about a file. */
|
|
1834
|
+
function pathFromTool(e) {
|
|
1835
|
+
let args = e.args;
|
|
1836
|
+
if (!args && e.detail) args = null;
|
|
1837
|
+
if (args) {
|
|
1838
|
+
try {
|
|
1839
|
+
const parsed = JSON.parse(args);
|
|
1840
|
+
for (const key of ['path', 'file', 'filename', 'file_path']) {
|
|
1841
|
+
if (typeof parsed[key] === 'string' && parsed[key].trim()) return parsed[key].trim();
|
|
1842
|
+
}
|
|
1843
|
+
if (Array.isArray(parsed.paths) && typeof parsed.paths[0] === 'string') return parsed.paths[0];
|
|
1844
|
+
} catch (err) { /* not JSON: fall through to the one-line detail */ }
|
|
1845
|
+
}
|
|
1846
|
+
// The detail line is often just the path, which is enough to follow by.
|
|
1847
|
+
if (e.detail && /[./]/.test(e.detail) && !/\s/.test(e.detail.trim())) return e.detail.trim();
|
|
1848
|
+
return null;
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
/** Whether a tool changes a file, which decides whether the panel shows content or a diff. */
|
|
1852
|
+
function toolWrites(name) {
|
|
1853
|
+
return /write|edit|patch|replace|apply|create|delete|move|rename|format/i.test(name || '');
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
function setFollow(on) {
|
|
1857
|
+
follow.on = on;
|
|
1858
|
+
try { localStorage.setItem('koneck.follow', on ? 'on' : 'off'); } catch (e) {}
|
|
1859
|
+
$('pfollow').innerHTML = 'follow <b>' + (on ? 'on' : 'off') + '</b>';
|
|
1860
|
+
$('watch').hidden = !on || !follow.path;
|
|
1861
|
+
if (on && follow.path) void showWatched(follow.path, null);
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
/** Points the panel at a path. Cheap to call repeatedly; only the newest target is fetched. */
|
|
1865
|
+
function watchPath(path, toolName, wrote) {
|
|
1866
|
+
if (!follow.on || !path) return;
|
|
1867
|
+
follow.path = path;
|
|
1868
|
+
$('watch').hidden = false;
|
|
1869
|
+
$('wpath').textContent = path;
|
|
1870
|
+
$('wtool').textContent = toolName || 'watching';
|
|
1871
|
+
if (follow.busy) { follow.pending = [path, wrote]; return; }
|
|
1872
|
+
void showWatched(path, wrote);
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
/**
|
|
1876
|
+
* Draws the panel.
|
|
1877
|
+
*
|
|
1878
|
+
* A file that was only read is shown as itself. A file that was just written is shown as its diff,
|
|
1879
|
+
* because what changed is the thing worth seeing and reading the whole file to find three new
|
|
1880
|
+
* lines is work the panel should be doing.
|
|
1881
|
+
*/
|
|
1882
|
+
async function showWatched(path, wrote) {
|
|
1883
|
+
follow.busy = true;
|
|
1884
|
+
const host = $('wbody');
|
|
1885
|
+
try {
|
|
1886
|
+
if (wrote && state.session) {
|
|
1887
|
+
let ch = null;
|
|
1888
|
+
try {
|
|
1889
|
+
const out = await api('/api/changes?session=' + encodeURIComponent(state.session));
|
|
1890
|
+
ch = (out.changes || []).find((c) => c.path === path || path.endsWith(c.path));
|
|
1891
|
+
} catch (e) { /* fall through to showing the file */ }
|
|
1892
|
+
if (ch && ch.diff) {
|
|
1893
|
+
host.innerHTML = '';
|
|
1894
|
+
const head = el('div', 'wempty');
|
|
1895
|
+
head.style.padding = '8px 12px';
|
|
1896
|
+
head.textContent = '+' + ch.added + ' \u2212' + ch.removed + ' ' + ch.kind;
|
|
1897
|
+
host.appendChild(head);
|
|
1898
|
+
host.appendChild(renderDiff(ch.diff));
|
|
1899
|
+
return;
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
let data;
|
|
1903
|
+
try { data = await api('/api/file?path=' + encodeURIComponent(path)); }
|
|
1904
|
+
catch (e) {
|
|
1905
|
+
host.innerHTML = '';
|
|
1906
|
+
host.appendChild(el('div', 'wempty', e.message));
|
|
1907
|
+
return;
|
|
1908
|
+
}
|
|
1909
|
+
host.innerHTML = '';
|
|
1910
|
+
if (data.binary || data.text === null) {
|
|
1911
|
+
host.appendChild(el('div', 'wempty', 'Binary file — nothing to show as text.'));
|
|
1912
|
+
return;
|
|
1913
|
+
}
|
|
1914
|
+
const code = el('div', 'code');
|
|
1915
|
+
const lines = data.text.split('\n');
|
|
1916
|
+
code.appendChild(el('div', 'gut', lines.map((_, i) => i + 1).join('\n')));
|
|
1917
|
+
code.appendChild(el('div', 'src', data.text));
|
|
1918
|
+
host.appendChild(code);
|
|
1919
|
+
} finally {
|
|
1920
|
+
follow.busy = false;
|
|
1921
|
+
// A newer target arrived while this was in flight: honour the last one and drop the rest,
|
|
1922
|
+
// which is what keeps a run of twenty edits to twenty files at one request rather than twenty.
|
|
1923
|
+
if (follow.pending) {
|
|
1924
|
+
const next = follow.pending;
|
|
1925
|
+
follow.pending = null;
|
|
1926
|
+
if (next[0] !== path || next[1]) void showWatched(next[0], next[1]);
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
$('pfollow').onclick = () => setFollow(!follow.on);
|
|
1932
|
+
$('wclose').onclick = () => setFollow(false);
|
|
1933
|
+
|
|
1748
1934
|
/* ── choosing a model ────────────────────────────────────────────────────────────────────── */
|
|
1749
1935
|
|
|
1750
|
-
/**
|
|
1751
|
-
|
|
1936
|
+
/**
|
|
1937
|
+
* The provider's own listing, kept so filtering does not re-ask on every keystroke.
|
|
1938
|
+
*
|
|
1939
|
+
* The chosen entry is the model that will be switched to, and it is deliberately NOT the search
|
|
1940
|
+
* box. They were the same field at first, so opening the dialog pre-filled the box with the model
|
|
1941
|
+
* already in use — and since the box also filtered, 476 models were immediately narrowed to the
|
|
1942
|
+
* one you were on. It looked exactly like a text field with no choice in it, because it was.
|
|
1943
|
+
*/
|
|
1944
|
+
const catalog = { provider: null, entries: [], listed: false, note: '', chosen: '' };
|
|
1752
1945
|
|
|
1753
1946
|
/** Fills the provider dropdown from whatever the registry holds, declared entries included. */
|
|
1754
1947
|
async function fillProviders(selected) {
|
|
@@ -1770,46 +1963,61 @@ async function fillProviders(selected) {
|
|
|
1770
1963
|
return info;
|
|
1771
1964
|
}
|
|
1772
1965
|
|
|
1773
|
-
/**
|
|
1966
|
+
/** What Switch will use: whatever row is picked, or a name typed in full when nothing is. */
|
|
1967
|
+
function chosenModel() {
|
|
1968
|
+
const typed = $('mmodel').value.trim();
|
|
1969
|
+
// A provider that publishes no list leaves the box as the only way in, so there the box IS the
|
|
1970
|
+
// answer. Otherwise a picked row wins, and typing only narrows the list.
|
|
1971
|
+
if (!catalog.listed) return typed;
|
|
1972
|
+
if (catalog.chosen) return catalog.chosen;
|
|
1973
|
+
return typed;
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
/** Draws the listing, narrowed by the search box. */
|
|
1774
1977
|
function drawModels() {
|
|
1775
1978
|
const host = $('mlist');
|
|
1776
1979
|
const typed = $('mmodel').value.trim().toLowerCase();
|
|
1777
1980
|
host.innerHTML = '';
|
|
1981
|
+
if (!catalog.listed) { $('mnote').textContent = catalog.note || ''; return; }
|
|
1982
|
+
|
|
1778
1983
|
const shown = catalog.entries.filter((m) => !typed || m.id.toLowerCase().includes(typed));
|
|
1984
|
+
const total = catalog.entries.length;
|
|
1779
1985
|
|
|
1780
|
-
if (!catalog.listed) { $('mnote').textContent = catalog.note || ''; return; }
|
|
1781
1986
|
if (shown.length === 0) {
|
|
1782
|
-
|
|
1987
|
+
// Not a dead end: a gateway may serve something it does not advertise, so an unmatched name
|
|
1988
|
+
// stays usable — but it has to be said out loud rather than silently accepted.
|
|
1989
|
+
$('mnote').textContent = 'Nothing in ' + total + ' matches. Switch will use it as typed.';
|
|
1990
|
+
catalog.chosen = '';
|
|
1783
1991
|
return;
|
|
1784
1992
|
}
|
|
1785
|
-
$('mnote').textContent = shown.length ===
|
|
1786
|
-
|
|
1787
|
-
: shown.length + ' of ' + catalog.entries.length + ' models';
|
|
1993
|
+
$('mnote').textContent = (shown.length === total ? total + ' models' : shown.length + ' of ' + total)
|
|
1994
|
+
+ (catalog.chosen ? ' · using ' + catalog.chosen : ' · click one to choose');
|
|
1788
1995
|
|
|
1789
|
-
// Grouped by vendor, which is what turns a flat several-hundred-entry list into something
|
|
1790
|
-
//
|
|
1791
|
-
let group = null;
|
|
1996
|
+
// Grouped by vendor, which is what turns a flat several-hundred-entry list into something you
|
|
1997
|
+
// can actually read.
|
|
1998
|
+
let group = null, first = null;
|
|
1792
1999
|
for (const m of shown.slice(0, 400)) {
|
|
1793
2000
|
if (m.vendor !== group) {
|
|
1794
2001
|
group = m.vendor;
|
|
1795
2002
|
host.appendChild(el('div', 'mgroup', group));
|
|
1796
2003
|
}
|
|
1797
|
-
const
|
|
2004
|
+
const on = m.id === catalog.chosen;
|
|
2005
|
+
const row = el('div', 'mrow' + (on ? ' on' : ''));
|
|
1798
2006
|
row.appendChild(el('div', 'mid', m.id));
|
|
1799
2007
|
row.appendChild(el('div', 'mdet', m.detail));
|
|
1800
|
-
row.onclick = () => {
|
|
1801
|
-
$('mmodel').value = m.id;
|
|
1802
|
-
drawModels();
|
|
1803
|
-
};
|
|
2008
|
+
row.onclick = () => { catalog.chosen = m.id; drawModels(); };
|
|
1804
2009
|
host.appendChild(row);
|
|
2010
|
+
if (on && !first) first = row;
|
|
1805
2011
|
}
|
|
1806
2012
|
if (shown.length > 400) {
|
|
1807
2013
|
host.appendChild(el('div', 'mgroup', 'and ' + (shown.length - 400) + ' more — keep typing'));
|
|
1808
2014
|
}
|
|
2015
|
+
// The model in use should be in view when the dialog opens, not hundreds of rows down.
|
|
2016
|
+
if (first) first.scrollIntoView({ block: 'center' });
|
|
1809
2017
|
}
|
|
1810
2018
|
|
|
1811
2019
|
/** Asks the selected provider what it serves. */
|
|
1812
|
-
async function loadModels(provider) {
|
|
2020
|
+
async function loadModels(provider, preselect) {
|
|
1813
2021
|
if (catalog.provider === provider) { drawModels(); return; }
|
|
1814
2022
|
$('mlist').innerHTML = '';
|
|
1815
2023
|
$('mnote').textContent = 'Asking ' + provider + ' what it serves…';
|
|
@@ -1818,6 +2026,9 @@ async function loadModels(provider) {
|
|
|
1818
2026
|
catch (e) {
|
|
1819
2027
|
catalog.provider = provider; catalog.entries = []; catalog.listed = false;
|
|
1820
2028
|
catalog.note = e.message;
|
|
2029
|
+
catalog.chosen = '';
|
|
2030
|
+
// With no list, the box is the only way to name a model, so it gets the current one to edit.
|
|
2031
|
+
if (preselect) $('mmodel').value = preselect;
|
|
1821
2032
|
drawModels();
|
|
1822
2033
|
return;
|
|
1823
2034
|
}
|
|
@@ -1825,24 +2036,28 @@ async function loadModels(provider) {
|
|
|
1825
2036
|
catalog.entries = out.entries || [];
|
|
1826
2037
|
catalog.listed = !!out.listed;
|
|
1827
2038
|
catalog.note = out.note || '';
|
|
2039
|
+
// Preselect only if the provider actually serves it: carrying a name across providers is how a
|
|
2040
|
+
// session ends up asking Anthropic for qwen3-coder.
|
|
2041
|
+
catalog.chosen = preselect && catalog.entries.some((m) => m.id === preselect) ? preselect : '';
|
|
2042
|
+
if (!catalog.listed && preselect) $('mmodel').value = preselect;
|
|
1828
2043
|
drawModels();
|
|
1829
2044
|
}
|
|
1830
2045
|
|
|
1831
2046
|
$('pmodel').onclick = async () => {
|
|
1832
2047
|
const info = await fillProviders(state.provider);
|
|
1833
2048
|
if (!info) return;
|
|
1834
|
-
|
|
2049
|
+
// Empty, because this box searches. Pre-filling it with the model already in use filtered the
|
|
2050
|
+
// list down to that one model and made the whole listing look like it was not there.
|
|
2051
|
+
$('mmodel').value = '';
|
|
1835
2052
|
catalog.provider = null; // re-ask: a key may have been set since last time
|
|
1836
2053
|
$('mdlg').showModal();
|
|
1837
|
-
void loadModels($('mprov').value);
|
|
2054
|
+
void loadModels($('mprov').value, state.model === '-' ? '' : state.model);
|
|
1838
2055
|
};
|
|
1839
2056
|
$('mprov').onchange = () => {
|
|
1840
|
-
// A different provider serves different models,
|
|
1841
|
-
// the name of a model the previous one happened to serve.
|
|
1842
|
-
const chosen = $('mprov').selectedOptions[0];
|
|
2057
|
+
// A different provider serves different models, so nothing carries over.
|
|
1843
2058
|
$('mmodel').value = '';
|
|
1844
|
-
|
|
1845
|
-
|
|
2059
|
+
catalog.chosen = '';
|
|
2060
|
+
void loadModels($('mprov').value, '');
|
|
1846
2061
|
};
|
|
1847
2062
|
$('mmodel').oninput = () => drawModels();
|
|
1848
2063
|
|
|
@@ -1850,7 +2065,8 @@ $('addprov').onclick = () => {
|
|
|
1850
2065
|
$('pverr').hidden = true;
|
|
1851
2066
|
$('provdlg').showModal();
|
|
1852
2067
|
};
|
|
1853
|
-
|
|
2068
|
+
/** Submits the form, once plainly and once with consent if a built-in is in the way. */
|
|
2069
|
+
async function saveProvider(replace) {
|
|
1854
2070
|
const body = {
|
|
1855
2071
|
name: $('pvname').value.trim(),
|
|
1856
2072
|
label: $('pvname').value.trim(),
|
|
@@ -1858,25 +2074,52 @@ $('pvsave').onclick = async (ev) => {
|
|
|
1858
2074
|
defaultModel: $('pvmodel').value.trim(),
|
|
1859
2075
|
apiKeyEnv: $('pvkey').value.trim(),
|
|
1860
2076
|
};
|
|
2077
|
+
if (replace) body.replace = true;
|
|
2078
|
+
return api('/api/providers', { method: 'POST', body: JSON.stringify(body) });
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
$('pvsave').onclick = async (ev) => {
|
|
1861
2082
|
let out;
|
|
1862
|
-
try { out = await
|
|
2083
|
+
try { out = await saveProvider(false); }
|
|
1863
2084
|
catch (e) {
|
|
1864
2085
|
// Kept open on failure: the message says what to change, and closing would lose the typing.
|
|
1865
2086
|
ev.preventDefault();
|
|
1866
2087
|
$('pverr').textContent = e.message;
|
|
1867
2088
|
$('pverr').hidden = false;
|
|
2089
|
+
// A name KONECK ships with is not a mistake to correct, it is a decision to confirm — putting a
|
|
2090
|
+
// company proxy in front of a vendor is exactly this. So the refusal comes with the way through
|
|
2091
|
+
// rather than being a dead end.
|
|
2092
|
+
if (e.status === 409 && e.body && e.body.confirmWith === 'replace') {
|
|
2093
|
+
const go = el('button', 'go', 'Repoint ' + e.body.shadowsBuiltIn + ' anyway');
|
|
2094
|
+
go.type = 'button';
|
|
2095
|
+
go.style.marginTop = '8px';
|
|
2096
|
+
go.onclick = async () => {
|
|
2097
|
+
let done;
|
|
2098
|
+
try { done = await saveProvider(true); }
|
|
2099
|
+
catch (e2) { $('pverr').textContent = e2.message; return; }
|
|
2100
|
+
$('provdlg').close();
|
|
2101
|
+
await afterProviderSaved(done);
|
|
2102
|
+
};
|
|
2103
|
+
$('pverr').appendChild(document.createElement('br'));
|
|
2104
|
+
$('pverr').appendChild(go);
|
|
2105
|
+
}
|
|
1868
2106
|
return;
|
|
1869
2107
|
}
|
|
2108
|
+
await afterProviderSaved(out);
|
|
2109
|
+
};
|
|
2110
|
+
|
|
2111
|
+
/** Clears the form and moves the model dialog onto whatever was just declared. */
|
|
2112
|
+
async function afterProviderSaved(out) {
|
|
1870
2113
|
$('provdlg').close();
|
|
1871
2114
|
for (const id of ['pvname', 'pvurl', 'pvmodel', 'pvkey']) $(id).value = '';
|
|
1872
2115
|
await fillProviders(out.name);
|
|
1873
2116
|
$('mmodel').value = out.defaultModel === 'default' ? '' : out.defaultModel;
|
|
1874
2117
|
catalog.provider = null;
|
|
1875
2118
|
void loadModels(out.name);
|
|
1876
|
-
}
|
|
2119
|
+
}
|
|
1877
2120
|
|
|
1878
2121
|
$('msave').onclick = async () => {
|
|
1879
|
-
const provider = $('mprov').value, model =
|
|
2122
|
+
const provider = $('mprov').value, model = chosenModel();
|
|
1880
2123
|
if (!state.session) await newSession();
|
|
1881
2124
|
let info;
|
|
1882
2125
|
try {
|
|
@@ -1890,6 +2133,9 @@ for (const b of document.querySelectorAll('.hero .ex button')) {
|
|
|
1890
2133
|
b.onclick = () => send(b.textContent);
|
|
1891
2134
|
}
|
|
1892
2135
|
|
|
2136
|
+
// The pill has to say what the remembered setting actually is before anything is asked.
|
|
2137
|
+
setFollow(follow.on);
|
|
2138
|
+
|
|
1893
2139
|
(async function start() {
|
|
1894
2140
|
let hello;
|
|
1895
2141
|
try { hello = await fetch('/api/hello').then((r) => r.json()); } catch (e) {}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ui-client.js","sourceRoot":"","sources":["../../src/web/ui-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,YAAY;IAC1B,OAAO,MAAM,CAAC,GAAG,CAAA
|
|
1
|
+
{"version":3,"file":"ui-client.js","sourceRoot":"","sources":["../../src/web/ui-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,YAAY;IAC1B,OAAO,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4lElB,CAAC;AACF,CAAC"}
|
package/dist/web/ui-css.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ui-css.d.ts","sourceRoot":"","sources":["../../src/web/ui-css.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,wBAAgB,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"ui-css.d.ts","sourceRoot":"","sources":["../../src/web/ui-css.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,wBAAgB,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAkiB3C"}
|
package/dist/web/ui-css.js
CHANGED
|
@@ -518,6 +518,35 @@ dialog:has(.dlg.wide){max-width:560px}
|
|
|
518
518
|
.mrow .mid{font-family:var(--mono);font-size:12.5px;flex:1;min-width:0;overflow-wrap:anywhere}
|
|
519
519
|
.mrow .mdet{font-size:11px;color:var(--dim);white-space:nowrap;flex:0 0 auto}
|
|
520
520
|
.mrow.on .mid{color:var(--cyan)}
|
|
521
|
+
/*
|
|
522
|
+
* The file being worked on, beside the conversation.
|
|
523
|
+
*
|
|
524
|
+
* Watching an agent work means watching files change, and a tab you have to click is a tab you
|
|
525
|
+
* find out about afterwards. This follows the tool calls: the path a tool names becomes the panel,
|
|
526
|
+
* and once the file has actually been written it becomes that file's diff.
|
|
527
|
+
*/
|
|
528
|
+
.chatsplit{flex:1;display:flex;min-height:0;min-width:0}
|
|
529
|
+
.chatsplit .wrap{flex:1 1 auto;min-width:0}
|
|
530
|
+
.watch{flex:0 0 44%;max-width:720px;min-width:0;border-left:1px solid var(--line);
|
|
531
|
+
display:flex;flex-direction:column;background:var(--panel-2)}
|
|
532
|
+
.whead{display:flex;gap:9px;align-items:center;padding:9px 12px;background:var(--panel);
|
|
533
|
+
border-bottom:1px solid var(--line);font-size:11.5px;flex:0 0 auto}
|
|
534
|
+
.whead .wt{color:var(--cyan);font-size:10px;letter-spacing:.09em;text-transform:uppercase;
|
|
535
|
+
flex:0 0 auto}
|
|
536
|
+
.whead .wp{font-family:var(--mono);font-size:12px;flex:1;min-width:0;overflow:hidden;
|
|
537
|
+
text-overflow:ellipsis;white-space:nowrap;direction:rtl;text-align:left}
|
|
538
|
+
.whead .wx{flex:0 0 auto;background:none;border:0;color:var(--dim);font-size:15px;cursor:pointer;
|
|
539
|
+
line-height:1;padding:0 2px}
|
|
540
|
+
.whead .wx:hover{color:var(--red)}
|
|
541
|
+
.wbody{flex:1;overflow:auto;min-height:0}
|
|
542
|
+
.wbody .code{min-height:100%}
|
|
543
|
+
.wbody .diff{border-top:0}
|
|
544
|
+
.wempty{padding:22px 14px;color:var(--dim);font-size:12.5px;line-height:1.6}
|
|
545
|
+
@media (max-width:1100px){ .watch{display:none} }
|
|
546
|
+
|
|
547
|
+
/* What the turn came to, in the tense that matches — the line the terminal has always printed. */
|
|
548
|
+
.landed{font-size:11.5px;color:var(--dim);font-family:var(--mono);margin:2px 0 14px;
|
|
549
|
+
letter-spacing:.02em}
|
|
521
550
|
.dlg .row{display:flex;gap:8px;justify-content:flex-end}
|
|
522
551
|
.dlg .row button{padding:7px 15px;border-radius:8px;border:1px solid var(--line)}
|
|
523
552
|
.dlg .go{border-color:var(--cyan);color:var(--cyan)}
|
package/dist/web/ui-css.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ui-css.js","sourceRoot":"","sources":["../../src/web/ui-css.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,GAAG,CAAC,OAAe;IACjC,OAAO;;;;;;;;;;gBAUO,OAAO
|
|
1
|
+
{"version":3,"file":"ui-css.js","sourceRoot":"","sources":["../../src/web/ui-css.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,GAAG,CAAC,OAAe;IACjC,OAAO;;;;;;;;;;gBAUO,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAshBtB,CAAC;AACF,CAAC"}
|
package/dist/web/ui.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ui.d.ts","sourceRoot":"","sources":["../../src/web/ui.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;
|
|
1
|
+
{"version":3,"file":"ui.d.ts","sourceRoot":"","sources":["../../src/web/ui.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAOH,wBAAgB,IAAI,IAAI,MAAM,CA2O7B"}
|
package/dist/web/ui.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* model, which is one that is offline.
|
|
9
9
|
*/
|
|
10
10
|
import { MARK_MASK } from './mark.js';
|
|
11
|
+
import { WORK_WORDS } from '../work-words.js';
|
|
11
12
|
import { css } from './ui-css.js';
|
|
12
13
|
import { clientScript } from './ui-client.js';
|
|
13
14
|
export function page() {
|
|
@@ -51,6 +52,7 @@ export function page() {
|
|
|
51
52
|
<button class="tab" id="tab-settings">Settings</button>
|
|
52
53
|
</div>
|
|
53
54
|
<div class="spacer"></div>
|
|
55
|
+
<button class="pill" id="pfollow" title="Show the file being worked on beside the conversation">follow <b>on</b></button>
|
|
54
56
|
<button class="pill" id="pgoal" title="A standing objective, put in front of every message">goal <b>none</b></button>
|
|
55
57
|
<button class="pill" id="peffort" title="How hard to think">effort <b>medium</b></button>
|
|
56
58
|
<button class="pill" id="pmode" title="What KONECK may do without asking">mode <b>auto</b></button>
|
|
@@ -62,6 +64,7 @@ export function page() {
|
|
|
62
64
|
</header>
|
|
63
65
|
|
|
64
66
|
<div class="view" id="chatview">
|
|
67
|
+
<div class="chatsplit">
|
|
65
68
|
<div class="wrap" id="stream">
|
|
66
69
|
<div class="hero">
|
|
67
70
|
<div class="mark"></div>
|
|
@@ -74,6 +77,17 @@ export function page() {
|
|
|
74
77
|
</div>
|
|
75
78
|
</div>
|
|
76
79
|
</div>
|
|
80
|
+
<!-- The file being worked on, beside the conversation rather than behind a tab. A tool call
|
|
81
|
+
names a path; this shows it, and shows the diff once the file has actually been written. -->
|
|
82
|
+
<aside class="watch" id="watch" hidden>
|
|
83
|
+
<div class="whead">
|
|
84
|
+
<span class="wt" id="wtool">watching</span>
|
|
85
|
+
<span class="wp" id="wpath">—</span>
|
|
86
|
+
<button class="wx" id="wclose" title="Stop following (also the follow pill)">×</button>
|
|
87
|
+
</div>
|
|
88
|
+
<div class="wbody" id="wbody"></div>
|
|
89
|
+
</aside>
|
|
90
|
+
</div>
|
|
77
91
|
</div>
|
|
78
92
|
|
|
79
93
|
<div class="view" id="changesview" hidden></div>
|
|
@@ -199,7 +213,7 @@ export function page() {
|
|
|
199
213
|
<!-- Typing stays possible on purpose: a gateway may not publish a list at all, and a virtual
|
|
200
214
|
model such as OmniRoute's auto/best-coding is a routing instruction that appears in no
|
|
201
215
|
catalogue. The list below narrows as you type. -->
|
|
202
|
-
<input id="mmodel" placeholder="
|
|
216
|
+
<input id="mmodel" placeholder="search the list, or type a name to use" autocomplete="off" spellcheck="false">
|
|
203
217
|
<div class="mnote" id="mnote">Asking the provider what it serves…</div>
|
|
204
218
|
<div class="mlist" id="mlist"></div>
|
|
205
219
|
<div class="row"><button>Cancel</button><button class="go" id="msave">Switch</button></div>
|
|
@@ -225,6 +239,10 @@ export function page() {
|
|
|
225
239
|
</form></dialog>
|
|
226
240
|
|
|
227
241
|
<script>
|
|
242
|
+
/* The same vocabulary the terminal narrates with, embedded rather than fetched so the page still
|
|
243
|
+
has it on a machine with no network — which is the machine most likely to be running a local
|
|
244
|
+
model. Generated from src/work-words.ts, so there is one list, not two. */
|
|
245
|
+
const WORK_WORDS = ${JSON.stringify(WORK_WORDS)};
|
|
228
246
|
${clientScript()}
|
|
229
247
|
</script>
|
|
230
248
|
</body></html>`;
|
package/dist/web/ui.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ui.js","sourceRoot":"","sources":["../../src/web/ui.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAClC,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAE9C,MAAM,UAAU,IAAI;IAClB,OAAO;;;;;;;SAOA,GAAG,CAAC,SAAS,CAAC
|
|
1
|
+
{"version":3,"file":"ui.js","sourceRoot":"","sources":["../../src/web/ui.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAClC,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAE9C,MAAM,UAAU,IAAI;IAClB,OAAO;;;;;;;SAOA,GAAG,CAAC,SAAS,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBA+NF,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC;EAC7C,YAAY,EAAE;;eAED,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The words, in one place.
|
|
3
|
+
*
|
|
4
|
+
* These lived in the Ink component, so the browser had no way to reach them and showed a bare
|
|
5
|
+
* "working" instead — the same turn narrated with personality in one window and flatly in the
|
|
6
|
+
* other. Nothing here is terminal-specific: it is vocabulary and a rule for choosing from it.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* What KONECK says it is doing, and what it says it did.
|
|
10
|
+
*
|
|
11
|
+
* Grouped by how much work the turn turned out to be, so the wording matches the wait: a
|
|
12
|
+
* sub-three-second answer gets something snappy, a two-minute one gets something that sounds
|
|
13
|
+
* like effort, and a turn that had to recover from a failed tool gets a clutch verb. The word
|
|
14
|
+
* shown while working escalates through the tiers as time passes, and the finished row reports
|
|
15
|
+
* the tense that matches the real elapsed time.
|
|
16
|
+
*
|
|
17
|
+
* Apostrophes are ASCII on purpose: U+2019 is East Asian Ambiguous, the same class of character
|
|
18
|
+
* that used to tear the panel borders.
|
|
19
|
+
*/
|
|
20
|
+
export type WorkTier = 'reply' | 'quick' | 'normal' | 'heavy' | 'recovery';
|
|
21
|
+
export declare const WORK_WORDS: Record<WorkTier, Array<readonly [string, string]>>;
|
|
22
|
+
/** Which vocabulary the wait deserves. */
|
|
23
|
+
/** What a turn actually amounted to, which is what the wording should follow. */
|
|
24
|
+
export interface TurnEffort {
|
|
25
|
+
elapsedMs: number;
|
|
26
|
+
/** Tools the turn ran. Zero means it only thought and answered. */
|
|
27
|
+
tools: number;
|
|
28
|
+
/** Output tokens produced. */
|
|
29
|
+
tokens: number;
|
|
30
|
+
/** A tool failed and the turn recovered from it. */
|
|
31
|
+
recovered: boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Whether the turn earned a verb at all.
|
|
35
|
+
*
|
|
36
|
+
* "Cooked lekker in 5.0s" for answering "hi" is a boast about nothing: no tool ran, ten tokens
|
|
37
|
+
* came back, and the only reason it took five seconds is that the provider is slow. Elapsed time
|
|
38
|
+
* alone cannot tell effort from latency, so a turn that ran no tools and said almost nothing gets
|
|
39
|
+
* its timing reported plainly and no verb.
|
|
40
|
+
*/
|
|
41
|
+
export declare function shouldName(effort: TurnEffort): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Which family of words fits.
|
|
44
|
+
*
|
|
45
|
+
* Time still decides between quick, normal and heavy, but only once the turn has done something.
|
|
46
|
+
* A turn with no tools belongs to `reply` however long it took, because nothing was built and a
|
|
47
|
+
* cooking verb would be describing work that never happened.
|
|
48
|
+
*/
|
|
49
|
+
export declare function tierFor(effort: TurnEffort): WorkTier;
|
|
50
|
+
/**
|
|
51
|
+
* The gerund to show while working, and the past tense to report when the turn lands.
|
|
52
|
+
*
|
|
53
|
+
* The past tense is empty when the turn did not earn one, which the caller renders as timing
|
|
54
|
+
* alone.
|
|
55
|
+
*/
|
|
56
|
+
export declare function workWord(effort: TurnEffort, seed: number): readonly [string, string];
|
|
57
|
+
//# sourceMappingURL=work-words.d.ts.map
|