koneck 2.31.1 → 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/ui-client.d.ts.map +1 -1
- package/dist/web/ui-client.js +241 -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 +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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ui-client.d.ts","sourceRoot":"","sources":["../../src/web/ui-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,IAAI,MAAM,
|
|
1
|
+
{"version":3,"file":"ui-client.d.ts","sourceRoot":"","sources":["../../src/web/ui-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,IAAI,MAAM,CA8lErC"}
|
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
|
|
|
@@ -1750,10 +1812,136 @@ $('pmode').onclick = async () => {
|
|
|
1750
1812
|
}
|
|
1751
1813
|
$('modedlg').showModal();
|
|
1752
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
|
+
|
|
1753
1934
|
/* ── choosing a model ────────────────────────────────────────────────────────────────────── */
|
|
1754
1935
|
|
|
1755
|
-
/**
|
|
1756
|
-
|
|
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: '' };
|
|
1757
1945
|
|
|
1758
1946
|
/** Fills the provider dropdown from whatever the registry holds, declared entries included. */
|
|
1759
1947
|
async function fillProviders(selected) {
|
|
@@ -1775,46 +1963,61 @@ async function fillProviders(selected) {
|
|
|
1775
1963
|
return info;
|
|
1776
1964
|
}
|
|
1777
1965
|
|
|
1778
|
-
/**
|
|
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. */
|
|
1779
1977
|
function drawModels() {
|
|
1780
1978
|
const host = $('mlist');
|
|
1781
1979
|
const typed = $('mmodel').value.trim().toLowerCase();
|
|
1782
1980
|
host.innerHTML = '';
|
|
1981
|
+
if (!catalog.listed) { $('mnote').textContent = catalog.note || ''; return; }
|
|
1982
|
+
|
|
1783
1983
|
const shown = catalog.entries.filter((m) => !typed || m.id.toLowerCase().includes(typed));
|
|
1984
|
+
const total = catalog.entries.length;
|
|
1784
1985
|
|
|
1785
|
-
if (!catalog.listed) { $('mnote').textContent = catalog.note || ''; return; }
|
|
1786
1986
|
if (shown.length === 0) {
|
|
1787
|
-
|
|
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 = '';
|
|
1788
1991
|
return;
|
|
1789
1992
|
}
|
|
1790
|
-
$('mnote').textContent = shown.length ===
|
|
1791
|
-
|
|
1792
|
-
: 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');
|
|
1793
1995
|
|
|
1794
|
-
// Grouped by vendor, which is what turns a flat several-hundred-entry list into something
|
|
1795
|
-
//
|
|
1796
|
-
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;
|
|
1797
1999
|
for (const m of shown.slice(0, 400)) {
|
|
1798
2000
|
if (m.vendor !== group) {
|
|
1799
2001
|
group = m.vendor;
|
|
1800
2002
|
host.appendChild(el('div', 'mgroup', group));
|
|
1801
2003
|
}
|
|
1802
|
-
const
|
|
2004
|
+
const on = m.id === catalog.chosen;
|
|
2005
|
+
const row = el('div', 'mrow' + (on ? ' on' : ''));
|
|
1803
2006
|
row.appendChild(el('div', 'mid', m.id));
|
|
1804
2007
|
row.appendChild(el('div', 'mdet', m.detail));
|
|
1805
|
-
row.onclick = () => {
|
|
1806
|
-
$('mmodel').value = m.id;
|
|
1807
|
-
drawModels();
|
|
1808
|
-
};
|
|
2008
|
+
row.onclick = () => { catalog.chosen = m.id; drawModels(); };
|
|
1809
2009
|
host.appendChild(row);
|
|
2010
|
+
if (on && !first) first = row;
|
|
1810
2011
|
}
|
|
1811
2012
|
if (shown.length > 400) {
|
|
1812
2013
|
host.appendChild(el('div', 'mgroup', 'and ' + (shown.length - 400) + ' more — keep typing'));
|
|
1813
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' });
|
|
1814
2017
|
}
|
|
1815
2018
|
|
|
1816
2019
|
/** Asks the selected provider what it serves. */
|
|
1817
|
-
async function loadModels(provider) {
|
|
2020
|
+
async function loadModels(provider, preselect) {
|
|
1818
2021
|
if (catalog.provider === provider) { drawModels(); return; }
|
|
1819
2022
|
$('mlist').innerHTML = '';
|
|
1820
2023
|
$('mnote').textContent = 'Asking ' + provider + ' what it serves…';
|
|
@@ -1823,6 +2026,9 @@ async function loadModels(provider) {
|
|
|
1823
2026
|
catch (e) {
|
|
1824
2027
|
catalog.provider = provider; catalog.entries = []; catalog.listed = false;
|
|
1825
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;
|
|
1826
2032
|
drawModels();
|
|
1827
2033
|
return;
|
|
1828
2034
|
}
|
|
@@ -1830,24 +2036,28 @@ async function loadModels(provider) {
|
|
|
1830
2036
|
catalog.entries = out.entries || [];
|
|
1831
2037
|
catalog.listed = !!out.listed;
|
|
1832
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;
|
|
1833
2043
|
drawModels();
|
|
1834
2044
|
}
|
|
1835
2045
|
|
|
1836
2046
|
$('pmodel').onclick = async () => {
|
|
1837
2047
|
const info = await fillProviders(state.provider);
|
|
1838
2048
|
if (!info) return;
|
|
1839
|
-
|
|
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 = '';
|
|
1840
2052
|
catalog.provider = null; // re-ask: a key may have been set since last time
|
|
1841
2053
|
$('mdlg').showModal();
|
|
1842
|
-
void loadModels($('mprov').value);
|
|
2054
|
+
void loadModels($('mprov').value, state.model === '-' ? '' : state.model);
|
|
1843
2055
|
};
|
|
1844
2056
|
$('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];
|
|
2057
|
+
// A different provider serves different models, so nothing carries over.
|
|
1848
2058
|
$('mmodel').value = '';
|
|
1849
|
-
|
|
1850
|
-
|
|
2059
|
+
catalog.chosen = '';
|
|
2060
|
+
void loadModels($('mprov').value, '');
|
|
1851
2061
|
};
|
|
1852
2062
|
$('mmodel').oninput = () => drawModels();
|
|
1853
2063
|
|
|
@@ -1909,7 +2119,7 @@ async function afterProviderSaved(out) {
|
|
|
1909
2119
|
}
|
|
1910
2120
|
|
|
1911
2121
|
$('msave').onclick = async () => {
|
|
1912
|
-
const provider = $('mprov').value, model =
|
|
2122
|
+
const provider = $('mprov').value, model = chosenModel();
|
|
1913
2123
|
if (!state.session) await newSession();
|
|
1914
2124
|
let info;
|
|
1915
2125
|
try {
|
|
@@ -1923,6 +2133,9 @@ for (const b of document.querySelectorAll('.hero .ex button')) {
|
|
|
1923
2133
|
b.onclick = () => send(b.textContent);
|
|
1924
2134
|
}
|
|
1925
2135
|
|
|
2136
|
+
// The pill has to say what the remembered setting actually is before anything is asked.
|
|
2137
|
+
setFollow(follow.on);
|
|
2138
|
+
|
|
1926
2139
|
(async function start() {
|
|
1927
2140
|
let hello;
|
|
1928
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
|
|
@@ -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"}
|