Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/dev/shot_termstage.mjs

9.2 KiB, 1 run

created by r2519314175:197, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1// shot_termstage.mjs — look at the Terminal in its new zone.
2//
3// The panel moved from the dock to the stage, and the two are laid out by
4// different rules: a dock panel fills a 300px column of a tiled grid, a stage
5// panel takes a seat beside the conversation and is sized inline by the engine.
6// What that change is worth cannot be read off a passing assertion — the screen
7// is the artefact — so this opens a live session against the same two doubles
8// dev/verify_termpanel.mjs uses, writes something into it, and takes the picture
9// at the widths that matter. The narrow one is the point of the whole move: below
10// 1900px the dock's automatic grid is ONE column of four, so a fifth panel there
11// could only arrive by closing one of the four already in it.
12//
13// It also measures, because an eye is not a ruler: the terminal's box against the
14// panel it lives in, the grid the kernel was last told, and how many panels the
15// dock is actually seating.
16//
17// node dev/shot_termstage.mjs
18//
19// Needs dev/serve.mjs (DAIMOND_PORT, default 8777) and dev/mockllm.mjs
20// (DAIMOND_MOCK_PORT, default 9099).
21
22import { open, shot } from './harness.mjs';
23
24const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
25
26const s = await open({ name: 'termstage' });
27const p = s.page;
28
29/// The link double: what hand.js is asked to provide, and nothing more.
30async function installLink() {
31 await p.evaluate(() => {
32 window.__link = { sent: [], subs: {}, opens: 0 };
33 const fire = (id, msg) => (window.__link.subs[id] || []).slice().forEach((f) => f(msg));
34 window.__emit = fire;
35 window.DaimondHand = {
36 send(m) {
37 window.__link.sent.push(m);
38 if (m.t === 'open') {
39 window.__link.opens++;
40 setTimeout(() => fire(m.id, { t: 'opened', pid: 4242 }), 0);
41 }
42 if (m.t === 'signal') setTimeout(() => fire(m.id, { t: 'closed', exit: 0 }), 0);
43 return Promise.resolve();
44 },
45 subscribe(id, fn) {
46 (window.__link.subs[id] = window.__link.subs[id] || []).push(fn);
47 return () => {};
48 },
49 status() {
50 return Promise.resolve(JSON.stringify({
51 paired: true, transport: 'machine', machine: 'double', os: 'linux',
52 root: '/nowhere/ws', caps: ['fence:linux', 'landlock:abi-8'],
53 }));
54 },
55 hasHand: () => true,
56 };
57 });
58}
59
60/// The Rust answer double. The fence is a fiction and nothing here reads it.
61async function installComposer() {
62 await p.evaluate(() => {
63 window.DaimondTerm._setRequestForTest(async (json) => {
64 const ask = JSON.parse(json);
65 return JSON.stringify({
66 t: 'open', id: 'ts-' + (++window.__seq || (window.__seq = 1)),
67 argv: ['bash', '-i'], cwd: '/nowhere/ws', env: [],
68 size: { cols: ask.cols, rows: ask.rows },
69 fence: { rw: ['/nowhere/ws'], ro: [], deny: [], net: false },
70 });
71 });
72 });
73}
74
75/// Push bytes at the live session the way the hand would.
76async function write(text, seq) {
77 const id = await p.evaluate(() => window.DaimondTerm.session());
78 if (!id) return;
79 await p.evaluate(({ id, text, seq }) => {
80 const u8 = new TextEncoder().encode(text);
81 let raw = '';
82 for (const b of u8) raw += String.fromCharCode(b);
83 window.__emit(id, { t: 'output', seq, data: btoa(raw) });
84 }, { id, text, seq });
85 await sleep(400);
86}
87
88/// The panel, the terminal inside it, and what the dock is holding.
89const measure = () => p.evaluate(() => {
90 const panel = document.getElementById('panel-term');
91 const card = panel && panel.querySelector('.termp-card');
92 const host = document.getElementById('termp-host');
93 const term = document.querySelector('#termp-host .term');
94 const box = (e) => { const r = e.getBoundingClientRect(); return { w: Math.round(r.width), h: Math.round(r.height) }; };
95 return {
96 zone: panel && panel.dataset.zone,
97 parent: panel && panel.parentElement && (panel.parentElement.id || panel.parentElement.className),
98 panel: panel && box(panel),
99 card: card && box(card),
100 host: host && box(host),
101 term: term && box(term),
102 grid: term && { cols: +term.dataset.cols, rows: +term.dataset.rows },
103 stage: window.DaimondPanels.model().panels.filter((x) => x.zone === 'stage' && x.open).map((x) => x.id),
104 dockOpen: window.DaimondPanels.model().panels.filter((x) => x.zone === 'dock' && x.open).map((x) => x.id),
105 dockSeats: [...document.querySelectorAll('#dock .pcol > .panel')]
106 .filter((e) => getComputedStyle(e).display !== 'none').map((e) => e.dataset.panel),
107 dockMax: window.DaimondPanels.model().dockMax,
108 lastSize: (window.__link.sent.filter((m) => m.t === 'resize').pop() || {}).size || null,
109 };
110});
111
112await installLink();
113await installComposer();
114await p.evaluate(() => window.DaimondPanels.show('term'));
115await sleep(1200);
116await write('$ ls -la\r\ntotal 24\r\ndrwxr-xr-x 5 you you 4096 Aug 3 00:14 .\r\n'
117 + 'drwxr-xr-x 18 you you 4096 Aug 3 00:12 ..\r\n-rw-r--r-- 1 you you 221 Aug 3 00:13 notes.txt\r\n$ ', 0);
118
119for (const [w, h] of [[1500, 950], [1440, 900], [1280, 820]]) {
120 await p.setViewportSize({ width: w, height: h });
121 await p.evaluate(() => window.DaimondPanels.reflow());
122 await sleep(900);
123 for (const theme of ['dark', 'light']) {
124 await p.evaluate((t) => window.DaimondTheme.set(t), theme);
125 await sleep(500);
126 await shot(s, `termstage-${w}-${theme}`);
127 }
128 await p.evaluate(() => window.DaimondTheme.set('dark'));
129 await sleep(400);
130 console.log(`\n${w}x${h}: ` + JSON.stringify(await measure(), null, 1));
131}
132
133// And a resize that has to reach the kernel: drag the stage's own divider.
134//
135// At 1900 rather than at the laptop width, because MIN_W.stage is 380 for each
136// seat and a 1440px window leaves the two of them about twenty pixels of play
137// between them -- a correct clamp, and a useless demonstration.
138await p.setViewportSize({ width: 1900, height: 950 });
139await p.evaluate(() => window.DaimondPanels.reflow());
140await sleep(900);
141const before = await measure();
142// A real drag, with the mouse, because the handle captures the pointer and a
143// synthesised PointerEvent with no capture never reaches the engine's move.
144{
145 const box = await p.$eval('#handle-stage', (e) => {
146 const r = e.getBoundingClientRect();
147 return { x: r.left + r.width / 2, y: r.top + r.height / 2 };
148 });
149 await p.mouse.move(box.x, box.y);
150 await p.mouse.down();
151 await p.mouse.move(box.x - 90, box.y, { steps: 8 });
152 await p.mouse.move(box.x - 180, box.y, { steps: 8 });
153 await p.mouse.up();
154}
155await sleep(1200);
156const after = await measure();
157console.log('\nafter dragging the stage divider 180px left:');
158console.log(' grid ' + JSON.stringify(before.grid) + ' -> ' + JSON.stringify(after.grid));
159console.log(' told ' + JSON.stringify(before.lastSize) + ' -> ' + JSON.stringify(after.lastSize));
160console.log(' term ' + JSON.stringify(before.term) + ' -> ' + JSON.stringify(after.term));
161await shot(s, 'termstage-1900-dragged');
162
163// ── The phone ───────────────────────────────────────────────────────
164//
165// A stage panel on a phone is not a destination on the bottom bar: it RISES as a
166// sheet over the conversation, so the daimon stays under the thing being worked
167// in. The Terminal already did that as a dock panel with no seat; what changes
168// with the zone is only that it is now doing it for the reason the others do.
169//
170// The sheet is also where the fit has to catch up: the panel is measured while
171// it is still hidden behind the phone stylesheet, so the first grid is the 20x4
172// minimum, and the terminal's own ResizeObserver is what turns it into the
173// sheet's real size a moment later. That is the transition worth watching.
174await p.setViewportSize({ width: 390, height: 844 });
175await p.evaluate(() => window.DaimondPanels.hide('term'));
176await sleep(600);
177await p.evaluate(() => window.DaimondPanels.show('term'));
178await sleep(1800);
179const phone = await p.evaluate(() => {
180 const panel = document.getElementById('panel-term');
181 const term = document.querySelector('#termp-host .term');
182 const sheet = document.getElementById('msheet');
183 const r = (e) => { const b = e.getBoundingClientRect(); return { w: Math.round(b.width), h: Math.round(b.height) }; };
184 return {
185 inSheet: !!(panel && panel.closest('#msheet')),
186 sheetOpen: !!(sheet && sheet.classList.contains('open')),
187 sheetTitle: (document.querySelector('.msheet-title') || {}).textContent,
188 floor: document.body.dataset.mpanel,
189 barSeats: [...document.querySelectorAll('#mnav button')].map((b) => b.dataset.mp),
190 panel: panel && r(panel),
191 term: term && r(term),
192 grid: term && { cols: +term.dataset.cols, rows: +term.dataset.rows },
193 told: (window.__link.sent.filter((m) => m.t === 'resize').pop() || {}).size || null,
194 // The panel's own name must not be said twice, once by the sheet and once
195 // by the head inside it.
196 nameShown: !!(panel && panel.querySelector('.chead .ctitle')
197 && getComputedStyle(panel.querySelector('.chead .ctitle')).display !== 'none'),
198 startVisible: !!(panel && panel.querySelector('[data-act="term-start"]')
199 && getComputedStyle(panel.querySelector('[data-act="term-start"]')).display !== 'none'),
200 };
201});
202console.log('\nphone 390x844: ' + JSON.stringify(phone, null, 1));
203await shot(s, 'termstage-phone');
204
205const noise = s.errs.filter((e) => !/favicon|ERR_ABORTED|502|Bad Gateway|net::ERR/i.test(e));
206console.log('\nconsole: ' + (noise.length ? noise.slice(0, 4).join(' | ') : 'clean'));
207await s.close();