koneck 2.34.0 → 2.35.1

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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"router-failure.js","sourceRoot":"","sources":["../src/router-failure.ts"],"names":[],"mappings":"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"}
@@ -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,CAmoErC"}
1
+ {"version":3,"file":"ui-client.d.ts","sourceRoot":"","sources":["../../src/web/ui-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAk1ErC"}
@@ -83,11 +83,14 @@ const el = (tag, cls, text) => {
83
83
  };
84
84
  const mark = (cls) => el('div', 'mark' + (cls ? ' ' + cls : ''));
85
85
  const stream = () => $('stream');
86
+ // The conversation column, which is what scrolls. It used to be the whole chat tab, and the file
87
+ // panel beside it scrolled along with the transcript instead of on its own.
88
+ const conv = () => $('conv');
86
89
  const atBottom = () => {
87
- const f = $('chatview');
90
+ const f = conv();
88
91
  return f.scrollHeight - f.scrollTop - f.clientHeight < 100;
89
92
  };
90
- const stick = (was) => { if (was) $('chatview').scrollTop = $('chatview').scrollHeight; };
93
+ const stick = (was) => { if (was) conv().scrollTop = conv().scrollHeight; };
91
94
  const add = (node) => { const was = atBottom(); stream().appendChild(node); stick(was); return node; };
92
95
  const clearHero = () => { const h = stream().querySelector('.hero'); if (h) h.remove(); };
93
96
  /**
@@ -402,22 +405,24 @@ function onEvent(e) {
402
405
  turn.tools += 1;
403
406
  $('livetool').textContent = e.name + (e.detail ? ' ' + e.detail : '');
404
407
  renderTool(e);
405
- // Follow it: the path a tool names is the file to be looking at.
406
- watchPath(pathFromTool(e), e.name, false);
408
+ // Follow it: not just which file, but which lines and which pattern.
409
+ watchTool(e, false);
407
410
  break;
408
411
  case 'tool-output':
409
412
  if (state.lastTool) {
410
413
  state.lastTool.body.textContent =
411
414
  (state.lastTool.body.textContent + e.line + '\n').slice(-5000);
412
415
  }
416
+ // And in the panel, where a running command is the thing being watched.
417
+ watchOutput(e.line);
413
418
  break;
414
419
  case 'tool-done': {
415
420
  finishTool(e);
416
421
  setBusy(true, 'thinking');
417
422
  $('livetool').textContent = '';
418
423
  // 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);
424
+ watchTool(e, true);
425
+ stopWatchClock();
421
426
  break;
422
427
  }
423
428
  case 'plan': renderPlan(e.steps); break;
@@ -1627,7 +1632,7 @@ async function openPast(file) {
1627
1632
  if (!data.entries.length) {
1628
1633
  stream().appendChild(el('div', 'empty', 'This session has no transcript in it.'));
1629
1634
  }
1630
- $('chatview').scrollTop = 0;
1635
+ conv().scrollTop = 0;
1631
1636
  }
1632
1637
 
1633
1638
  async function refreshSessions() {
@@ -1849,40 +1854,59 @@ $('pmode').onclick = async () => {
1849
1854
  }
1850
1855
  $('modedlg').showModal();
1851
1856
  };
1852
- /* ── following the file being worked on ───────────────────────────────────────────────────── */
1857
+ /* ── watching it work ─────────────────────────────────────────────────────────────────────── */
1853
1858
 
1854
1859
  /**
1855
- * The panel beside the conversation, showing whatever the agent is touching.
1860
+ * The panel beside the conversation, showing what the agent is doing while it does it.
1856
1861
  *
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.
1862
+ * Not "the last file it mentioned". A tool call says more than which file: read_files says which
1863
+ * LINES, grep_search says which pattern, execute_command streams its output as it runs. All of that
1864
+ * was already arriving and being thrown away, and the difference between showing a file and showing
1865
+ * the four lines actually read is the difference between a decoration and a view.
1866
+ *
1867
+ * On by default, remembered per browser, and closable three ways — the pill, the panel's own
1868
+ * button, and Escape — because a panel that follows you around had better be easy to stop.
1860
1869
  */
1861
1870
  const follow = {
1862
1871
  on: (() => { try { return localStorage.getItem('koneck.follow') !== 'off'; } catch (e) { return true; } })(),
1863
1872
  path: null,
1864
- /** Set while a fetch is in flight, so a burst of tool calls does not queue a burst of requests. */
1873
+ /** 'code' | 'diff' | 'term' what the body is currently showing. */
1874
+ kind: null,
1875
+ verb: '',
1865
1876
  busy: false,
1866
- /** The path a newer tool named while a fetch was running. */
1867
1877
  pending: null,
1878
+ /** Line numbers under attention, one-based. */
1879
+ attention: [],
1880
+ /** Text to mark inside those lines: a grep pattern, or the text a replace is looking for. */
1881
+ needle: null,
1882
+ /** Lines of a running command's output, in arrival order. */
1883
+ output: [],
1884
+ /** Everything this turn has touched: { label, path, verb, kind, ok }. */
1885
+ trail: [],
1886
+ startedAt: 0,
1887
+ timer: null,
1868
1888
  };
1869
1889
 
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;
1890
+ /** How many output lines a command keeps in view. Beyond this the head is dropped. */
1891
+ const WATCH_OUTPUT_LINES = 400;
1892
+
1893
+ /** What a tool is doing, as a word. The verb is the most informative thing in the header. */
1894
+ function verbFor(name) {
1895
+ const n = String(name || '');
1896
+ if (/^read_files$/.test(n)) return 'reading';
1897
+ if (/grep|search_files/.test(n)) return 'searching';
1898
+ if (/glob/.test(n)) return 'matching';
1899
+ if (/list_dir/.test(n)) return 'listing';
1900
+ if (/^write_file$/.test(n)) return 'writing';
1901
+ if (/edit|patch|apply_diff|search_replace/.test(n)) return 'editing';
1902
+ if (/execute_command|run/.test(n)) return 'running';
1903
+ if (/delete/.test(n)) return 'deleting';
1904
+ if (/move|rename/.test(n)) return 'moving';
1905
+ if (/^git_/.test(n)) return n.replace('git_', 'git ');
1906
+ if (/code_lookup/.test(n)) return 'asking the compiler';
1907
+ if (/fetch_url|web_search/.test(n)) return 'fetching';
1908
+ if (/^mcp__/.test(n)) return n.split('__')[1] || 'calling';
1909
+ return 'working';
1886
1910
  }
1887
1911
 
1888
1912
  /** Whether a tool changes a file, which decides whether the panel shows content or a diff. */
@@ -1890,37 +1914,217 @@ function toolWrites(name) {
1890
1914
  return /write|edit|patch|replace|apply|create|delete|move|rename|format/i.test(name || '');
1891
1915
  }
1892
1916
 
1917
+ /**
1918
+ * Everything a tool call says about where it is looking.
1919
+ *
1920
+ * The arguments are the only honest source for this: a detail line is a summary written for a
1921
+ * transcript row, and guessing a path out of prose is how a panel ends up confidently showing the
1922
+ * wrong file.
1923
+ */
1924
+ function targetOf(e) {
1925
+ const out = { path: null, lines: [], needle: null, command: null };
1926
+ let parsed = null;
1927
+ try { parsed = e.args ? JSON.parse(e.args) : null; } catch (err) { parsed = null; }
1928
+ if (parsed) {
1929
+ for (const key of ['path', 'file', 'file_path', 'filename']) {
1930
+ if (typeof parsed[key] === 'string' && parsed[key].trim()) { out.path = parsed[key].trim(); break; }
1931
+ }
1932
+ if (!out.path && Array.isArray(parsed.paths) && typeof parsed.paths[0] === 'string') {
1933
+ out.path = parsed.paths[0];
1934
+ }
1935
+ // read_files says which lines. This is the whole point: showing the region, not the file.
1936
+ if (typeof parsed.offset === 'number') {
1937
+ const from = Math.max(1, parsed.offset);
1938
+ const count = typeof parsed.limit === 'number' && parsed.limit > 0 ? parsed.limit : 40;
1939
+ for (let i = from; i < from + Math.min(count, 400); i++) out.lines.push(i);
1940
+ }
1941
+ if (typeof parsed.pattern === 'string') out.needle = parsed.pattern;
1942
+ if (typeof parsed.search === 'string') out.needle = parsed.search;
1943
+ if (typeof parsed.command === 'string') out.command = parsed.command;
1944
+ }
1945
+ if (!out.path && !out.command && e.detail && /[./]/.test(e.detail) && !/\s/.test(e.detail.trim())) {
1946
+ out.path = e.detail.trim();
1947
+ }
1948
+ return out;
1949
+ }
1950
+
1893
1951
  function setFollow(on) {
1894
1952
  follow.on = on;
1895
1953
  try { localStorage.setItem('koneck.follow', on ? 'on' : 'off'); } catch (e) {}
1896
1954
  $('pfollow').innerHTML = 'follow <b>' + (on ? 'on' : 'off') + '</b>';
1897
- $('watch').hidden = !on || !follow.path;
1898
- if (on && follow.path) void showWatched(follow.path, null);
1955
+ $('watch').hidden = !on || (!follow.path && follow.kind !== 'term');
1956
+ if (!on) stopWatchClock();
1957
+ else if (follow.path) void showWatched(follow.path, follow.kind === 'diff');
1899
1958
  }
1900
1959
 
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;
1960
+ function stopWatchClock() {
1961
+ if (follow.timer) { clearInterval(follow.timer); follow.timer = null; }
1962
+ $('wms').textContent = '';
1963
+ }
1964
+
1965
+ /** The header: the verb, the target, and how long this step has been going. */
1966
+ function watchHead(verb, target) {
1967
+ $('wtool').textContent = verb;
1968
+ $('wpath').textContent = target;
1969
+ follow.startedAt = Date.now();
1970
+ if (!follow.timer) {
1971
+ follow.timer = setInterval(() => {
1972
+ $('wms').textContent = ms(Date.now() - follow.startedAt);
1973
+ }, 200);
1974
+ }
1975
+ }
1976
+
1977
+ /** Adds to the trail, or marks the existing entry live. Bounded, newest last. */
1978
+ function trailPush(entry) {
1979
+ const key = entry.label;
1980
+ const existing = follow.trail.find((t) => t.label === key);
1981
+ if (existing) { Object.assign(existing, entry); } else { follow.trail.push(entry); }
1982
+ while (follow.trail.length > 14) follow.trail.shift();
1983
+ drawTrail(key);
1984
+ }
1985
+
1986
+ function drawTrail(activeLabel) {
1987
+ const host = $('wtrail');
1988
+ host.innerHTML = '';
1989
+ host.hidden = follow.trail.length === 0;
1990
+ for (const t of follow.trail) {
1991
+ const chip = el('div', 'wchip' + (t.label === activeLabel ? ' on' : '') + (t.ok === false ? ' bad' : ''));
1992
+ chip.appendChild(el('span', 'dot'));
1993
+ chip.appendChild(el('span', null, t.label));
1994
+ chip.title = t.verb + ' ' + (t.path || t.label);
1995
+ // A trail you can walk back along. Revisiting stops the panel following, because otherwise
1996
+ // the next tool call would yank it away from whatever you went back to look at.
1997
+ chip.onclick = () => {
1998
+ if (t.kind === 'term') { drawTerm(t.verb, t.command || t.label, t.lines || []); drawTrail(t.label); return; }
1999
+ follow.path = t.path;
2000
+ follow.attention = t.lines || [];
2001
+ follow.needle = t.needle || null;
2002
+ watchHead(t.verb, t.path || t.label);
2003
+ stopWatchClock();
2004
+ void showWatched(t.path, t.kind === 'diff');
2005
+ drawTrail(t.label);
2006
+ };
2007
+ host.appendChild(chip);
2008
+ }
2009
+ // Keep the live end in view without dragging the reader back if they scrolled away.
2010
+ if (activeLabel) host.scrollLeft = host.scrollWidth;
2011
+ }
2012
+
2013
+ /** Points the panel at whatever a tool call is about. Cheap to call on every event. */
2014
+ function watchTool(e, done) {
2015
+ if (!follow.on) return;
2016
+ const t = targetOf(e);
2017
+ const verb = verbFor(e.name);
2018
+
2019
+ if (t.command) {
2020
+ // A command is not a file, and its output IS the thing to watch.
2021
+ follow.kind = 'term';
2022
+ follow.path = null;
2023
+ if (!done) { follow.output = []; follow.verb = verb; }
2024
+ $('watch').hidden = false;
2025
+ watchHead(verb, t.command);
2026
+ drawTerm(verb, t.command, follow.output);
2027
+ trailPush({ label: t.command.split(/\s+/)[0] || 'command', verb, kind: 'term',
2028
+ command: t.command, lines: follow.output, ok: done ? e.ok !== false : undefined });
2029
+ return;
2030
+ }
2031
+ if (!t.path) return;
2032
+
2033
+ follow.path = t.path;
2034
+ follow.attention = t.lines;
2035
+ follow.needle = t.needle;
2036
+ follow.kind = done && toolWrites(e.name) ? 'diff' : 'code';
2037
+ follow.verb = verb;
1905
2038
  $('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);
2039
+ watchHead(verb, t.path);
2040
+ trailPush({ label: t.path.split('/').pop() || t.path, verb, kind: follow.kind, path: t.path,
2041
+ lines: t.lines, needle: t.needle, ok: done ? e.ok !== false : undefined });
2042
+
2043
+ if (follow.busy) { follow.pending = [t.path, follow.kind === 'diff']; return; }
2044
+ void showWatched(t.path, follow.kind === 'diff');
2045
+ }
2046
+
2047
+ /** A line of a running command, as it arrives. */
2048
+ function watchOutput(line) {
2049
+ if (!follow.on || follow.kind !== 'term') return;
2050
+ follow.output.push(line);
2051
+ while (follow.output.length > WATCH_OUTPUT_LINES) follow.output.shift();
2052
+ const host = $('wbody');
2053
+ const term = host.querySelector('.wterm');
2054
+ if (!term) return;
2055
+ const wasAtEnd = host.scrollHeight - host.scrollTop - host.clientHeight < 40;
2056
+ term.appendChild(el('span', 'ln', line));
2057
+ // Follows the tail, unless the reader has scrolled up to look at something.
2058
+ if (wasAtEnd) host.scrollTop = host.scrollHeight;
2059
+ }
2060
+
2061
+ function drawTerm(verb, command, lines) {
2062
+ const host = $('wbody');
2063
+ host.innerHTML = '';
2064
+ const term = el('div', 'wterm');
2065
+ term.appendChild(el('span', 'cmd', '$ ' + command));
2066
+ for (const line of lines) term.appendChild(el('span', 'ln', line));
2067
+ if (lines.length === 0) term.appendChild(el('span', 'stub', 'waiting for output…'));
2068
+ host.appendChild(term);
1910
2069
  }
1911
2070
 
1912
2071
  /**
1913
- * Draws the panel.
2072
+ * Draws a file, with the region under attention marked.
1914
2073
  *
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.
2074
+ * The gutter and the source are separate columns, so a highlight has to be applied to both or the
2075
+ * numbers and the tint drift apart the moment a line wraps which is why neither column wraps.
1918
2076
  */
1919
- async function showWatched(path, wrote) {
2077
+ function drawCode(text, attention, needle) {
2078
+ const lines = text.split('\n');
2079
+ const hot = new Set(attention || []);
2080
+ const code = el('div', 'rows');
2081
+
2082
+ /*
2083
+ * One element per line in each column, and no newline characters inside them.
2084
+ *
2085
+ * The first version was a text node per line carrying its own newline, highlighted with an
2086
+ * inline-block. It measured correct — seventy rows in each column, the same four marked — and
2087
+ * rendered wrong: four consecutive highlighted lines came out as one, and the gutter jumped from
2088
+ * 44 to 49. An inline-block whose content ends in a newline, inside a pre-formatted parent, is
2089
+ * a layout argument nobody wins. Block rows cannot drift: row N of one column is row N of the
2090
+ * other, because each is exactly one line box tall by construction.
2091
+ */
2092
+ const gut = el('div', 'gut');
2093
+ const src = el('div', 'src');
2094
+ for (let i = 0; i < lines.length; i++) {
2095
+ const n = i + 1;
2096
+ const marked = hot.has(n);
2097
+ gut.appendChild(el('div', marked ? 'on' : null, String(n)));
2098
+
2099
+ const one = lines[i];
2100
+ const row = el('div', marked ? 'on' : null);
2101
+ // The exact text a pattern matched, inside the line — the finest grain the tool gives us.
2102
+ let did = false;
2103
+ if (needle && one) {
2104
+ const at = one.indexOf(needle);
2105
+ if (at !== -1) {
2106
+ row.appendChild(document.createTextNode(one.slice(0, at)));
2107
+ row.appendChild(el('span', 'hit', needle));
2108
+ row.appendChild(document.createTextNode(one.slice(at + needle.length)));
2109
+ did = true;
2110
+ }
2111
+ }
2112
+ // An empty line still needs to occupy one, or every column below it is off by one.
2113
+ if (!did) row.textContent = one === '' ? '\u200b' : one;
2114
+ src.appendChild(row);
2115
+ }
2116
+ code.appendChild(gut);
2117
+ code.appendChild(src);
2118
+ return code;
2119
+ }
2120
+
2121
+ /** Draws the panel for a path: the file itself, or its diff once it has been written. */
2122
+ async function showWatched(path, asDiff) {
2123
+ if (!path) return;
1920
2124
  follow.busy = true;
1921
2125
  const host = $('wbody');
1922
2126
  try {
1923
- if (wrote && state.session) {
2127
+ if (asDiff && state.session) {
1924
2128
  let ch = null;
1925
2129
  try {
1926
2130
  const out = await api('/api/changes?session=' + encodeURIComponent(state.session));
@@ -1929,7 +2133,7 @@ async function showWatched(path, wrote) {
1929
2133
  if (ch && ch.diff) {
1930
2134
  host.innerHTML = '';
1931
2135
  const head = el('div', 'wempty');
1932
- head.style.padding = '8px 12px';
2136
+ head.style.padding = '8px 13px';
1933
2137
  head.textContent = '+' + ch.added + ' \u2212' + ch.removed + ' ' + ch.kind;
1934
2138
  host.appendChild(head);
1935
2139
  host.appendChild(renderDiff(ch.diff));
@@ -1948,15 +2152,12 @@ async function showWatched(path, wrote) {
1948
2152
  host.appendChild(el('div', 'wempty', 'Binary file — nothing to show as text.'));
1949
2153
  return;
1950
2154
  }
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);
2155
+ host.appendChild(drawCode(data.text, follow.attention, follow.needle));
2156
+ // Put the region being read in the middle of the panel, which is the point of knowing it.
2157
+ const first = host.querySelector('.src .on');
2158
+ if (first) first.scrollIntoView({ block: 'center' });
1956
2159
  } finally {
1957
2160
  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
2161
  if (follow.pending) {
1961
2162
  const next = follow.pending;
1962
2163
  follow.pending = null;
@@ -1967,6 +2168,12 @@ async function showWatched(path, wrote) {
1967
2168
 
1968
2169
  $('pfollow').onclick = () => setFollow(!follow.on);
1969
2170
  $('wclose').onclick = () => setFollow(false);
2171
+ // Escape closes it too, unless something modal is open — a dialog owns Escape while it is up.
2172
+ addEventListener('keydown', (ev) => {
2173
+ if (ev.key !== 'Escape' || $('watch').hidden) return;
2174
+ if (document.querySelector('dialog[open]') || !$('palette').hidden) return;
2175
+ setFollow(false);
2176
+ });
1970
2177
 
1971
2178
  /* ── choosing a model ────────────────────────────────────────────────────────────────────── */
1972
2179
 
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAioElB,CAAC;AACF,CAAC"}
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAg1ElB,CAAC;AACF,CAAC"}
@@ -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,CA8iB3C"}
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,CA2nB3C"}
@@ -20,6 +20,8 @@ export function css(markUrl) {
20
20
  --cyan:#0d7f92; --cyan-ink:#ffffff; --amber:#8a5a00; --green:#0f7a4a; --red:#b4232a;
21
21
  --violet:#6d4bc4;
22
22
  --add-bg:#e4f6ea; --add-ink:#0d5c37; --del-bg:#fdeaea; --del-ink:#8f1c22;
23
+ /* The region a tool is reading, and the exact text it matched inside that region. */
24
+ --attn-bg:#e8f4fb; --attn-hit:#bde4f7;
23
25
  --shadow:0 1px 2px rgba(15,28,34,.07), 0 8px 24px rgba(15,28,34,.05);
24
26
  --radius:10px; --mono:ui-monospace,'SF Mono',Menlo,Consolas,'DejaVu Sans Mono',monospace;
25
27
  --mark:url("${markUrl}");
@@ -31,6 +33,8 @@ export function css(markUrl) {
31
33
  --cyan:#4dd6e8; --cyan-ink:#04181c; --amber:#e8b04d; --green:#57d99a; --red:#ef767b;
32
34
  --violet:#b39cfb;
33
35
  --add-bg:#0f2a1e; --add-ink:#7ee0a7; --del-bg:#2b1416; --del-ink:#f4a3a6;
36
+ --attn-bg:#10222c; --attn-hit:#1c3f52;
37
+ --attn-bg:#10222c; --attn-hit:#1c3f52;
34
38
  --shadow:0 1px 2px rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.3);
35
39
  }
36
40
  }
@@ -114,6 +118,20 @@ header{height:47px;flex:0 0 47px;border-bottom:1px solid var(--line);display:fle
114
118
  .stopbtn{border:1px solid var(--red);color:var(--red);border-radius:999px;padding:4px 11px}
115
119
 
116
120
  .view{flex:1;overflow-y:auto;min-height:0}
121
+ /*
122
+ * The hidden attribute wins, always.
123
+ *
124
+ * A browser hides [hidden] with display:none at the same specificity as a single class, and this
125
+ * stylesheet comes later, so any rule setting display on a class silently beats it and the
126
+ * element can never be hidden. Reported as a panel that would not close: the button set the
127
+ * attribute, nothing happened, and because the state behind it HAD changed the visible panel
128
+ * then stopped updating. One cause, two symptoms that looked unrelated.
129
+ *
130
+ * Every element added since has needed its own guard — .view, .live, .attached, footer, #palette —
131
+ * and .watch was the one that did not get one. So it is stated once, for anything hidden ever.
132
+ */
133
+ [hidden]{display:none!important}
134
+
117
135
  .view[hidden]{display:none}
118
136
  .wrap{max-width:840px;margin:0 auto;padding:24px}
119
137
  .hero{text-align:center;padding:12vh 0 0;color:var(--muted)}
@@ -525,8 +543,18 @@ dialog:has(.dlg.wide){max-width:560px}
525
543
  * find out about afterwards. This follows the tool calls: the path a tool names becomes the panel,
526
544
  * and once the file has actually been written it becomes that file's diff.
527
545
  */
546
+ /*
547
+ * Two scrollers, not one.
548
+ *
549
+ * .view is the scroller for every other tab, and the panel started out inside it — so scrolling
550
+ * the conversation scrolled the file with it, and the panel's own overflow never came into play
551
+ * because nothing bounded its height. The chat tab therefore does not scroll itself: the split
552
+ * fills it, and each half scrolls independently.
553
+ */
554
+ #chatview{overflow:hidden;display:flex}
528
555
  .chatsplit{flex:1;display:flex;min-height:0;min-width:0}
529
- .chatsplit .wrap{flex:1 1 auto;min-width:0}
556
+ .chatsplit .conv{flex:1 1 auto;min-width:0;overflow-y:auto;overflow-x:hidden}
557
+ .chatsplit .conv .wrap{min-width:0}
530
558
  .watch{flex:0 0 44%;max-width:720px;min-width:0;border-left:1px solid var(--line);
531
559
  display:flex;flex-direction:column;background:var(--panel-2)}
532
560
  .whead{display:flex;gap:9px;align-items:center;padding:9px 12px;background:var(--panel);
@@ -535,13 +563,62 @@ dialog:has(.dlg.wide){max-width:560px}
535
563
  flex:0 0 auto}
536
564
  .whead .wp{font-family:var(--mono);font-size:12px;flex:1;min-width:0;overflow:hidden;
537
565
  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}
566
+ .whead .wms{flex:0 0 auto;font-size:10.5px;color:var(--dim);font-variant-numeric:tabular-nums;
567
+ font-family:var(--mono)}
568
+ /* A real hit area: a 15px glyph is a thing you aim at, not a thing you click. */
569
+ .whead .wx{flex:0 0 auto;background:none;border:0;color:var(--dim);font-size:17px;cursor:pointer;
570
+ line-height:1;padding:2px 8px;margin:-4px -6px -4px 0;border-radius:6px}
571
+ .whead .wx:hover{color:var(--red);background:var(--panel-2)}
572
+ .wbody{flex:1;overflow:auto;min-height:0;position:relative}
542
573
  .wbody .code{min-height:100%}
543
574
  .wbody .diff{border-top:0}
544
575
  .wempty{padding:22px 14px;color:var(--dim);font-size:12.5px;line-height:1.6}
576
+
577
+ /*
578
+ * Where it is looking, not just what at.
579
+ *
580
+ * A tool call says which lines it is reading and which pattern it is matching, so the panel can
581
+ * mark them: the region under attention is tinted and carries a marker in the gutter. Watching a
582
+ * file scroll past is not the same as seeing which four lines were actually read.
583
+ */
584
+ /*
585
+ * A row per line, in both columns.
586
+ *
587
+ * Not inline-blocks carrying their own newline: that measured correct and rendered wrong, four
588
+ * highlighted lines collapsing into one and the gutter jumping 44 → 49. A block row is exactly one
589
+ * line box by construction, so row N of the gutter is row N of the source and no highlight can
590
+ * make them drift.
591
+ */
592
+ .rows{display:flex;font-family:var(--mono);font-size:12.5px;line-height:1.5;align-items:stretch}
593
+ .rows .gut{flex:0 0 auto;padding:10px 10px 10px 14px;text-align:right;color:var(--dim);
594
+ background:var(--panel);border-right:1px solid var(--line);user-select:none;
595
+ font-variant-numeric:tabular-nums}
596
+ .rows .src{flex:1 1 auto;padding:10px 0 10px 14px;min-width:0;overflow-x:auto;tab-size:4}
597
+ .rows .gut > div,.rows .src > div{white-space:pre;height:1.5em}
598
+ .rows .gut > div.on{color:var(--cyan);background:var(--attn-bg)}
599
+ .rows .src > div.on{background:var(--attn-bg);box-shadow:inset 2px 0 0 var(--cyan)}
600
+ .rows .src .hit{background:var(--attn-hit);border-radius:2px}
601
+
602
+ /* A command, as it runs. */
603
+ .wterm{font-family:var(--mono);font-size:11.5px;line-height:1.5;padding:10px 13px;
604
+ white-space:pre-wrap;word-break:break-word;color:var(--muted)}
605
+ .wterm .cmd{color:var(--cyan);display:block;margin-bottom:7px}
606
+ .wterm .ln{display:block}
607
+ .wterm .stub{color:var(--dim);font-style:italic}
608
+
609
+ /* The trail: everything this turn has touched, still reachable. */
610
+ .wtrail{flex:0 0 auto;display:flex;gap:6px;padding:8px 10px;overflow-x:auto;
611
+ border-top:1px solid var(--line);background:var(--panel)}
612
+ .wtrail::-webkit-scrollbar{height:0}
613
+ .wchip{flex:0 0 auto;display:flex;gap:5px;align-items:center;padding:3px 9px;border-radius:999px;
614
+ border:1px solid var(--line);background:var(--panel-2);font-size:11px;cursor:pointer;
615
+ white-space:nowrap;color:var(--muted);font-family:var(--mono)}
616
+ .wchip:hover{border-color:var(--cyan);color:var(--ink)}
617
+ .wchip.on{border-color:var(--cyan);color:var(--cyan)}
618
+ .wchip .dot{width:5px;height:5px;border-radius:50%;background:var(--dim);flex:0 0 auto}
619
+ .wchip.on .dot{background:var(--cyan)}
620
+ .wchip.bad .dot{background:var(--red)}
621
+
545
622
  @media (max-width:1100px){ .watch{display:none} }
546
623
 
547
624
  /*
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkiBtB,CAAC;AACF,CAAC"}
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;;;;;;;;;;;;gBAYO,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6mBtB,CAAC;AACF,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"ui.d.ts","sourceRoot":"","sources":["../../src/web/ui.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAOH,wBAAgB,IAAI,IAAI,MAAM,CA2O7B"}
1
+ {"version":3,"file":"ui.d.ts","sourceRoot":"","sources":["../../src/web/ui.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAOH,wBAAgB,IAAI,IAAI,MAAM,CAmP7B"}
package/dist/web/ui.js CHANGED
@@ -65,6 +65,8 @@ export function page() {
65
65
 
66
66
  <div class="view" id="chatview">
67
67
  <div class="chatsplit">
68
+ <!-- The conversation is its own scroller so the panel beside it can be another. -->
69
+ <div class="conv" id="conv">
68
70
  <div class="wrap" id="stream">
69
71
  <div class="hero">
70
72
  <div class="mark"></div>
@@ -77,15 +79,21 @@ export function page() {
77
79
  </div>
78
80
  </div>
79
81
  </div>
82
+ </div>
80
83
  <!-- The file being worked on, beside the conversation rather than behind a tab. A tool call
81
84
  names a path; this shows it, and shows the diff once the file has actually been written. -->
82
85
  <aside class="watch" id="watch" hidden>
83
86
  <div class="whead">
84
87
  <span class="wt" id="wtool">watching</span>
85
88
  <span class="wp" id="wpath">&mdash;</span>
86
- <button class="wx" id="wclose" title="Stop following (also the follow pill)">&times;</button>
89
+ <span class="wms" id="wms"></span>
90
+ <button type="button" class="wx" id="wclose" aria-label="Stop following"
91
+ title="Stop following — or press Escape, or the follow pill">&times;</button>
87
92
  </div>
88
93
  <div class="wbody" id="wbody"></div>
94
+ <!-- Everything this turn has touched, in order. The current one is live; the rest are
95
+ one click away, which is what makes it a record rather than a glimpse. -->
96
+ <div class="wtrail" id="wtrail" hidden></div>
89
97
  </aside>
90
98
  </div>
91
99
  </div>
@@ -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,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"}
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAuOF,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC;EAC7C,YAAY,EAAE;;eAED,CAAC;AAChB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koneck",
3
- "version": "2.34.0",
3
+ "version": "2.35.1",
4
4
  "description": "Kinetically Orchestrated Neural Execution Engine for Code",
5
5
  "type": "module",
6
6
  "bin": {