koneck 2.55.0 → 2.57.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/engine.d.ts.map +1 -1
- package/dist/engine.js +21 -0
- package/dist/engine.js.map +1 -1
- package/dist/partial-json.d.ts +39 -0
- package/dist/partial-json.d.ts.map +1 -0
- package/dist/partial-json.js +89 -0
- package/dist/partial-json.js.map +1 -0
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +56 -10
- package/dist/tools.js.map +1 -1
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/web/api.d.ts.map +1 -1
- package/dist/web/api.js +58 -0
- package/dist/web/api.js.map +1 -1
- package/dist/web/events.d.ts +14 -0
- package/dist/web/events.d.ts.map +1 -1
- package/dist/web/events.js.map +1 -1
- package/dist/web/jobs.d.ts.map +1 -1
- package/dist/web/jobs.js +13 -2
- package/dist/web/jobs.js.map +1 -1
- package/dist/web/sessions.d.ts.map +1 -1
- package/dist/web/sessions.js +11 -0
- package/dist/web/sessions.js.map +1 -1
- package/dist/web/ui-client.d.ts.map +1 -1
- package/dist/web/ui-client.js +351 -78
- package/dist/web/ui-client.js.map +1 -1
- package/dist/web/ui-css.d.ts +0 -13
- package/dist/web/ui-css.d.ts.map +1 -1
- package/dist/web/ui-css.js +100 -52
- package/dist/web/ui-css.js.map +1 -1
- package/dist/web/ui.d.ts.map +1 -1
- package/dist/web/ui.js +28 -14
- package/dist/web/ui.js.map +1 -1
- package/img/konect terminal.jpeg +0 -0
- package/package.json +2 -1
package/dist/web/ui-client.js
CHANGED
|
@@ -168,7 +168,14 @@ function setView(name) {
|
|
|
168
168
|
if (name === 'changes') loadChanges();
|
|
169
169
|
if (name === 'timing') loadTiming();
|
|
170
170
|
if (name === 'files') loadTree('');
|
|
171
|
-
if (name === 'terminal') {
|
|
171
|
+
if (name === 'terminal') {
|
|
172
|
+
// Built on first sight and kept, so a pane's scrollback survives leaving the tab and coming
|
|
173
|
+
// back — a terminal that forgets what it printed when you look away is not a terminal.
|
|
174
|
+
if (term.panes.length === 0 || !term.panes[0].el) buildTerminal();
|
|
175
|
+
watchTerminal();
|
|
176
|
+
const here = term.panes[term.active];
|
|
177
|
+
if (here && here.el) here.el.input.focus();
|
|
178
|
+
}
|
|
172
179
|
if (name === 'search') $('q').focus();
|
|
173
180
|
if (name === 'review') loadReview();
|
|
174
181
|
if (name === 'refactor') loadRefactor();
|
|
@@ -411,6 +418,20 @@ function onEvent(e) {
|
|
|
411
418
|
// Follow it: not just which file, but which lines and which pattern.
|
|
412
419
|
watchTool(e, false);
|
|
413
420
|
break;
|
|
421
|
+
case 'tool-args': {
|
|
422
|
+
// Only a write has anything to show mid-flight; a read has nothing to watch. The fields were
|
|
423
|
+
// pulled out of the still-arriving JSON on the server, where that can be tested.
|
|
424
|
+
if (!follow.on || !toolWrites(e.name) || !e.path || !e.content) break;
|
|
425
|
+
const path = e.path;
|
|
426
|
+
const content = e.content;
|
|
427
|
+
$('watch').hidden = false;
|
|
428
|
+
// Whatever it was showing, the thing happening now is this file being written.
|
|
429
|
+
if (follow.mode !== 'work') setPanelMode('work');
|
|
430
|
+
watchHead('writing', path);
|
|
431
|
+
follow.kind = 'write';
|
|
432
|
+
drawWriteInProgress(path, content);
|
|
433
|
+
break;
|
|
434
|
+
}
|
|
414
435
|
case 'tool-output':
|
|
415
436
|
if (state.lastTool) {
|
|
416
437
|
state.lastTool.body.textContent =
|
|
@@ -2606,8 +2627,17 @@ addEventListener('keydown', (ev) => {
|
|
|
2606
2627
|
*/
|
|
2607
2628
|
function ansiToHtml(text) {
|
|
2608
2629
|
const escChar = String.fromCharCode(27);
|
|
2630
|
+
/*
|
|
2631
|
+
* Colour is kept; everything else a terminal emits is swallowed.
|
|
2632
|
+
*
|
|
2633
|
+
* The last alternative is the one that was missing: an escape followed by a single character —
|
|
2634
|
+
* ESC = for keypad mode, ESC > , ESC 7 to save the cursor — has no bracket, so the CSI pattern
|
|
2635
|
+
* never matched it and it reached the page as a control glyph. That is what appeared at the top
|
|
2636
|
+
* of a pane running git.
|
|
2637
|
+
*/
|
|
2609
2638
|
const pattern = new RegExp(escChar + '\\[([0-9;]*)m|' + escChar + '\\[[0-9;?]*[A-Za-z]|'
|
|
2610
|
-
+ escChar + '\\][^\\u0007]
|
|
2639
|
+
+ escChar + '\\][^\\u0007]*(?:\\u0007|' + escChar + '\\\\)|'
|
|
2640
|
+
+ escChar + '[=>78MDEHcZ]|[\\u0000-\\u0008\\u000b\\u000c\\u000e-\\u001f]', 'g');
|
|
2611
2641
|
let out = '';
|
|
2612
2642
|
let open = 0;
|
|
2613
2643
|
let last = 0;
|
|
@@ -2645,96 +2675,303 @@ function ansiToHtml(text) {
|
|
|
2645
2675
|
* matters is not a setting but a fact about the command: a test run finishes and a dev server
|
|
2646
2676
|
* does not, and a runner that waits for the second one looks broken.
|
|
2647
2677
|
*/
|
|
2648
|
-
|
|
2678
|
+
/*
|
|
2679
|
+
* A terminal, not a form with an output box.
|
|
2680
|
+
*
|
|
2681
|
+
* What was here was a job list beside a pane of text: you typed into a field at the top, picked a
|
|
2682
|
+
* row, and read the result — which arrived in whole-buffer snapshots a few times a second, because
|
|
2683
|
+
* the view polled while the runner had been emitting a line-by-line event all along. Everything
|
|
2684
|
+
* about it said "web page".
|
|
2685
|
+
*
|
|
2686
|
+
* This is a scrollback: the command you typed stays above what it printed, the cursor waits at the
|
|
2687
|
+
* bottom, and the next thing you type continues the same column of text. Up to three panes, each
|
|
2688
|
+
* its own shell with its own history, because two is the common case — watch the server, run the
|
|
2689
|
+
* tests — and four on a laptop is four things too narrow to read.
|
|
2690
|
+
*/
|
|
2691
|
+
const term = {
|
|
2692
|
+
panes: [],
|
|
2693
|
+
/** Which pane the keyboard belongs to. */
|
|
2694
|
+
active: 0,
|
|
2695
|
+
stream: null,
|
|
2696
|
+
/** Pane index by job id, so a line can be routed to the pane that asked for it. */
|
|
2697
|
+
ownerOf: {},
|
|
2698
|
+
fontPx: 12.5,
|
|
2699
|
+
};
|
|
2649
2700
|
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2701
|
+
function newPane() {
|
|
2702
|
+
return { job: null, history: [], at: -1, lines: [], running: false, el: null,
|
|
2703
|
+
/** Where this command's output begins, so a catch-up can replace exactly that. */
|
|
2704
|
+
outputStart: 0,
|
|
2705
|
+
/** How many output lines have arrived on the stream, for the same reason. */
|
|
2706
|
+
received: 0 };
|
|
2707
|
+
}
|
|
2708
|
+
|
|
2709
|
+
/** The pane a job's output belongs to, or null when nothing here started it. */
|
|
2710
|
+
function paneForJob(id) {
|
|
2711
|
+
const idx = term.ownerOf[id];
|
|
2712
|
+
return idx === undefined ? null : term.panes[idx] || null;
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
function buildTerminal() {
|
|
2716
|
+
if (term.panes.length === 0) term.panes.push(newPane());
|
|
2717
|
+
const host = $('termpanes');
|
|
2654
2718
|
host.innerHTML = '';
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
if (j.state === 'running') anyRunning = true;
|
|
2662
|
-
const row = el('div', 'jrow' + (j.id === pick ? ' on' : ''));
|
|
2663
|
-
row.appendChild(el('div', 'jc', j.command));
|
|
2664
|
-
const meta = el('div', 'jm');
|
|
2665
|
-
meta.appendChild(el('span', 'js ' + j.state, j.state
|
|
2666
|
-
+ (j.exitCode !== null && j.state !== 'running' ? ' ' + j.exitCode : '')));
|
|
2667
|
-
// Who ran it is a column, not a separate screen.
|
|
2668
|
-
if (j.by === 'agent') meta.appendChild(el('span', null, 'by the agent'));
|
|
2669
|
-
if (j.background) meta.appendChild(el('span', null, 'background'));
|
|
2670
|
-
if (j.pty) meta.appendChild(el('span', null, 'tty'));
|
|
2671
|
-
meta.appendChild(el('span', null, j.lines + (j.lines === 1 ? ' line' : ' lines')));
|
|
2672
|
-
if (j.state === 'running' && j.by !== 'agent') {
|
|
2673
|
-
const stop = el('button', 'jk', 'kill');
|
|
2674
|
-
stop.onclick = async (ev) => {
|
|
2675
|
-
ev.stopPropagation();
|
|
2676
|
-
try { await api('/api/kill', { method: 'POST', body: JSON.stringify({ id: j.id }) }); }
|
|
2677
|
-
catch (e) {}
|
|
2678
|
-
loadJobs(j.id);
|
|
2679
|
-
};
|
|
2680
|
-
meta.appendChild(stop);
|
|
2719
|
+
term.panes.forEach((pane, index) => {
|
|
2720
|
+
if (index > 0) {
|
|
2721
|
+
// A draggable divider, so a pane can be made wide enough for the thing being read.
|
|
2722
|
+
const grip = el('div', 'termgrip');
|
|
2723
|
+
grip.onmousedown = (ev) => startPaneDrag(ev, index);
|
|
2724
|
+
host.appendChild(grip);
|
|
2681
2725
|
}
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2726
|
+
const box = el('div', 'termpane' + (index === term.active ? ' on' : ''));
|
|
2727
|
+
box.style.flex = String(pane.flex || 1);
|
|
2728
|
+
box.onmousedown = () => { term.active = index; markActivePane(); };
|
|
2729
|
+
|
|
2730
|
+
const scroll = el('div', 'termscroll');
|
|
2731
|
+
const body = el('pre', 'termbody');
|
|
2732
|
+
scroll.appendChild(body);
|
|
2733
|
+
|
|
2734
|
+
const row = el('div', 'termrow');
|
|
2735
|
+
row.appendChild(el('span', 'termps', '$'));
|
|
2736
|
+
const input = document.createElement('input');
|
|
2737
|
+
input.className = 'termin';
|
|
2738
|
+
input.spellcheck = false;
|
|
2739
|
+
input.autocomplete = 'off';
|
|
2740
|
+
input.placeholder = index === 0 ? 'npm test, git status, npm run dev…' : '';
|
|
2741
|
+
input.onkeydown = (ev) => paneKey(ev, index);
|
|
2742
|
+
input.onfocus = () => { term.active = index; markActivePane(); };
|
|
2743
|
+
row.appendChild(input);
|
|
2744
|
+
const stop = el('button', 'termstop', 'stop');
|
|
2745
|
+
stop.title = 'Kill what is running in this pane';
|
|
2746
|
+
stop.onclick = () => void killPane(index);
|
|
2747
|
+
row.appendChild(stop);
|
|
2748
|
+
|
|
2749
|
+
box.appendChild(scroll);
|
|
2750
|
+
box.appendChild(row);
|
|
2751
|
+
host.appendChild(box);
|
|
2752
|
+
pane.el = { box: box, scroll: scroll, body: body, input: input, stop: stop };
|
|
2753
|
+
paintPane(index);
|
|
2754
|
+
});
|
|
2755
|
+
applyTerminalLook();
|
|
2756
|
+
markActivePane();
|
|
2757
|
+
const here = term.panes[term.active];
|
|
2758
|
+
if (here && here.el) here.el.input.focus();
|
|
2697
2759
|
}
|
|
2698
2760
|
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
const
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2761
|
+
function markActivePane() {
|
|
2762
|
+
term.panes.forEach((pane, i) => {
|
|
2763
|
+
if (pane.el) pane.el.box.className = 'termpane' + (i === term.active ? ' on' : '');
|
|
2764
|
+
});
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2767
|
+
/** Everything this pane has printed, as one column of text. */
|
|
2768
|
+
function paintPane(index) {
|
|
2769
|
+
const pane = term.panes[index];
|
|
2770
|
+
if (!pane || !pane.el) return;
|
|
2771
|
+
const atEnd = pane.el.scroll.scrollHeight - pane.el.scroll.scrollTop
|
|
2772
|
+
- pane.el.scroll.clientHeight < 60;
|
|
2773
|
+
pane.el.body.innerHTML = ansiToHtml(pane.lines.join('\n'));
|
|
2774
|
+
pane.el.stop.hidden = !pane.running;
|
|
2775
|
+
if (atEnd) pane.el.scroll.scrollTop = pane.el.scroll.scrollHeight;
|
|
2776
|
+
}
|
|
2777
|
+
|
|
2778
|
+
function pushPane(index, line, redraw) {
|
|
2779
|
+
const pane = term.panes[index];
|
|
2780
|
+
if (!pane) return;
|
|
2781
|
+
if (redraw && pane.lines.length > 0) pane.lines[pane.lines.length - 1] = line;
|
|
2782
|
+
else pane.lines.push(line);
|
|
2783
|
+
while (pane.lines.length > 4000) pane.lines.shift();
|
|
2784
|
+
paintPane(index);
|
|
2785
|
+
}
|
|
2786
|
+
|
|
2787
|
+
async function paneKey(ev, index) {
|
|
2788
|
+
const pane = term.panes[index];
|
|
2789
|
+
if (!pane || !pane.el) return;
|
|
2790
|
+
const input = pane.el.input;
|
|
2791
|
+
if (ev.key === 'Enter') {
|
|
2792
|
+
const command = input.value.trim();
|
|
2793
|
+
input.value = '';
|
|
2794
|
+
if (command === '') { pushPane(index, '$ '); return; }
|
|
2795
|
+
if (command === 'clear') { pane.lines = []; paintPane(index); return; }
|
|
2796
|
+
pane.history.push(command);
|
|
2797
|
+
pane.at = pane.history.length;
|
|
2798
|
+
// Echoed above its own output, which is the whole difference from a form.
|
|
2799
|
+
pushPane(index, '$ ' + command);
|
|
2800
|
+
pane.outputStart = pane.lines.length;
|
|
2801
|
+
pane.received = 0;
|
|
2802
|
+
await runInPane(index, command);
|
|
2803
|
+
return;
|
|
2715
2804
|
}
|
|
2716
|
-
|
|
2805
|
+
// A history nobody has to think about: up and down, as everywhere else.
|
|
2806
|
+
if (ev.key === 'ArrowUp') {
|
|
2807
|
+
ev.preventDefault();
|
|
2808
|
+
if (pane.at > 0) pane.at--;
|
|
2809
|
+
input.value = pane.history[pane.at] || '';
|
|
2810
|
+
return;
|
|
2811
|
+
}
|
|
2812
|
+
if (ev.key === 'ArrowDown') {
|
|
2813
|
+
ev.preventDefault();
|
|
2814
|
+
if (pane.at < pane.history.length) pane.at++;
|
|
2815
|
+
input.value = pane.history[pane.at] || '';
|
|
2816
|
+
return;
|
|
2817
|
+
}
|
|
2818
|
+
if (ev.key === 'c' && ev.ctrlKey) { void killPane(index); }
|
|
2717
2819
|
}
|
|
2718
2820
|
|
|
2719
|
-
async function
|
|
2720
|
-
const
|
|
2721
|
-
if (!
|
|
2821
|
+
async function runInPane(index, command) {
|
|
2822
|
+
const pane = term.panes[index];
|
|
2823
|
+
if (!pane) return;
|
|
2824
|
+
// Anything that does not exit on its own is a background job; a runner that waits for a dev
|
|
2825
|
+
// server looks broken, and the fact is about the command rather than a box somebody ticked.
|
|
2826
|
+
const looksLikeServer = /\b(dev|serve|start|watch|nodemon|vite|next\s+dev)\b/.test(command);
|
|
2722
2827
|
let out;
|
|
2723
2828
|
try {
|
|
2724
|
-
out = await api('/api/run', { method: 'POST',
|
|
2725
|
-
command: command, background:
|
|
2829
|
+
out = await api('/api/run', { method: 'POST',
|
|
2830
|
+
body: JSON.stringify({ command: command, background: looksLikeServer }) });
|
|
2726
2831
|
} catch (e) {
|
|
2727
|
-
|
|
2832
|
+
pushPane(index, e.message);
|
|
2728
2833
|
return;
|
|
2729
2834
|
}
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2835
|
+
pane.job = out.id;
|
|
2836
|
+
pane.running = true;
|
|
2837
|
+
term.ownerOf[out.id] = index;
|
|
2838
|
+
paintPane(index);
|
|
2839
|
+
|
|
2840
|
+
/*
|
|
2841
|
+
* One catch-up read, because a command can outrun its own stream.
|
|
2842
|
+
*
|
|
2843
|
+
* The event stream is opened when the tab is first shown and takes a moment to establish. A
|
|
2844
|
+
* command typed inside that moment printed its first lines before anything was listening, and
|
|
2845
|
+
* they were gone: the pane showed the echoed command and nothing under it. Measured — the stream
|
|
2846
|
+
* was still in readyState 0 when the lines were emitted.
|
|
2847
|
+
*
|
|
2848
|
+
* So the runner is asked once, shortly after, what the job has actually produced, and anything
|
|
2849
|
+
* the stream missed is filled in. Counted rather than merged, so a line never appears twice.
|
|
2850
|
+
*/
|
|
2851
|
+
setTimeout(() => { void catchUpPane(index, out.id); }, 400);
|
|
2852
|
+
}
|
|
2853
|
+
|
|
2854
|
+
async function catchUpPane(index, jobId) {
|
|
2855
|
+
const pane = term.panes[index];
|
|
2856
|
+
if (!pane || pane.job !== jobId) return;
|
|
2857
|
+
let job;
|
|
2858
|
+
try { job = await api('/api/jobs?id=' + encodeURIComponent(jobId)); } catch (e) { return; }
|
|
2859
|
+
const produced = (job.lines || []).length;
|
|
2860
|
+
if (produced <= pane.received) return;
|
|
2861
|
+
pane.lines = pane.lines.slice(0, pane.outputStart).concat(job.lines || []);
|
|
2862
|
+
pane.received = produced;
|
|
2863
|
+
if (job.state && job.state !== 'running') pane.running = false;
|
|
2864
|
+
paintPane(index);
|
|
2865
|
+
}
|
|
2866
|
+
|
|
2867
|
+
async function killPane(index) {
|
|
2868
|
+
const pane = term.panes[index];
|
|
2869
|
+
if (!pane || !pane.job || !pane.running) return;
|
|
2870
|
+
try { await api('/api/kill', { method: 'POST', body: JSON.stringify({ id: pane.job }) }); }
|
|
2871
|
+
catch (e) { pushPane(index, e.message); }
|
|
2872
|
+
}
|
|
2873
|
+
|
|
2874
|
+
/*
|
|
2875
|
+
* Live, from the runner's own event stream.
|
|
2876
|
+
*
|
|
2877
|
+
* Its own stream rather than the session's: a command belongs to the workspace, not to whichever
|
|
2878
|
+
* conversation was open when it was typed.
|
|
2879
|
+
*/
|
|
2880
|
+
function watchTerminal() {
|
|
2881
|
+
if (term.stream) return;
|
|
2882
|
+
const url = '/api/terminal/events' + (state.token ? '?token=' + encodeURIComponent(state.token) : '');
|
|
2883
|
+
const source = new EventSource(url);
|
|
2884
|
+
term.stream = source;
|
|
2885
|
+
source.onmessage = (ev) => {
|
|
2886
|
+
let e;
|
|
2887
|
+
try { e = JSON.parse(ev.data); } catch (err) { return; }
|
|
2888
|
+
if (e.t === 'job-line') {
|
|
2889
|
+
const idx = term.ownerOf[e.id];
|
|
2890
|
+
if (idx === undefined) return;
|
|
2891
|
+
const pane = term.panes[idx];
|
|
2892
|
+
if (pane && !e.redraw) pane.received++;
|
|
2893
|
+
pushPane(idx, e.line, e.redraw === true);
|
|
2894
|
+
noticeServedUrl(e.line);
|
|
2895
|
+
return;
|
|
2896
|
+
}
|
|
2897
|
+
if (e.t === 'job' && e.job) {
|
|
2898
|
+
const idx = term.ownerOf[e.job.id];
|
|
2899
|
+
if (idx === undefined) return;
|
|
2900
|
+
const pane = term.panes[idx];
|
|
2901
|
+
if (!pane) return;
|
|
2902
|
+
if (e.job.state !== 'running') {
|
|
2903
|
+
pane.running = false;
|
|
2904
|
+
// Said the way a shell says it, rather than as a status badge.
|
|
2905
|
+
pushPane(idx, e.job.exitCode === 0 || e.job.exitCode === null
|
|
2906
|
+
? ''
|
|
2907
|
+
: '[' + e.job.state + ' ' + e.job.exitCode + ']');
|
|
2908
|
+
}
|
|
2909
|
+
paintPane(idx);
|
|
2910
|
+
}
|
|
2911
|
+
};
|
|
2912
|
+
source.onerror = () => { /* the browser reconnects on its own */ };
|
|
2913
|
+
}
|
|
2914
|
+
|
|
2915
|
+
/** Font size and backdrop, applied to every pane at once. */
|
|
2916
|
+
function applyTerminalLook() {
|
|
2917
|
+
const host = $('termpanes');
|
|
2918
|
+
host.style.setProperty('--term-font', term.fontPx + 'px');
|
|
2919
|
+
const on = $('tbgimg').checked;
|
|
2920
|
+
const strength = Number($('tbgop').value) / 100;
|
|
2921
|
+
host.classList.toggle('backdrop', on);
|
|
2922
|
+
host.style.setProperty('--term-bg-strength', on ? String(strength) : '0');
|
|
2923
|
+
if (on && !host.style.getPropertyValue('--term-bg-image')) {
|
|
2924
|
+
const url = '/api/terminal/background'
|
|
2925
|
+
+ (state.token ? '?token=' + encodeURIComponent(state.token) : '');
|
|
2926
|
+
host.style.setProperty('--term-bg-image', 'url("' + url + '")');
|
|
2927
|
+
}
|
|
2733
2928
|
}
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2929
|
+
|
|
2930
|
+
function startPaneDrag(ev, dividerIndex) {
|
|
2931
|
+
ev.preventDefault();
|
|
2932
|
+
const left = term.panes[dividerIndex - 1];
|
|
2933
|
+
const right = term.panes[dividerIndex];
|
|
2934
|
+
if (!left || !right || !left.el || !right.el) return;
|
|
2935
|
+
const startX = ev.clientX;
|
|
2936
|
+
const leftWas = left.el.box.getBoundingClientRect().width;
|
|
2937
|
+
const rightWas = right.el.box.getBoundingClientRect().width;
|
|
2938
|
+
const total = leftWas + rightWas;
|
|
2939
|
+
const move = (m) => {
|
|
2940
|
+
const wanted = Math.min(Math.max(leftWas + (m.clientX - startX), 120), total - 120);
|
|
2941
|
+
left.flex = wanted / total;
|
|
2942
|
+
right.flex = (total - wanted) / total;
|
|
2943
|
+
left.el.box.style.flex = String(left.flex);
|
|
2944
|
+
right.el.box.style.flex = String(right.flex);
|
|
2945
|
+
};
|
|
2946
|
+
const up = () => {
|
|
2947
|
+
window.removeEventListener('mousemove', move);
|
|
2948
|
+
window.removeEventListener('mouseup', up);
|
|
2949
|
+
};
|
|
2950
|
+
window.addEventListener('mousemove', move);
|
|
2951
|
+
window.addEventListener('mouseup', up);
|
|
2952
|
+
}
|
|
2953
|
+
|
|
2954
|
+
$('tsplit').onclick = () => {
|
|
2955
|
+
// Three, deliberately. Four on a laptop is four things too narrow to read.
|
|
2956
|
+
if (term.panes.length >= 3) return;
|
|
2957
|
+
term.panes.push(newPane());
|
|
2958
|
+
term.active = term.panes.length - 1;
|
|
2959
|
+
buildTerminal();
|
|
2960
|
+
};
|
|
2961
|
+
|
|
2962
|
+
$('tunsplit').onclick = () => {
|
|
2963
|
+
if (term.panes.length <= 1) return;
|
|
2964
|
+
const gone = term.panes.pop();
|
|
2965
|
+
if (gone && gone.job) delete term.ownerOf[gone.job];
|
|
2966
|
+
term.active = Math.min(term.active, term.panes.length - 1);
|
|
2967
|
+
buildTerminal();
|
|
2968
|
+
};
|
|
2969
|
+
|
|
2970
|
+
$('tbgimg').onchange = applyTerminalLook;
|
|
2971
|
+
$('tbgop').oninput = applyTerminalLook;
|
|
2972
|
+
$('tfontup').onclick = () => { term.fontPx = Math.min(20, term.fontPx + 1); applyTerminalLook(); };
|
|
2973
|
+
$('tfontdown').onclick = () => { term.fontPx = Math.max(9, term.fontPx - 1); applyTerminalLook(); };
|
|
2974
|
+
|
|
2738
2975
|
|
|
2739
2976
|
/* ── the page, as it really renders ───────────────────────────────────────────────────────── */
|
|
2740
2977
|
|
|
@@ -3110,6 +3347,16 @@ function watchTool(e, done) {
|
|
|
3110
3347
|
return;
|
|
3111
3348
|
}
|
|
3112
3349
|
|
|
3350
|
+
/*
|
|
3351
|
+
* Whatever the panel was showing, it follows the work.
|
|
3352
|
+
*
|
|
3353
|
+
* Page mode was entered by a browser call and never left, so a session that opened a page and
|
|
3354
|
+
* then went back to editing code sat on a stale screenshot while the interesting thing happened
|
|
3355
|
+
* out of sight. The browser is still one click away — and any browser call switches straight back
|
|
3356
|
+
* to it — but the panel follows what is being done now.
|
|
3357
|
+
*/
|
|
3358
|
+
if (follow.mode === 'page') setPanelMode('work');
|
|
3359
|
+
|
|
3113
3360
|
if (t.command) {
|
|
3114
3361
|
// A command is not a file, and its output IS the thing to watch.
|
|
3115
3362
|
follow.kind = 'term';
|
|
@@ -3220,6 +3467,32 @@ function prettyArgs(args) {
|
|
|
3220
3467
|
return String(args).slice(0, 8000);
|
|
3221
3468
|
}
|
|
3222
3469
|
|
|
3470
|
+
/*
|
|
3471
|
+
* The write in progress, repainted as its content arrives.
|
|
3472
|
+
*
|
|
3473
|
+
* Plain while it streams and coloured the moment it lands: highlighting needs a round trip per
|
|
3474
|
+
* repaint, and four of those a second to colour text that is about to change again is a cost with
|
|
3475
|
+
* no reader. The finished render replaces it with the coloured version a fraction of a second
|
|
3476
|
+
* later, which is what the eye actually follows.
|
|
3477
|
+
*/
|
|
3478
|
+
function drawWriteInProgress(path, content) {
|
|
3479
|
+
const host = $('wbody');
|
|
3480
|
+
let head = host.querySelector('.wwrite');
|
|
3481
|
+
if (!head) {
|
|
3482
|
+
host.innerHTML = '';
|
|
3483
|
+
head = el('div', 'wwrite');
|
|
3484
|
+
host.appendChild(head);
|
|
3485
|
+
host.appendChild(el('div', 'wwbody'));
|
|
3486
|
+
}
|
|
3487
|
+
const lines = content.split('\n');
|
|
3488
|
+
head.textContent = 'writing ' + path + ' — ' + lines.length
|
|
3489
|
+
+ (lines.length === 1 ? ' line so far' : ' lines so far');
|
|
3490
|
+
const body = host.querySelector('.wwbody');
|
|
3491
|
+
body.innerHTML = '';
|
|
3492
|
+
body.appendChild(drawCode(content, [], null, null));
|
|
3493
|
+
host.scrollTop = host.scrollHeight;
|
|
3494
|
+
}
|
|
3495
|
+
|
|
3223
3496
|
/** Paints the content a write is carrying, coloured for the file it is going into. */
|
|
3224
3497
|
async function drawWriting(path, content, before) {
|
|
3225
3498
|
const host = $('wbody');
|
|
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmiIlB,CAAC;AACF,CAAC"}
|
package/dist/web/ui-css.d.ts
CHANGED
|
@@ -1,15 +1,2 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The stylesheet, and the three themes.
|
|
3
|
-
*
|
|
4
|
-
* Every colour is a token defined once on `:root`, so a theme is a list of token values rather
|
|
5
|
-
* than a second stylesheet. Three states, which is what "light, dark and system" actually needs:
|
|
6
|
-
* an explicit choice writes `data-theme` on the root and wins; with nothing written, the browser's
|
|
7
|
-
* own preference decides.
|
|
8
|
-
*
|
|
9
|
-
* Contrast was asked for specifically — "font visible well in dark theme". The dark palette's body
|
|
10
|
-
* text is #e6f1f5 on #0d1418, which is about 14:1, and the muted tone is #93aab5 at about 7:1;
|
|
11
|
-
* both clear WCAG AA for body text with room to spare. The first version used #7f9aa6 for anything
|
|
12
|
-
* secondary, which measures 4.6:1 — passable on paper and genuinely hard to read on a dim screen.
|
|
13
|
-
*/
|
|
14
1
|
export declare function css(markUrl: string): string;
|
|
15
2
|
//# sourceMappingURL=ui-css.d.ts.map
|
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":"
|
|
1
|
+
{"version":3,"file":"ui-css.d.ts","sourceRoot":"","sources":["../../src/web/ui-css.ts"],"names":[],"mappings":"AA8CA,wBAAgB,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAy8B3C"}
|