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 |
| 24 | import fs from 'node:fs'; |
| 25 | import path from 'node:path'; |
| 26 | import { fileURLToPath } from 'node:url'; |
| 27 | import { open, newChat } from './harness.mjs'; |
| 28 | |
| 29 | const SHOTS = path.join(path.dirname(fileURLToPath(import.meta.url)), 'shots'); |
| 30 | |
| 31 | const argv = process.argv.slice(2); |
| 32 | const 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 | }; |
| 36 | const TAG = flag('tag', 'now'); |
| 37 | |
| 38 | const s = await open({ name: 'hates' }); |
| 39 | const { page } = s; |
| 40 | const 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. |
| 44 | await 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 | |
| 54 | await newChat(s); |
| 55 | await sleep(700); |
| 56 | const focus = await page.evaluate(() => window.DaimondAttach.focus()); |
| 57 | const chatId = focus && focus.id; |
| 58 | if (!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. |
| 62 | await 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); |
| 71 | await sleep(400); |
| 72 | await 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); |
| 78 | await sleep(900); |
| 79 | |
| 80 | fs.mkdirSync(SHOTS, { recursive: true }); |
| 81 | const 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 | |
| 90 | await shotOf('#chat-attachments', 'footer'); |
| 91 | |
| 92 | // And the Workspace panel, listed one folder deep so the parent control has |
| 93 | // something to do. |
| 94 | await page.evaluate(() => window.DaimondPanels && DaimondPanels.show('work')); |
| 95 | await sleep(700); |
| 96 | await page.click('#panel-work [data-act="refresh"]', { force: true }).catch(() => {}); |
| 97 | await sleep(800); |
| 98 | await page.evaluate(() => { |
| 99 | const row = [...document.querySelectorAll('#panel-work .files-row')] |
| 100 | .find((r) => r.dataset.path === 'books'); |
| 101 | if (row) row.click(); |
| 102 | }); |
| 103 | await sleep(900); |
| 104 | await 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. |
| 108 | const 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 | }); |
| 135 | console.log(JSON.stringify(said, null, 1)); |
| 136 | fs.writeFileSync(path.join(SHOTS, `hates-${TAG}.json`), JSON.stringify(said, null, 1) + '\n'); |
| 137 | await s.browser.close(); |