koneck 2.31.1 → 2.33.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/aside.d.ts +35 -0
- package/dist/aside.d.ts.map +1 -0
- package/dist/aside.js +69 -0
- package/dist/aside.js.map +1 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +48 -0
- package/dist/engine.js.map +1 -1
- package/dist/hooks.d.ts +110 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +258 -0
- package/dist/hooks.js.map +1 -0
- package/dist/ink-chat.d.ts +2 -51
- package/dist/ink-chat.d.ts.map +1 -1
- package/dist/ink-chat.js +27 -238
- package/dist/ink-chat.js.map +1 -1
- package/dist/types.d.ts +5 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/web/api.d.ts.map +1 -1
- package/dist/web/api.js +29 -0
- package/dist/web/api.js.map +1 -1
- package/dist/web/mark.d.ts +15 -9
- package/dist/web/mark.d.ts.map +1 -1
- package/dist/web/mark.js +72 -9
- package/dist/web/mark.js.map +1 -1
- package/dist/web/ui-client.d.ts.map +1 -1
- package/dist/web/ui-client.js +278 -28
- 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 +41 -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
|
@@ -279,6 +279,39 @@ function renderAsk(e) {
|
|
|
279
279
|
*/
|
|
280
280
|
let tick = null;
|
|
281
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
|
+
|
|
282
315
|
function setBusy(on, what) {
|
|
283
316
|
state.busy = on;
|
|
284
317
|
$('send').disabled = on || !$('ta').value.trim();
|
|
@@ -288,8 +321,20 @@ function setBusy(on, what) {
|
|
|
288
321
|
if (on) {
|
|
289
322
|
if (!tick) {
|
|
290
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);
|
|
291
328
|
tick = setInterval(() => {
|
|
292
|
-
|
|
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
|
+
}
|
|
293
338
|
}, 250);
|
|
294
339
|
}
|
|
295
340
|
} else if (tick) {
|
|
@@ -354,8 +399,11 @@ function onEvent(e) {
|
|
|
354
399
|
// shape:" as one paragraph.
|
|
355
400
|
state.replyEl = null;
|
|
356
401
|
state.thinkEl = null;
|
|
402
|
+
turn.tools += 1;
|
|
357
403
|
$('livetool').textContent = e.name + (e.detail ? ' ' + e.detail : '');
|
|
358
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);
|
|
359
407
|
break;
|
|
360
408
|
case 'tool-output':
|
|
361
409
|
if (state.lastTool) {
|
|
@@ -363,11 +411,15 @@ function onEvent(e) {
|
|
|
363
411
|
(state.lastTool.body.textContent + e.line + '\n').slice(-5000);
|
|
364
412
|
}
|
|
365
413
|
break;
|
|
366
|
-
case 'tool-done':
|
|
414
|
+
case 'tool-done': {
|
|
367
415
|
finishTool(e);
|
|
368
416
|
setBusy(true, 'thinking');
|
|
369
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);
|
|
370
421
|
break;
|
|
422
|
+
}
|
|
371
423
|
case 'plan': renderPlan(e.steps); break;
|
|
372
424
|
case 'agents': renderAgents(e.agents); break;
|
|
373
425
|
case 'tokens':
|
|
@@ -383,7 +435,16 @@ function onEvent(e) {
|
|
|
383
435
|
// The server says a turn touched files, so the badge needs no polling to be right.
|
|
384
436
|
$('tab-changes').innerHTML = 'Changes <span class="n">' + e.files + '</span>';
|
|
385
437
|
break;
|
|
386
|
-
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);
|
|
387
448
|
setBusy(false);
|
|
388
449
|
state.replyEl = null; state.thinkEl = null;
|
|
389
450
|
if (state.view === 'changes') loadChanges();
|
|
@@ -393,6 +454,7 @@ function onEvent(e) {
|
|
|
393
454
|
if (mine) setPosture(mine);
|
|
394
455
|
}).catch(() => {});
|
|
395
456
|
break;
|
|
457
|
+
}
|
|
396
458
|
}
|
|
397
459
|
}
|
|
398
460
|
|
|
@@ -1337,8 +1399,43 @@ const SLASH = {
|
|
|
1337
1399
|
'/stop': () => $('stop').click(),
|
|
1338
1400
|
};
|
|
1339
1401
|
|
|
1402
|
+
/**
|
|
1403
|
+
* Asks a question beside the running turn.
|
|
1404
|
+
*
|
|
1405
|
+
* Deliberately not routed through /api/send: the point of a side question is that the conversation
|
|
1406
|
+
* does not learn about it, so a long build keeps reasoning over the history it had. It also works
|
|
1407
|
+
* while the agent is busy, which is when it is actually wanted.
|
|
1408
|
+
*/
|
|
1409
|
+
async function sendAside(question) {
|
|
1410
|
+
clearHero();
|
|
1411
|
+
if (!question) {
|
|
1412
|
+
add(el('div', 'notice', 'Ask something after /aside — it answers beside the running turn, '
|
|
1413
|
+
+ 'read-only, without interrupting it.'));
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
const box = el('div', 'aside');
|
|
1417
|
+
box.appendChild(el('div', 'ah', 'aside · ' + question));
|
|
1418
|
+
const body = el('div', 'ab', 'asking…');
|
|
1419
|
+
box.appendChild(body);
|
|
1420
|
+
add(box);
|
|
1421
|
+
let out;
|
|
1422
|
+
try {
|
|
1423
|
+
out = await api('/api/aside', { method: 'POST',
|
|
1424
|
+
body: JSON.stringify({ session: state.session, question: question }) });
|
|
1425
|
+
} catch (e) {
|
|
1426
|
+
body.className = 'ab err';
|
|
1427
|
+
body.textContent = e.message;
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
body.innerHTML = md(out.answer);
|
|
1431
|
+
const foot = el('div', 'af', ms(out.ms) + (out.tools.length ? ' · ' + out.tools.join(', ') : ''));
|
|
1432
|
+
box.appendChild(foot);
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1340
1435
|
function runSlash(text) {
|
|
1341
1436
|
const word = text.trim().split(/\s+/)[0].toLowerCase();
|
|
1437
|
+
// /aside carries an argument, so it is handled before the no-argument table.
|
|
1438
|
+
if (word === '/aside') { void sendAside(text.trim().slice(6).trim()); return true; }
|
|
1342
1439
|
const found = SLASH[word];
|
|
1343
1440
|
if (found) { found(); return true; }
|
|
1344
1441
|
if (word.startsWith('/')) {
|
|
@@ -1354,6 +1451,8 @@ function runSlash(text) {
|
|
|
1354
1451
|
/* ── command palette ───────────────────────────────────────── */
|
|
1355
1452
|
const COMMANDS = [
|
|
1356
1453
|
{ k: 'New session', d: 'start a fresh conversation', g: 'session', run: () => newSession() },
|
|
1454
|
+
{ k: 'Ask aside', d: 'a read-only question answered without interrupting the run', g: 'session',
|
|
1455
|
+
run: () => { const q = prompt('Ask beside the running turn:'); if (q) void sendAside(q.trim()); } },
|
|
1357
1456
|
{ k: 'Chat', d: 'the conversation', g: 'view', run: () => setView('chat') },
|
|
1358
1457
|
{ k: 'Changes', d: 'what this session changed, overall and per turn', g: 'view',
|
|
1359
1458
|
run: () => setView('changes') },
|
|
@@ -1750,10 +1849,136 @@ $('pmode').onclick = async () => {
|
|
|
1750
1849
|
}
|
|
1751
1850
|
$('modedlg').showModal();
|
|
1752
1851
|
};
|
|
1852
|
+
/* ── following the file being worked on ───────────────────────────────────────────────────── */
|
|
1853
|
+
|
|
1854
|
+
/**
|
|
1855
|
+
* The panel beside the conversation, showing whatever the agent is touching.
|
|
1856
|
+
*
|
|
1857
|
+
* On by default, because the interesting part of watching an agent work is watching files change,
|
|
1858
|
+
* and a view you have to go and click is one you find out about after the fact. Remembered per
|
|
1859
|
+
* browser: a preference about a layout belongs to whoever is looking at it.
|
|
1860
|
+
*/
|
|
1861
|
+
const follow = {
|
|
1862
|
+
on: (() => { try { return localStorage.getItem('koneck.follow') !== 'off'; } catch (e) { return true; } })(),
|
|
1863
|
+
path: null,
|
|
1864
|
+
/** Set while a fetch is in flight, so a burst of tool calls does not queue a burst of requests. */
|
|
1865
|
+
busy: false,
|
|
1866
|
+
/** The path a newer tool named while a fetch was running. */
|
|
1867
|
+
pending: null,
|
|
1868
|
+
};
|
|
1869
|
+
|
|
1870
|
+
/** The file a tool is acting on, from its arguments. Null when it is not about a file. */
|
|
1871
|
+
function pathFromTool(e) {
|
|
1872
|
+
let args = e.args;
|
|
1873
|
+
if (!args && e.detail) args = null;
|
|
1874
|
+
if (args) {
|
|
1875
|
+
try {
|
|
1876
|
+
const parsed = JSON.parse(args);
|
|
1877
|
+
for (const key of ['path', 'file', 'filename', 'file_path']) {
|
|
1878
|
+
if (typeof parsed[key] === 'string' && parsed[key].trim()) return parsed[key].trim();
|
|
1879
|
+
}
|
|
1880
|
+
if (Array.isArray(parsed.paths) && typeof parsed.paths[0] === 'string') return parsed.paths[0];
|
|
1881
|
+
} catch (err) { /* not JSON: fall through to the one-line detail */ }
|
|
1882
|
+
}
|
|
1883
|
+
// The detail line is often just the path, which is enough to follow by.
|
|
1884
|
+
if (e.detail && /[./]/.test(e.detail) && !/\s/.test(e.detail.trim())) return e.detail.trim();
|
|
1885
|
+
return null;
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
/** Whether a tool changes a file, which decides whether the panel shows content or a diff. */
|
|
1889
|
+
function toolWrites(name) {
|
|
1890
|
+
return /write|edit|patch|replace|apply|create|delete|move|rename|format/i.test(name || '');
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
function setFollow(on) {
|
|
1894
|
+
follow.on = on;
|
|
1895
|
+
try { localStorage.setItem('koneck.follow', on ? 'on' : 'off'); } catch (e) {}
|
|
1896
|
+
$('pfollow').innerHTML = 'follow <b>' + (on ? 'on' : 'off') + '</b>';
|
|
1897
|
+
$('watch').hidden = !on || !follow.path;
|
|
1898
|
+
if (on && follow.path) void showWatched(follow.path, null);
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
/** Points the panel at a path. Cheap to call repeatedly; only the newest target is fetched. */
|
|
1902
|
+
function watchPath(path, toolName, wrote) {
|
|
1903
|
+
if (!follow.on || !path) return;
|
|
1904
|
+
follow.path = path;
|
|
1905
|
+
$('watch').hidden = false;
|
|
1906
|
+
$('wpath').textContent = path;
|
|
1907
|
+
$('wtool').textContent = toolName || 'watching';
|
|
1908
|
+
if (follow.busy) { follow.pending = [path, wrote]; return; }
|
|
1909
|
+
void showWatched(path, wrote);
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
/**
|
|
1913
|
+
* Draws the panel.
|
|
1914
|
+
*
|
|
1915
|
+
* A file that was only read is shown as itself. A file that was just written is shown as its diff,
|
|
1916
|
+
* because what changed is the thing worth seeing and reading the whole file to find three new
|
|
1917
|
+
* lines is work the panel should be doing.
|
|
1918
|
+
*/
|
|
1919
|
+
async function showWatched(path, wrote) {
|
|
1920
|
+
follow.busy = true;
|
|
1921
|
+
const host = $('wbody');
|
|
1922
|
+
try {
|
|
1923
|
+
if (wrote && state.session) {
|
|
1924
|
+
let ch = null;
|
|
1925
|
+
try {
|
|
1926
|
+
const out = await api('/api/changes?session=' + encodeURIComponent(state.session));
|
|
1927
|
+
ch = (out.changes || []).find((c) => c.path === path || path.endsWith(c.path));
|
|
1928
|
+
} catch (e) { /* fall through to showing the file */ }
|
|
1929
|
+
if (ch && ch.diff) {
|
|
1930
|
+
host.innerHTML = '';
|
|
1931
|
+
const head = el('div', 'wempty');
|
|
1932
|
+
head.style.padding = '8px 12px';
|
|
1933
|
+
head.textContent = '+' + ch.added + ' \u2212' + ch.removed + ' ' + ch.kind;
|
|
1934
|
+
host.appendChild(head);
|
|
1935
|
+
host.appendChild(renderDiff(ch.diff));
|
|
1936
|
+
return;
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
let data;
|
|
1940
|
+
try { data = await api('/api/file?path=' + encodeURIComponent(path)); }
|
|
1941
|
+
catch (e) {
|
|
1942
|
+
host.innerHTML = '';
|
|
1943
|
+
host.appendChild(el('div', 'wempty', e.message));
|
|
1944
|
+
return;
|
|
1945
|
+
}
|
|
1946
|
+
host.innerHTML = '';
|
|
1947
|
+
if (data.binary || data.text === null) {
|
|
1948
|
+
host.appendChild(el('div', 'wempty', 'Binary file — nothing to show as text.'));
|
|
1949
|
+
return;
|
|
1950
|
+
}
|
|
1951
|
+
const code = el('div', 'code');
|
|
1952
|
+
const lines = data.text.split('\n');
|
|
1953
|
+
code.appendChild(el('div', 'gut', lines.map((_, i) => i + 1).join('\n')));
|
|
1954
|
+
code.appendChild(el('div', 'src', data.text));
|
|
1955
|
+
host.appendChild(code);
|
|
1956
|
+
} finally {
|
|
1957
|
+
follow.busy = false;
|
|
1958
|
+
// A newer target arrived while this was in flight: honour the last one and drop the rest,
|
|
1959
|
+
// which is what keeps a run of twenty edits to twenty files at one request rather than twenty.
|
|
1960
|
+
if (follow.pending) {
|
|
1961
|
+
const next = follow.pending;
|
|
1962
|
+
follow.pending = null;
|
|
1963
|
+
if (next[0] !== path || next[1]) void showWatched(next[0], next[1]);
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
$('pfollow').onclick = () => setFollow(!follow.on);
|
|
1969
|
+
$('wclose').onclick = () => setFollow(false);
|
|
1970
|
+
|
|
1753
1971
|
/* ── choosing a model ────────────────────────────────────────────────────────────────────── */
|
|
1754
1972
|
|
|
1755
|
-
/**
|
|
1756
|
-
|
|
1973
|
+
/**
|
|
1974
|
+
* The provider's own listing, kept so filtering does not re-ask on every keystroke.
|
|
1975
|
+
*
|
|
1976
|
+
* The chosen entry is the model that will be switched to, and it is deliberately NOT the search
|
|
1977
|
+
* box. They were the same field at first, so opening the dialog pre-filled the box with the model
|
|
1978
|
+
* already in use — and since the box also filtered, 476 models were immediately narrowed to the
|
|
1979
|
+
* one you were on. It looked exactly like a text field with no choice in it, because it was.
|
|
1980
|
+
*/
|
|
1981
|
+
const catalog = { provider: null, entries: [], listed: false, note: '', chosen: '' };
|
|
1757
1982
|
|
|
1758
1983
|
/** Fills the provider dropdown from whatever the registry holds, declared entries included. */
|
|
1759
1984
|
async function fillProviders(selected) {
|
|
@@ -1775,46 +2000,61 @@ async function fillProviders(selected) {
|
|
|
1775
2000
|
return info;
|
|
1776
2001
|
}
|
|
1777
2002
|
|
|
1778
|
-
/**
|
|
2003
|
+
/** What Switch will use: whatever row is picked, or a name typed in full when nothing is. */
|
|
2004
|
+
function chosenModel() {
|
|
2005
|
+
const typed = $('mmodel').value.trim();
|
|
2006
|
+
// A provider that publishes no list leaves the box as the only way in, so there the box IS the
|
|
2007
|
+
// answer. Otherwise a picked row wins, and typing only narrows the list.
|
|
2008
|
+
if (!catalog.listed) return typed;
|
|
2009
|
+
if (catalog.chosen) return catalog.chosen;
|
|
2010
|
+
return typed;
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
/** Draws the listing, narrowed by the search box. */
|
|
1779
2014
|
function drawModels() {
|
|
1780
2015
|
const host = $('mlist');
|
|
1781
2016
|
const typed = $('mmodel').value.trim().toLowerCase();
|
|
1782
2017
|
host.innerHTML = '';
|
|
2018
|
+
if (!catalog.listed) { $('mnote').textContent = catalog.note || ''; return; }
|
|
2019
|
+
|
|
1783
2020
|
const shown = catalog.entries.filter((m) => !typed || m.id.toLowerCase().includes(typed));
|
|
2021
|
+
const total = catalog.entries.length;
|
|
1784
2022
|
|
|
1785
|
-
if (!catalog.listed) { $('mnote').textContent = catalog.note || ''; return; }
|
|
1786
2023
|
if (shown.length === 0) {
|
|
1787
|
-
|
|
2024
|
+
// Not a dead end: a gateway may serve something it does not advertise, so an unmatched name
|
|
2025
|
+
// stays usable — but it has to be said out loud rather than silently accepted.
|
|
2026
|
+
$('mnote').textContent = 'Nothing in ' + total + ' matches. Switch will use it as typed.';
|
|
2027
|
+
catalog.chosen = '';
|
|
1788
2028
|
return;
|
|
1789
2029
|
}
|
|
1790
|
-
$('mnote').textContent = shown.length ===
|
|
1791
|
-
|
|
1792
|
-
: shown.length + ' of ' + catalog.entries.length + ' models';
|
|
2030
|
+
$('mnote').textContent = (shown.length === total ? total + ' models' : shown.length + ' of ' + total)
|
|
2031
|
+
+ (catalog.chosen ? ' · using ' + catalog.chosen : ' · click one to choose');
|
|
1793
2032
|
|
|
1794
|
-
// Grouped by vendor, which is what turns a flat several-hundred-entry list into something
|
|
1795
|
-
//
|
|
1796
|
-
let group = null;
|
|
2033
|
+
// Grouped by vendor, which is what turns a flat several-hundred-entry list into something you
|
|
2034
|
+
// can actually read.
|
|
2035
|
+
let group = null, first = null;
|
|
1797
2036
|
for (const m of shown.slice(0, 400)) {
|
|
1798
2037
|
if (m.vendor !== group) {
|
|
1799
2038
|
group = m.vendor;
|
|
1800
2039
|
host.appendChild(el('div', 'mgroup', group));
|
|
1801
2040
|
}
|
|
1802
|
-
const
|
|
2041
|
+
const on = m.id === catalog.chosen;
|
|
2042
|
+
const row = el('div', 'mrow' + (on ? ' on' : ''));
|
|
1803
2043
|
row.appendChild(el('div', 'mid', m.id));
|
|
1804
2044
|
row.appendChild(el('div', 'mdet', m.detail));
|
|
1805
|
-
row.onclick = () => {
|
|
1806
|
-
$('mmodel').value = m.id;
|
|
1807
|
-
drawModels();
|
|
1808
|
-
};
|
|
2045
|
+
row.onclick = () => { catalog.chosen = m.id; drawModels(); };
|
|
1809
2046
|
host.appendChild(row);
|
|
2047
|
+
if (on && !first) first = row;
|
|
1810
2048
|
}
|
|
1811
2049
|
if (shown.length > 400) {
|
|
1812
2050
|
host.appendChild(el('div', 'mgroup', 'and ' + (shown.length - 400) + ' more — keep typing'));
|
|
1813
2051
|
}
|
|
2052
|
+
// The model in use should be in view when the dialog opens, not hundreds of rows down.
|
|
2053
|
+
if (first) first.scrollIntoView({ block: 'center' });
|
|
1814
2054
|
}
|
|
1815
2055
|
|
|
1816
2056
|
/** Asks the selected provider what it serves. */
|
|
1817
|
-
async function loadModels(provider) {
|
|
2057
|
+
async function loadModels(provider, preselect) {
|
|
1818
2058
|
if (catalog.provider === provider) { drawModels(); return; }
|
|
1819
2059
|
$('mlist').innerHTML = '';
|
|
1820
2060
|
$('mnote').textContent = 'Asking ' + provider + ' what it serves…';
|
|
@@ -1823,6 +2063,9 @@ async function loadModels(provider) {
|
|
|
1823
2063
|
catch (e) {
|
|
1824
2064
|
catalog.provider = provider; catalog.entries = []; catalog.listed = false;
|
|
1825
2065
|
catalog.note = e.message;
|
|
2066
|
+
catalog.chosen = '';
|
|
2067
|
+
// With no list, the box is the only way to name a model, so it gets the current one to edit.
|
|
2068
|
+
if (preselect) $('mmodel').value = preselect;
|
|
1826
2069
|
drawModels();
|
|
1827
2070
|
return;
|
|
1828
2071
|
}
|
|
@@ -1830,24 +2073,28 @@ async function loadModels(provider) {
|
|
|
1830
2073
|
catalog.entries = out.entries || [];
|
|
1831
2074
|
catalog.listed = !!out.listed;
|
|
1832
2075
|
catalog.note = out.note || '';
|
|
2076
|
+
// Preselect only if the provider actually serves it: carrying a name across providers is how a
|
|
2077
|
+
// session ends up asking Anthropic for qwen3-coder.
|
|
2078
|
+
catalog.chosen = preselect && catalog.entries.some((m) => m.id === preselect) ? preselect : '';
|
|
2079
|
+
if (!catalog.listed && preselect) $('mmodel').value = preselect;
|
|
1833
2080
|
drawModels();
|
|
1834
2081
|
}
|
|
1835
2082
|
|
|
1836
2083
|
$('pmodel').onclick = async () => {
|
|
1837
2084
|
const info = await fillProviders(state.provider);
|
|
1838
2085
|
if (!info) return;
|
|
1839
|
-
|
|
2086
|
+
// Empty, because this box searches. Pre-filling it with the model already in use filtered the
|
|
2087
|
+
// list down to that one model and made the whole listing look like it was not there.
|
|
2088
|
+
$('mmodel').value = '';
|
|
1840
2089
|
catalog.provider = null; // re-ask: a key may have been set since last time
|
|
1841
2090
|
$('mdlg').showModal();
|
|
1842
|
-
void loadModels($('mprov').value);
|
|
2091
|
+
void loadModels($('mprov').value, state.model === '-' ? '' : state.model);
|
|
1843
2092
|
};
|
|
1844
2093
|
$('mprov').onchange = () => {
|
|
1845
|
-
// A different provider serves different models,
|
|
1846
|
-
// the name of a model the previous one happened to serve.
|
|
1847
|
-
const chosen = $('mprov').selectedOptions[0];
|
|
2094
|
+
// A different provider serves different models, so nothing carries over.
|
|
1848
2095
|
$('mmodel').value = '';
|
|
1849
|
-
|
|
1850
|
-
|
|
2096
|
+
catalog.chosen = '';
|
|
2097
|
+
void loadModels($('mprov').value, '');
|
|
1851
2098
|
};
|
|
1852
2099
|
$('mmodel').oninput = () => drawModels();
|
|
1853
2100
|
|
|
@@ -1909,7 +2156,7 @@ async function afterProviderSaved(out) {
|
|
|
1909
2156
|
}
|
|
1910
2157
|
|
|
1911
2158
|
$('msave').onclick = async () => {
|
|
1912
|
-
const provider = $('mprov').value, model =
|
|
2159
|
+
const provider = $('mprov').value, model = chosenModel();
|
|
1913
2160
|
if (!state.session) await newSession();
|
|
1914
2161
|
let info;
|
|
1915
2162
|
try {
|
|
@@ -1923,6 +2170,9 @@ for (const b of document.querySelectorAll('.hero .ex button')) {
|
|
|
1923
2170
|
b.onclick = () => send(b.textContent);
|
|
1924
2171
|
}
|
|
1925
2172
|
|
|
2173
|
+
// The pill has to say what the remembered setting actually is before anything is asked.
|
|
2174
|
+
setFollow(follow.on);
|
|
2175
|
+
|
|
1926
2176
|
(async function start() {
|
|
1927
2177
|
let hello;
|
|
1928
2178
|
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAioElB,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,CA8iB3C"}
|
package/dist/web/ui-css.js
CHANGED
|
@@ -518,6 +518,47 @@ 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
|
+
/*
|
|
548
|
+
* A side question, visibly beside the conversation rather than in it — because that is exactly
|
|
549
|
+
* what it is: the agent's own history never sees it.
|
|
550
|
+
*/
|
|
551
|
+
.aside{border:1px dashed var(--line);border-left:2px solid var(--violet);border-radius:8px;
|
|
552
|
+
padding:10px 13px;margin:10px 0;background:var(--panel-2)}
|
|
553
|
+
.aside .ah{font-size:10.5px;letter-spacing:.06em;text-transform:uppercase;color:var(--violet);
|
|
554
|
+
margin-bottom:7px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
555
|
+
.aside .ab{font-size:13px;line-height:1.6}
|
|
556
|
+
.aside .ab.err{color:var(--red)}
|
|
557
|
+
.aside .af{margin-top:7px;font-size:11px;color:var(--dim);font-family:var(--mono)}
|
|
558
|
+
|
|
559
|
+
/* What the turn came to, in the tense that matches — the line the terminal has always printed. */
|
|
560
|
+
.landed{font-size:11.5px;color:var(--dim);font-family:var(--mono);margin:2px 0 14px;
|
|
561
|
+
letter-spacing:.02em}
|
|
521
562
|
.dlg .row{display:flex;gap:8px;justify-content:flex-end}
|
|
522
563
|
.dlg .row button{padding:7px 15px;border-radius:8px;border:1px solid var(--line)}
|
|
523
564
|
.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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkiBtB,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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"work-words.d.ts","sourceRoot":"","sources":["../src/work-words.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,GAAG,UAAU,CAAC;AAE3E,eAAO,MAAM,UAAU,EAAE,MAAM,CAAC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAqLzE,CAAC;AAEF,0CAA0C;AAC1C,iFAAiF;AACjF,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,mEAAmE;IACnE,KAAK,EAAE,MAAM,CAAC;IACd,8BAA8B;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,oDAAoD;IACpD,SAAS,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAItD;AAED;;;;;;GAMG;AACH,wBAAgB,OAAO,CAAC,MAAM,EAAE,UAAU,GAAG,QAAQ,CAMpD;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAIpF"}
|