Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/dev/shot_hates.mjs

6.0 KiB, 1 run

created by r2519314175:159, 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_hates.mjs — photograph the two surfaces dev/HATES.md is about.
2//
3// The owner photographed his own screen on 2026-08-24 and the picture is the
4// brief: the chat footer split into `Workspace · Machine` and `In front of the
5// model`, a `+ Mark a folder in` button where a `+` used to be, and the
6// Workspace panel's parent-folder arrow up in the header away from the listing.
7//
8// TWO THINGS THIS CANNOT REPRODUCE, and both are honest rather than gaps:
9//
10// The heading reads `Workspace · Browser`, not `· Machine`. A page holds a
11// real folder only through `showDirectoryPicker()`, a native dialog no
12// automated browser can answer, so every harness run has an OPFS workspace.
13// The word after the dot is the ONLY difference; the layout under test is the
14// same element with the same class either way (`workspaceTitle`).
15//
16// The paths are this file's own fixtures rather than his. The state that
17// matters is the SHAPE — two folders marked in with `Note` + `Workspace`, one
18// file merely attached with `Read` — and that is built below.
19//
20// node dev/shot_hates.mjs --tag before
21//
22// --tag NAME what to call the pair, default `now`
23// --world N dev server port 8777 + n, matching DAIMOND_PORT
24import fs from 'node:fs';
25import path from 'node:path';
26import { fileURLToPath } from 'node:url';
27import { open, newChat } from './harness.mjs';
28
29const SHOTS = path.join(path.dirname(fileURLToPath(import.meta.url)), 'shots');
30
31const argv = process.argv.slice(2);
32const flag = (n, d) => {
33 const i = argv.indexOf('--' + n);
34 return i >= 0 && argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[i + 1] : d;
35};
36const TAG = flag('tag', 'now');
37
38const s = await open({ name: 'hates' });
39const { page } = s;
40const sleep = (ms) => page.waitForTimeout(ms);
41
42// The tree the panel will list, and the files the footer will hold. Written
43// through the real tool, so they exist the way the user's own do.
44await page.evaluate(async () => {
45 const m = await import('/pkg/oxedyne_daimond.js');
46 const app = new m.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 4096, '', true);
47 const w = (p, c) => app.run_tool('file_write', JSON.stringify({ path: p, content: c }));
48 await w('books/ontheism/ch1.md', '# One\n');
49 await w('books/elearnity/writing_spec.md', '# Spec\nthe sentence in the file\n');
50 await w('code/ai/context/dump/skills/note.md', '# Skills\n');
51 await w('code/rust/fe2o3/README.md', '# fe2o3\n');
52});
53
54await newChat(s);
55await sleep(700);
56const focus = await page.evaluate(() => window.DaimondAttach.focus());
57const chatId = focus && focus.id;
58if (!chatId) throw new Error('no chat in focus');
59
60// The state his screenshot shows: two folders marked in, one file merely read,
61// one folder attached without a mark.
62await page.evaluate((id) => {
63 const A = window.DaimondAttach;
64 A.chatToggle(id, 'dir:[browser]books/ontheism', true, 'books/ontheism');
65 A.chatToggle(id, 'dir:[browser]books/elearnity', true, 'books/elearnity');
66 A.chatToggle(id, 'file:[browser]books/elearnity/writing_spec.md', false,
67 'books/elearnity/writing_spec.md');
68 A.chatToggle(id, 'dir:[browser]code/ai/context/dump/skills', true,
69 'code/ai/context/dump/skills');
70}, chatId);
71await sleep(400);
72await page.evaluate((id) => {
73 const A = window.DaimondAttach;
74 A.chatWs(id, 'dir:[browser]books/ontheism', true);
75 A.chatWs(id, 'dir:[browser]books/elearnity', true);
76 A.chatState(id, 'file:[browser]books/elearnity/writing_spec.md', 'read');
77}, chatId);
78await sleep(900);
79
80fs.mkdirSync(SHOTS, { recursive: true });
81const shotOf = async (sel, label) => {
82 const el = await page.$(sel);
83 if (!el) { console.log(` MISSING ${sel}`); return ''; }
84 const p = path.join(SHOTS, `hates-${TAG}-${label}.png`);
85 await el.screenshot({ path: p, timeout: 8000 });
86 console.log(` wrote ${p}`);
87 return p;
88};
89
90await shotOf('#chat-attachments', 'footer');
91
92// And the Workspace panel, listed one folder deep so the parent control has
93// something to do.
94await page.evaluate(() => window.DaimondPanels && DaimondPanels.show('work'));
95await sleep(700);
96await page.click('#panel-work [data-act="refresh"]', { force: true }).catch(() => {});
97await sleep(800);
98await page.evaluate(() => {
99 const row = [...document.querySelectorAll('#panel-work .files-row')]
100 .find((r) => r.dataset.path === 'books');
101 if (row) row.click();
102});
103await sleep(900);
104await shotOf('#panel-work', 'panel');
105
106// What is actually on screen, in words, so a reader who cannot open a PNG can
107// still tell the two states apart.
108const said = await page.evaluate(() => {
109 const heads = [...document.querySelectorAll('#chat-attachments .attach-group-title')]
110 .map((e) => e.textContent);
111 const add = document.querySelector('#chat-attachments [data-act="attach-add"]');
112 const rows = [...document.querySelectorAll('#chat-attachments .arte-row')].map((r) => ({
113 path: (r.querySelector('.arte-open') || {}).textContent || '',
114 chips: [...r.querySelectorAll('.attach-state, .attach-ws')].map((c) => c.textContent),
115 }));
116 // Two different questions about the same action: is it a button in the panel's
117 // HEADER, and is it a row at the top of the LISTING? The point of the change is
118 // that the first is false and the second is true, and one selector cannot say so.
119 const up = document.querySelector('#panel-work .railhead [data-act="up"]');
120 const first = [...document.querySelectorAll('#panel-work .files-tree .files-row')]
121 .slice(0, 3).map((r) => (r.querySelector('.files-name') || {}).textContent || '');
122 return {
123 headings: heads,
124 add: add ? { text: (add.textContent || '').trim(), title: add.title } : null,
125 rows,
126 parentButtonInHeader: !!up,
127 parentRowInListing: (() => {
128 const r = document.querySelector('#panel-work .files-tree .files-row');
129 if (!r) return null;
130 return { text: r.textContent.trim(), act: r.dataset.act || (r.querySelector('[data-act]') || {}).dataset ? 'yes' : '' };
131 })(),
132 firstRows: first,
133 };
134});
135console.log(JSON.stringify(said, null, 1));
136fs.writeFileSync(path.join(SHOTS, `hates-${TAG}.json`), JSON.stringify(said, null, 1) + '\n');
137await s.browser.close();