// scripts/qa-cdp-shoot-showcase.mjs — boss showcase batch (20 shots). // // Drives a fresh DSH_QA=1 Electron on --remote-debugging-port=9260 via // CDP; every shot uses fixture/mock data (no API burn). Writes PNGs into // docs/demo-shots/showcase-2026-07-18/ named 01-*.png … 20-*.png in list // order so the summary can be relayed 1:1. import { writeFileSync, mkdirSync, readFileSync } from 'node:fs' import { resolve } from 'node:path' const [,, portArg, outdirArg] = process.argv const port = portArg || '9260' const outdir = outdirArg || 'docs/demo-shots/showcase-2026-07-18' mkdirSync(outdir, { recursive: true }) // ---------------- CDP plumbing ---------------- async function cdp() { const listRes = await fetch(`http://localhost:${port}/json/list`) const targets = await listRes.json() const target = targets.find((t) => t.type === 'page') if (!target) throw new Error('no page target on port ' + port) const ws = new WebSocket(target.webSocketDebuggerUrl) await new Promise((r, x) => { ws.onopen = r; ws.onerror = (e) => x(e) }) let id = 1 const pending = new Map() const consoleEntries = [] ws.onmessage = (ev) => { const data = typeof ev.data === 'string' ? ev.data : String(ev.data) let msg; try { msg = JSON.parse(data) } catch { return } if (msg.method === 'Runtime.consoleAPICalled') { const text = (msg.params?.args || []).map((a) => a?.value ?? a?.description ?? '').join(' ') consoleEntries.push({ level: msg.params?.type, text }) return } if (msg.method === 'Runtime.exceptionThrown') { const desc = msg.params?.exceptionDetails?.exception?.description || msg.params?.exceptionDetails?.text || '' consoleEntries.push({ level: 'exception', text: desc }) return } if (msg.id != null && pending.has(msg.id)) { const [ok, err] = pending.get(msg.id); pending.delete(msg.id) if (msg.error) err(new Error(msg.error.message)); else ok(msg.result) } } const call = (m, p = {}, timeoutMs = 60000) => new Promise((ok, err) => { const _id = id++ const t = setTimeout(() => { pending.delete(_id); err(new Error(`cdp timeout: ${m}`)) }, timeoutMs) pending.set(_id, [(v) => { clearTimeout(t); ok(v) }, (e) => { clearTimeout(t); err(e) }]) ws.send(JSON.stringify({ id: _id, method: m, params: p })) }) const evjs = async (js) => { const r = await call('Runtime.evaluate', { expression: js, returnByValue: true, awaitPromise: true }) if (r.exceptionDetails) throw new Error(r.exceptionDetails.exception?.description || r.exceptionDetails.text) return r.result?.value } const sleep = (ms) => new Promise((r) => setTimeout(r, ms)) await call('Runtime.enable') await call('Page.enable') return { call, evjs, sleep, consoleEntries, close: () => ws.close() } } async function shoot(c, name) { // Retry once — heavy fixtures + expanded trees can push a single capture // past the default 60s window; a second attempt after a short breath // usually succeeds. let r for (let attempt = 0; attempt < 3; attempt++) { try { r = await c.call('Page.captureScreenshot', { format: 'png' }, 90000) break } catch (e) { if (attempt === 2) throw e console.log(' captureScreenshot retry:', e.message) await c.sleep(1500) } } const p = resolve(outdir, name + '.png') writeFileSync(p, Buffer.from(r.data, 'base64')) const size = Buffer.from(r.data, 'base64').length console.log(' wrote', p, `(${(size / 1024).toFixed(0)} KB)`) return { path: p, size } } async function closeDevtools(c) { await c.evjs(`(function(){ const btn = document.querySelector('.devtools-drawer .devtools-close') if (btn) { btn.click(); return { closed: true } } const d = document.querySelector('.devtools-drawer') if (d && !d.hidden) { d.hidden = true; return { closed: 'hidden' } } return { closed: false } })()`) } // closeOverlays — reset any transient right/drawer overlays that leak between // shots (fork compare drawer, context rail drawer). Called before every shot // so the visible pane fills the frame without residue from the previous prep. async function closeOverlays(c) { await c.evjs(`(function(){ // Fork compare drawer — has its own close button + hidden fallback const forkBtn = document.querySelector('.fork-compare-drawer button, #fork-compare-drawer button') // The Close chip carries "Close" text — walk close-family buttons. const forkCloseBtns = document.querySelectorAll('.fork-compare-drawer button, #fork-compare-drawer button, .playground-compare-drawer button') for (const b of forkCloseBtns) { const txt = (b.textContent || '').trim().toLowerCase() if (txt === 'close' || txt === '×' || b.classList.contains('close')) { b.click(); break } } const forkDrawer = document.querySelector('.fork-compare-drawer, #fork-compare-drawer') if (forkDrawer) forkDrawer.hidden = true // Note: do NOT set style.display here — openForkCompare's re-open only // toggles hidden (the property), so a stuck display:none would blank the // drawer even after a fresh mock fires. // Context rail drawer const railClose = document.getElementById('context-rail-drawer-close') if (railClose) railClose.click() const rail = document.getElementById('context-rail-drawer') if (rail && !rail.hidden) rail.hidden = true // Devtools drawer const dtClose = document.querySelector('.devtools-drawer .devtools-close') if (dtClose) dtClose.click() const dt = document.querySelector('.devtools-drawer') if (dt && !dt.hidden) dt.hidden = true // Compact drawer / any generic overlay drawer const drawers = document.querySelectorAll('.overlay-drawer[open], details.overlay-drawer[open]') drawers.forEach(d => { try { d.open = false } catch (_) {} }) // Runtime warning banner ("The daemon reported an issue.") — cosmetic // but crowds the top of every shot; dismiss it via its own X button // so subsequent shots start clean. const bannerX = document.querySelector('#chat-runtime-banner .chat-runtime-banner-dismiss') if (bannerX) bannerX.click() const banner = document.getElementById('chat-runtime-banner') if (banner) banner.remove() // Annotation "Rate trajectory" side panel — persists across tab // switches; nuke it explicitly so shots 16-21 don't inherit shot 15's // panel. The panel exposes a close via window.__dshAnnotation.close. if (window.__dshAnnotation && typeof window.__dshAnnotation.close === 'function') { try { window.__dshAnnotation.close() } catch (_) {} } const ann = document.querySelector('#annotation-drawer, .annotation-drawer, .annotation-panel, [data-annotation-panel]') if (ann) { ann.hidden = true; ann.setAttribute('aria-hidden', 'true'); ann.style.display = 'none' } // Full-trace overlay — belt-and-suspenders in case shots 05-08 chained // and the caller forgot the explicit cleanup. const overlay = document.querySelector('.devtools-full-trace-overlay') if (overlay && overlay.parentNode) overlay.parentNode.removeChild(overlay) return { ok: true } })()`) } async function switchTab(c, name) { return c.evjs(`(function(){ if (window.__dshTabs && typeof window.__dshTabs.switchTo === 'function') { window.__dshTabs.switchTo(${JSON.stringify(name)}) return { via: 'switchTo' } } const btn = document.querySelector('.tab-btn[data-tab="' + ${JSON.stringify(name)} + '"]') if (btn) { btn.click(); return { via: 'click' } } return { via: 'none' } })()`) } async function clickById(c, id) { return c.evjs(`(function(){ const b = document.getElementById(${JSON.stringify(id)}) if (b) { b.click(); return { ok: true, id: ${JSON.stringify(id)} } } return { ok: false, id: ${JSON.stringify(id)} } })()`) } async function scrollTop(c, sel) { return c.evjs(`(function(){ const el = ${sel ? `document.querySelector(${JSON.stringify(sel)})` : 'document.getElementById("stream")'} if (el) { el.scrollTop = 0; return { ok: true } } return { ok: false } })()`) } async function scrollBottom(c, sel) { return c.evjs(`(function(){ const el = ${sel ? `document.querySelector(${JSON.stringify(sel)})` : 'document.getElementById("stream")'} if (el) { el.scrollTop = el.scrollHeight; return { ok: true, sh: el.scrollHeight } } return { ok: false } })()`) } async function seedFresh(c) { await switchTab(c, 'chat') await c.sleep(200) return c.evjs(`(async () => { const { id } = await window.dsh.newSession() if (typeof window.__dshChat === 'object' && window.__dshChat && typeof window.__dshChat.select === 'function') { await window.__dshChat.select(id) } // Clear the stream so residual fixtures from the last shot are gone. const s = document.getElementById('stream'); if (s) s.innerHTML = '' return { id } })()`) } async function playFixture(c, name) { return c.evjs(`(async () => { if (typeof window.__dshQaPlayFixture !== 'function') return { err: 'no seam' } return await window.__dshQaPlayFixture(${JSON.stringify(name)}) })()`) } async function loadSample(c) { return c.evjs(`(async () => { if (typeof window.__dshLoadSampleTrace === 'function') { await window.__dshLoadSampleTrace() return { via: 'loadSampleTrace' } } return { via: 'none' } })()`) } // ---------------- driver ---------------- async function main() { const c = await cdp() await c.evjs(`document.title`) // handshake console.log('CDP handshake OK on port', port) // Set the viewport to 1440×900 via Emulation.setDeviceMetricsOverride // (the main.js hardcodes 1200×800; overriding here gives the requested // showcase framing without touching source). await c.call('Emulation.setDeviceMetricsOverride', { width: 1440, height: 900, deviceScaleFactor: 2, mobile: false, }) await c.sleep(300) const notes = [] const rec = async (name, note, fn) => { console.log('shot:', name, '—', note) // Reset every transient overlay before we prep this shot's state so leftover // drawers from #12/etc. don't bleed through. try { await closeOverlays(c) } catch (_) {} await c.sleep(120) try { await fn() } catch (e) { console.log(' prep err:', e.message) notes.push({ name, note, err: e.message }) return } await c.sleep(400) await closeDevtools(c) await c.sleep(150) const res = await shoot(c, name) notes.push({ name, note, size: res.size }) } // ── 01 Chat 空态 launcher (4 卡) ───────────────────────────────────── await rec('01-chat-empty-launcher', 'Chat 空态:4 卡 launcher 一屏', async () => { await switchTab(c, 'chat') await c.sleep(300) // Force the empty-welcome block back: if the daemon auto-selected a prior // session, fetch the template and re-inject the launcher. await c.evjs(`(async () => { const stream = document.getElementById('stream'); if (!stream) return const res = await fetch(window.location.href.split('#')[0]) const html = await res.text() const m = html.match(/
\\s*<\\/div>\\s*<\\/section>/) if (!m) return const block = m[0].replace(/<\\/section>$/, '').trim() stream.innerHTML = block })()`) await c.sleep(400) }) // ── 02 对话流全景(fixture 1.1-trace-full) ───────────────────────── await rec('02-conversation-flow-full', '对话流全景:turn 容器 + reasoning 折叠展开 + tool 行 + turn footer', async () => { await seedFresh(c) await playFixture(c, '1.1-trace-full.json') await c.sleep(1200) // Expand any reasoning-block details on the last turn so both folded and // expanded states show in the same frame. await c.evjs(`(function(){ const rs = document.querySelectorAll('details.reasoning-block, details.reasoning') if (rs.length > 0) rs[rs.length - 1].open = true // scroll to bottom so turn footer/glyph is visible const s = document.getElementById('stream'); if (s) s.scrollTop = s.scrollHeight })()`) await c.sleep(500) }) // ── 03 Reasoning 差异化(mock-reasoning-only + trace 折叠展开) ───── await rec('03-reasoning-differentiator', 'Reasoning 差异化:reasoning-only 折叠卡展开 + 行显 reasoning', async () => { await seedFresh(c) await c.sleep(200) await clickById(c, 'mock-reasoning-only') await c.sleep(1200) await c.evjs(`(function(){ const rs = document.querySelectorAll('details.reasoning-block, details.reasoning') rs.forEach(d => d.open = true) const s = document.getElementById('stream'); if (s) s.scrollTop = 0 })()`) await c.sleep(400) }) // ── 04 Tracing 一级页 ──────────────────────────────────────────────── await rec('04-tracing-page-eight-column', 'Tracing 一级页:八列表格,3-5 条会话', async () => { // Pre-seed multiple sessions with different fixtures so the tracing table has rows. for (const fx of ['sample-session.json', '1.1-trace-full.json', '2.1-turn-trajectory-mixed.json', '1.7-compact-three-events.json', '2.6-subagent-inline-trace.json']) { try { await seedFresh(c); await playFixture(c, fx); await c.sleep(500) } catch (_) {} } await switchTab(c, 'tracing') await c.sleep(1200) }) // ── 05 三视图 Tree ─────────────────────────────────────────────────── // Rework2 (2026-07-18): fixture playback drops trace-cards directly into // the stream (no turn-footer, no auto-drawer), so we manually mount the // tri-view via the __dshTraceTriView module against the trace card's // attached _rec step-record. That is the same code path the per-turn // drawer would trigger; we just wrap the card ourselves for the shot. const triSetup = async () => { await seedFresh(c) await playFixture(c, '1.1-trace-full.json') await c.sleep(1500) await switchTab(c, 'chat') await c.sleep(200) const mount = await c.evjs(`(function(){ // Build session-scope records from the active session's cached events. const mod = window.__dshTraceTriView if (!mod) return { ok: false, err: 'no tri module' } const sid = (window.__dshChat && typeof window.__dshChat.getActiveSessionId === 'function') ? window.__dshChat.getActiveSessionId() : null let events = [] if (window.__dshChat && typeof window.__dshChat.getEventsForActive === 'function') { events = window.__dshChat.getEventsForActive() || [] } const records = mod.sessionTraceRecords(events) // For the tree tab we want a real trace card — the fixture playback // drops one or more <.trace-card> into the stream. Wrap the outer // section that holds all cards so the Tree view shows a rich walked // record, not the "per-turn" stub. const cards = document.querySelectorAll('.trace-card') // Stack every card into a synthetic parent so treeEl carries all steps. let treeEl = null if (cards.length) { const stack = document.createElement('div') stack.className = 'showcase-triview-tree-stack' for (const c of cards) stack.appendChild(c.cloneNode(true)) treeEl = stack } const view = mod.buildTriView(document, { treeEl: treeEl, records: records && records.length ? records : (cards[0] && cards[0]._rec ? cards[0]._rec : []), scope: 'session', sessionId: sid, defaultView: 'tree', onSeqClick: () => {}, }) const wrap = document.createElement('div') wrap.className = 'showcase-triview-mount' wrap.style.padding = '16px 24px' wrap.appendChild(view) const stream = document.getElementById('stream') if (stream) { for (const child of Array.from(stream.children)) child.style.display = 'none' stream.appendChild(wrap) } // Dismiss any runtime warning banner that may have appeared during // fixture playback. const warn = document.querySelector('.runtime-warning, .warning-banner') if (warn) { warn.style.display = 'none' } const dismiss = document.querySelector('.runtime-warning button, .warning-banner button, [aria-label="dismiss"]') if (dismiss) dismiss.click() wrap.scrollIntoView({ block: 'start' }) return { ok: true, via: 'session-scope', records: (records || []).length, cards: cards.length } })()`) console.log(' triSetup:', JSON.stringify(mount)) await c.sleep(500) } await rec('05-triview-tree', '三视图 Tree:内联时间条 + model chip + token/duration pill', async () => { await triSetup() await c.evjs(`(function(){ const chip = document.querySelector('.showcase-triview-mount .trace-tri-chip.chip-tree, .showcase-triview-mount .trace-tri-chip[data-view="tree"]') if (chip) chip.click() })()`) await c.sleep(500) }) // ── 06 三视图 Timeline (Gantt) ─────────────────────────────────────── await rec('06-triview-timeline', '三视图 Timeline (Gantt)', async () => { const present = await c.evjs(`(function(){ return !!document.querySelector('.showcase-triview-mount') })()`) if (!present) await triSetup() await c.evjs(`(function(){ const chip = document.querySelector('.showcase-triview-mount .trace-tri-chip.chip-timeline, .showcase-triview-mount .trace-tri-chip[data-view="timeline"]') if (chip) chip.click() })()`) await c.sleep(500) }) // ── 07 三视图 Graph ───────────────────────────────────────────────── await rec('07-triview-graph', '三视图 Graph:节点选中态', async () => { const present = await c.evjs(`(function(){ return !!document.querySelector('.showcase-triview-mount') })()`) if (!present) await triSetup() await c.evjs(`(function(){ const chip = document.querySelector('.showcase-triview-mount .trace-tri-chip.chip-graph, .showcase-triview-mount .trace-tri-chip[data-view="graph"]') if (chip) chip.click() })()`) await c.sleep(500) await c.evjs(`(function(){ const scope = document.querySelector('.showcase-triview-mount') || document const node = scope.querySelector('.trace-graph-node, [data-graph-node], .graph-node, circle[data-node], g[data-node]') if (node) { // SVG elements don't have HTMLElement.click(); dispatch a proper MouseEvent instead. const ev = new MouseEvent('click', { bubbles: true, cancelable: true, view: window }) node.dispatchEvent(ev) node.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true })) // Add a visual selected marker in case the tri-view keys on this class. node.classList && node.classList.add('selected') } })()`) await c.sleep(300) }) // ── 08 Detail pane (四段 + Fields 递归树 ≥3 层) ──────────────────── await rec('08-detail-pane-fields-tree', 'Detail pane:四区段 + Fields 递归树 ≥3 层展开', async () => { const present = await c.evjs(`(function(){ return !!document.querySelector('.showcase-triview-mount') })()`) if (!present) await triSetup() await c.evjs(`(function(){ const chip = document.querySelector('.showcase-triview-mount .trace-tri-chip.chip-tree, .showcase-triview-mount .trace-tri-chip[data-view="tree"]') if (chip) chip.click() })()`) await c.sleep(400) await c.evjs(`(function(){ const scope = document.querySelector('.showcase-triview-mount') || document // Find a leaf whose click will populate the detail pane. const rows = scope.querySelectorAll('.trace-event-row, .trace-tree-row, [data-step-row]') let hit = null for (const r of rows) { const txt = (r.textContent || '').toLowerCase() if (/tool|result|fields|read|write/.test(txt)) hit = r } if (!hit && rows.length) hit = rows[Math.min(2, rows.length - 1)] if (hit) hit.click() setTimeout(() => { const detailsAll = document.querySelectorAll('.trace-detail-pane details') detailsAll.forEach(d => d.open = true) }, 200) })()`) await c.sleep(900) }) // ── 09 Error tab (5 tab 红 banner) ────────────────────────────────── await rec('09-error-tab-banner', 'Error tab:5 tab 红 banner 态', async () => { // Remove any tri-view mount from the previous sequence and un-hide // the stream's original children. await c.evjs(`(function(){ const mount = document.querySelector('.showcase-triview-mount') if (mount && mount.parentNode) mount.parentNode.removeChild(mount) const stream = document.getElementById('stream') if (stream) for (const child of Array.from(stream.children)) child.style.display = '' const o = document.querySelector('.devtools-full-trace-overlay') if (o && o.parentNode) o.parentNode.removeChild(o) })()`) await c.sleep(150) await seedFresh(c) await playFixture(c, 'trace-parity-error-tool-result.json') await c.sleep(1000) await switchTab(c, 'chat') await c.sleep(200) await c.evjs(`(async () => { const drawers = document.querySelectorAll('details.turn-trace-drawer') if (drawers.length > 0) { const d = drawers[drawers.length - 1] d.open = true d.scrollIntoView({ block: 'center' }) } await new Promise(r => setTimeout(r, 500)) const scope = document.querySelector('details.turn-trace-drawer[open]') || document // Click into an error row const rows = scope.querySelectorAll('.trace-event-row, .trace-tree-row') for (const r of rows) { if (/error|fail|reject/i.test(r.textContent || '')) { r.click(); break } } await new Promise(r => setTimeout(r, 300)) // Click Error tab explicitly if present const errBtn = document.querySelector('.trace-detail-tab.is-error, .trace-detail-tab[data-tab="error"]') if (errBtn) errBtn.click() })()`) await c.sleep(600) }) // ── 10 Reasoning tab (5th tab 展开 982 tok) ───────────────────────── await rec('10-reasoning-tab', 'Reasoning tab:5th tab 展开 (differentiator)', async () => { await seedFresh(c) await clickById(c, 'mock-reasoning-only') await c.sleep(1200) await c.evjs(`(async () => { const drawers = document.querySelectorAll('details.turn-trace-drawer') if (drawers.length > 0) { const d = drawers[drawers.length - 1] d.open = true d.scrollIntoView({ block: 'center' }) } await new Promise(r => setTimeout(r, 500)) const scope = document.querySelector('details.turn-trace-drawer[open]') || document // Find a row that had reasoning tokens const rows = scope.querySelectorAll('.trace-event-row, .trace-tree-row') let hit = null for (const r of rows) { const txt = (r.textContent || '').toLowerCase() if (/reason|reasoning|assistant|llm/.test(txt)) hit = r } if (!hit && rows.length) hit = rows[rows.length - 1] if (hit) hit.click() await new Promise(r => setTimeout(r, 300)) const rBtn = document.querySelector('.trace-detail-tab[data-tab="reasoning"]') if (rBtn) rBtn.click() })()`) await c.sleep(500) }) // ── 11 Edit & re-run header ───────────────────────────────────────── await rec('11-edit-rerun-header', 'Edit & re-run:header 改参面板展开', async () => { await seedFresh(c) await loadSample(c) await c.sleep(1000) await switchTab(c, 'chat') await c.sleep(300) await c.evjs(`(function(){ // Open all edit-rerun-header details in the stream. const hs = document.querySelectorAll('details.edit-rerun-header') if (hs.length > 0) { const target = hs[hs.length - 1] target.open = true target.scrollIntoView({ block: 'center' }) } })()`) await c.sleep(500) }) // ── 12 Fork compare drawer ────────────────────────────────────────── await rec('12-fork-compare-drawer', 'Fork compare:父子并排抽屉', async () => { await seedFresh(c) await clickById(c, 'mock-fork-compare') await c.sleep(1000) await c.evjs(`(function(){ const drawer = document.querySelector('.fork-compare-drawer, .compare-drawer, [data-fork-compare]') if (drawer) drawer.scrollIntoView({ block: 'center' }) })()`) await c.sleep(400) }) // ── 13 Compact 卡 三 tab Diff 态 ──────────────────────────────────── await rec('13-compact-card-diff-tab', 'Compact 卡:三 tab Diff 态', async () => { await seedFresh(c) await playFixture(c, '1.7-compact-three-events.json') await c.sleep(1000) await c.evjs(`(function(){ // Click the diff tab on the compact card. const cards = document.querySelectorAll('.compact-card, .compact-badge, [data-compact-card]') let hit = null for (const c2 of cards) { const btn = c2.querySelector('button[data-tab="diff"], .compact-tab-diff, .compact-tabs .diff') if (btn) { btn.click(); hit = c2; break } } if (!hit) { // Fallback: find any tab labeled "Diff" inside a compact-* container const allBtns = document.querySelectorAll('.compact-card button, .compact-badge button') for (const b of allBtns) if (/diff/i.test(b.textContent || '')) { b.click(); break } } const first = document.querySelector('.compact-card, .compact-badge') if (first) first.scrollIntoView({ block: 'center' }) })()`) await c.sleep(500) }) // ── 14 Subagent 实时子轨迹 ────────────────────────────────────────── await rec('14-subagent-inline-trace', 'Subagent 实时子轨迹:DONE/RUNNING 卡', async () => { await seedFresh(c) await playFixture(c, '2.6-subagent-inline-trace.json') await c.sleep(1000) await c.evjs(`(function(){ const s = document.getElementById('stream'); if (s) s.scrollTop = s.scrollHeight // Ensure any subagent trace details are open const details = document.querySelectorAll('details.subagent-inline, details[data-subagent], .subagent-card details') details.forEach(d => d.open = true) })()`) await c.sleep(500) }) // ── 15 标注面板:三类型 rubric 打分态 ─────────────────────────────── await rec('15-annotation-panel-typed', '标注面板:Continuous 按钮排 + Categorical 枚举', async () => { await seedFresh(c) await loadSample(c) await c.sleep(600) await switchTab(c, 'chat') await c.sleep(200) await c.evjs(`(async () => { if (window.__dshAnnotation && typeof window.__dshAnnotation.open === 'function') { const sid = (window.__dshAnnotationSamples && window.__dshAnnotationSamples.sessions && window.__dshAnnotationSamples.sessions[0] && window.__dshAnnotationSamples.sessions[0].sessionId) || 'demo-session' window.__dshAnnotation.open(sid) } })()`) await c.sleep(800) }) // ── 16 Rubrics 页 + Create-from-scratch 表单 ──────────────────────── await rec('16-rubrics-page-create', 'Rubrics 页:28 类目录 + Create-from-scratch 表单', async () => { await switchTab(c, 'rubrics') await c.sleep(700) await c.evjs(`(function(){ // Click a "Create from scratch" / "new rubric" button if present. const btns = document.querySelectorAll('button') for (const b of btns) { const t = (b.textContent || '').toLowerCase() if (/create.*scratch|new rubric|from scratch/.test(t)) { b.click(); return { ok: true } } } // Also try id-based hooks. const bId = document.getElementById('rubrics-create-from-scratch') || document.getElementById('rubric-new') if (bId) { bId.click(); return { ok: 'id' } } return { ok: false } })()`) await c.sleep(700) }) // ── 17 Bench 页 (4 实验八列表) ─────────────────────────────────────── await rec('17-bench-page', 'Bench 页:4 实验八列表 + 详情', async () => { await switchTab(c, 'bench') await c.sleep(900) }) // ── 18 Hub 页 (七类资产目录) ──────────────────────────────────────── await rec('18-hub-page', 'Hub 页:七类资产目录', async () => { await switchTab(c, 'hub') await c.sleep(900) }) // ── 19 Context 页 (turn 行 + SDK legend) ──────────────────────────── await rec('19-context-page', 'Context 页:加载 sample 后的 turn 行 + SDK legend', async () => { // Seed sample so context ledger is populated. await seedFresh(c) await loadSample(c) await c.sleep(600) await switchTab(c, 'context') await c.sleep(900) }) // ── 20 Session Tree (fork 实线/subagent 虚线) ─────────────────────── await rec('20-session-tree', 'Session Tree:demo forest — fork 实线/subagent 虚线', async () => { await switchTab(c, 'tree') await c.sleep(1000) }) // ── 21 (bonus) turn/end error 完整行 ──────────────────────────────── await rec('21-turn-end-error-row', 'turn/end · error 完整行 (红字全因)', async () => { await seedFresh(c) await clickById(c, 'mock-turn-end-error') await c.sleep(900) await c.evjs(`(function(){ const s = document.getElementById('stream'); if (s) s.scrollTop = s.scrollHeight })()`) await c.sleep(400) }) // Console-probe assertion const syntaxHits = c.consoleEntries.filter(e => /SyntaxError/i.test(e.text)) const jsonPath = resolve(outdir, 'console-report.json') writeFileSync(jsonPath, JSON.stringify({ port, outdir, totalConsoleEntries: c.consoleEntries.length, syntaxErrors: syntaxHits, exceptions: c.consoleEntries.filter(e => e.level === 'exception').slice(0, 20), }, null, 2)) console.log('console report:', jsonPath, 'syntaxErrors=' + syntaxHits.length) // Write per-shot notes summary writeFileSync(resolve(outdir, 'shots-manifest.json'), JSON.stringify(notes, null, 2)) c.close() } main().catch((e) => { console.error(e); process.exit(1) })