Files
deepseek-harness/examples/desktop/scripts/qa-cdp-shoot-showcase.mjs
ZiyaZhang e8f5c0b51b feat(desktop): DSH Electron desktop shell — harness internals visualized
Minimal Electron shell over the DSH JSON-RPC runtime — a first-look at
what a ChatGPT.app-style host on top of the DeepSeek Harness looks
like, with the harness's normally-invisible internals (trace timeline,
context surface, subagent tree, compaction, plugin registry, rubrics)
brought forward as first-class UI surfaces so plugin authors and
researchers can see what the agent is actually doing.

Runs against three keyless-to-live profiles (stdio-echo works on
master out of the box; daemon-echo / daemon-vibe-echo activate once
the daemon-demo lands; stdio-deepseek and daemon-vibe hit the real
DeepSeek API when you supply a key). HARNESS_DEV auto-resolves to the
in-repo runtime when this shell ships under examples/desktop/, so a
fresh clone launches without config; env DSH_DEV_ROOT overrides for
custom layouts, and a sibling deepseek-harness-dev/ checkout is the
original dev workflow.

Cold-clone gate (P0 fixes for first-time-clone usability):
- HARNESS_DEV: 3-candidate resolver (env → walk-up in-repo marker →
  sibling), unit-tested via mock fs so ordering is locked without
  needing either real layout on disk.
- config yml leaves rewritten at assemble time so the sibling-clone
  paths (../../deepseek-harness-dev/examples/echo-agent/…) become
  the in-repo paths (../../echo-agent/…) in the released tree —
  source yml stays usable for local dev, released tree ships a
  working shape.
- pnpm-workspace.yaml allowBuilds.electron = true (was placeholder).
- missing-key card in stdio-deepseek offers a one-click switch to
  stdio-echo (the keyless profile that works on master) rather than
  daemon-echo (blocked on the not-yet-shipped daemon-demo).
- assemble-oss-release.sh rewrites the source-side breadcrumb name
  'dsh-desktop-demo' → 'dsh-desktop' for the released package.json.

FOUC guard on the onboarding gate (41fc5df carried) keeps the
first-launch splash from flashing before the runtime probe finishes.

Test suite (1634 tests in source, 3990 in the runtime repo) covers
resolver ordering, renderer classifiers, trace timeline shape,
compaction diff rendering, rubric parity, and the missing-key
onboarding paths.
2026-07-18 12:59:34 -07:00

679 lines
31 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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(/<div class="empty-welcome"[\\s\\S]*?<\\/div>\\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 tab5 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 tab5th 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-runheader 改参面板展开', 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 Treedemo 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) })