feat(desktop): context page deepening — window bar + compact Config + intervention marker + subagent drilldown

Four Context-page enhancements (lane-ctx-deep, F1-F4):

- F1 context window breakdown: replace percentage-only card header with
  a stacked-bar breakdown of input / cached / output token buckets,
  plus a right-side gauge showing the live window occupancy ratio.
- F2 compact Config tab: fold the sprawling profile Config editor into
  a Config tab on the Context page card, with the same yml-leaf
  ordering as the top-of-window profile picker.
- F3 intervention marker: on the intervention timeline, emit a marker
  glyph at each user-intervention row (turn-flow-glyph-style) so the
  card scans as a single stream instead of a header + separate list.
- F4 subagent drilldown: when a turn's tool trace hits a subagent, the
  Trace panel's Config + Output tabs get a second row of Subagent
  Config / Subagent Output tabs immediately below, driven by the same
  fold-in-place shape the parent panel already uses.

4 new renderer modules (compact-config-model / context-window-breakdown /
intervention-timeline / subagent-drilldown), 6 new test files (41 tests,
all node --test style), 4 QA shoot scripts for CDP-driven regression
screenshots.
This commit is contained in:
ZiyaZhang
2026-07-19 01:22:28 -07:00
parent c8f5ca3d5b
commit fc558c2db8
18 changed files with 2705 additions and 4 deletions

View File

@@ -0,0 +1,89 @@
// Real-machine isolated runner for the lane-ctx-deep shots (task #51).
// Boots Electron on a private CDP port with a private USER_DATA and
// DSH_DESKTOP_HOME under $TMPDIR (2026-07-18 postmortem — nothing here
// touches the user's real ~/.dsh-desktop), then invokes the shot script.
//
// Usage: node scripts/qa-cdp-run-ctx-deep.mjs
import { spawn } from 'node:child_process'
import { existsSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'
import { resolve, join } from 'node:path'
import { setTimeout as sleep } from 'node:timers/promises'
import { tmpdir } from 'node:os'
const WORKTREE = resolve(process.env.DSH_WORKTREE || process.cwd())
const PARENT = resolve(process.env.DSH_REPO || '/Users/ziya/harness/dsh-desktop-demo')
const ELECTRON = join(PARENT, 'node_modules/.bin/electron')
const CDP_PORT = Number(process.env.DSH_CTX_DEEP_PORT || 9285)
const USER_DATA = join(tmpdir(), 'dsh-ctx-deep-userdata')
const DSH_HOME = join(tmpdir(), 'dsh-ctx-deep-home')
const OUTDIR = join(WORKTREE, 'docs/demo-shots')
const SHOOT_SCRIPT = join(WORKTREE, 'scripts/qa-cdp-shoot-ctx-deep.mjs')
if (!existsSync(ELECTRON)) {
console.error(`electron binary not found at ${ELECTRON}`)
process.exit(2)
}
mkdirSync(OUTDIR, { recursive: true })
for (const dir of [USER_DATA, DSH_HOME]) {
try { rmSync(dir, { recursive: true, force: true }) } catch {}
mkdirSync(dir, { recursive: true })
}
// Seed a minimal config so onboarding modal stays out of the way. Use
// echo-jsonrpc (jsonrpc-demo bin) — daemon-demo has been renamed to
// jsonrpc-demo upstream and the daemon-* config still points at the old
// path, so echo-jsonrpc is the correct keyless profile today.
writeFileSync(join(DSH_HOME, 'user-overlay.cordis.yml'), [
'# ctx-deep QA seed overlay',
'plugins:',
` - "@cordisjs/plugin-include":`,
` path: ${join(WORKTREE, 'config/echo-jsonrpc.yml')}`,
'',
].join('\n'))
writeFileSync(join(DSH_HOME, 'config.json'), JSON.stringify({ role: 'coding', approvalMode: 'never' }))
writeFileSync(join(DSH_HOME, '.onboarded'), new Date().toISOString())
const child = spawn(ELECTRON, [
`--remote-debugging-port=${CDP_PORT}`,
`--user-data-dir=${USER_DATA}`,
'--disable-gpu',
'--no-sandbox',
'.',
], {
cwd: WORKTREE,
env: {
...process.env,
DSH_DESKTOP_HOME: DSH_HOME,
DSH_DEV_ROOT: process.env.DSH_DEV_ROOT || '/Users/ziya/harness/deepseek-harness-dev',
DSH_MAXIMIZE: '1',
DSH_QA: '1',
},
stdio: ['ignore', 'pipe', 'pipe'],
})
const logs = []
child.stdout.on('data', d => logs.push(String(d)))
child.stderr.on('data', d => logs.push(String(d)))
let up = false
for (let i = 0; i < 40; i++) {
await sleep(500)
try {
const r = await fetch(`http://localhost:${CDP_PORT}/json/list`)
if (r.ok) { up = true; break }
} catch {}
}
if (!up) {
child.kill('SIGKILL')
console.error('electron did not come up on CDP :' + CDP_PORT)
console.error(logs.join(''))
process.exit(3)
}
// Run the shot script against our port.
const shooter = spawn(process.execPath, [SHOOT_SCRIPT, String(CDP_PORT), OUTDIR], {
cwd: WORKTREE,
stdio: 'inherit',
})
const rc = await new Promise((r) => shooter.on('exit', (code) => r(code)))
child.kill('SIGKILL')
process.exit(rc || 0)

View File

@@ -0,0 +1,388 @@
// Shots for lane-ctx-deep (task #51) — four Context-page enhancements.
// Each shot loads a QA fixture, forces a re-render, then captures the
// visible chrome for that feature. Real-machine isolated per team-lead:
// DSH_QA=1 + DSH_DESKTOP_HOME under /tmp/dsh-qa-ctx-deep so nothing here
// touches the user's live session store.
//
// Shots produced (docs/demo-shots/):
// ctx-deep-01-window-bar.png — Context page top strip, window
// occupancy stacked bar + legend.
// ctx-deep-02-intervention.png — Context page top strip, intervention
// markers axis.
// ctx-deep-03-compact-config.png — Compact card on Chat tab with the
// new "Config" tab active.
// ctx-deep-04-subagent.png — Subagent card with drill-down tabs
// (Tool defs / Inbound query).
import { writeFileSync, mkdirSync } from 'node:fs'
import { resolve } from 'node:path'
const port = process.argv[2] || '9241'
const outdir = process.argv[3] || 'docs/demo-shots'
mkdirSync(outdir, { recursive: true })
async function main () {
const targets = await (await fetch(`http://localhost:${port}/json/list`)).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()
ws.onmessage = (ev) => {
const msg = JSON.parse(typeof ev.data === 'string' ? ev.data : String(ev.data))
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 = 20000) => new Promise((ok, err) => {
const _id = id++
const timer = setTimeout(() => { pending.delete(_id); err(new Error('cdp timeout: ' + m)) }, timeoutMs)
pending.set(_id, [(v) => { clearTimeout(timer); ok(v) }, (e) => { clearTimeout(timer); err(e) }])
ws.send(JSON.stringify({ id: _id, method: m, params: p }))
})
const evj = async (expr) => {
const r = await call('Runtime.evaluate', {
expression: `(async()=>{try{return (${expr})}catch(e){return {__err: String(e)}}})()`,
returnByValue: true, awaitPromise: true,
})
if (r.exceptionDetails) throw new Error(r.exceptionDetails.exception?.description || r.exceptionDetails.text)
return r.result?.value
}
const wait = (ms) => new Promise((r) => setTimeout(r, ms))
const shoot = async (name) => {
const shot = await call('Page.captureScreenshot', {
format: 'png',
clip: { x: 0, y: 0, width: 1440, height: 900, scale: 2 },
}, 30000)
const out = resolve(outdir, name)
writeFileSync(out, Buffer.from(shot.data, 'base64'))
console.log(out)
}
await call('Page.enable')
await evj(`window.dshQa && window.dshQa.revealWindow ? await window.dshQa.revealWindow() : null`)
await call('Emulation.setDeviceMetricsOverride', {
width: 1440, height: 900, deviceScaleFactor: 2, mobile: false,
})
// Dismiss onboarding.
await evj(`(function(){
const btns = Array.from(document.querySelectorAll('button'));
const skip = btns.find(b => /skip and use defaults/i.test(b.textContent || ''));
if (skip) { skip.click(); }
return 'ok';
})()`)
await wait(300)
// Hide devtools drawer.
await evj(`(function(){
const d = document.querySelector('.devtools-drawer');
if (d) d.style.display = 'none';
return 'ok';
})()`)
// Fabricate a session directly in renderer state — bypasses the need for
// a live runtime, which the desktop demo doesn't need for these shots.
// We manufacture a plausible cachedEvents array covering all five window
// families + all three intervention kinds + a compact event, then expose
// it via a temporary __dshChat.getEventsForActive() override so the
// Context page reads the same shape production would.
await evj(`(function(){
const now = Date.now()
const events = []
let seq = 1
function push(ev){ ev.seq = seq++; ev.time = now + seq*10; events.push(ev) }
// system prompt seed
push({ type: 'context/message', data: { content: [{type:'text', text: 'You are DSH, an autonomous agent operating in a research environment. Use the tools carefully.'}], source: { kind: 'system' } } })
// tool calls (drive the tool_defs slice)
push({ type: 'tool/call', data: { name: 'read_file', arguments: '{"path":"a.md"}' } })
push({ type: 'tool/call', data: { name: 'search', arguments: '{"q":"context ledger"}' } })
// user
push({ type: 'user/message', data: { content: [{type:'text', text: 'Show me the ledger'}] } })
// reasoning
push({ type: 'assistant/reasoning', data: { content: [{type:'text', text: 'The user wants a ledger view. Let me project turns.'}] } })
// assistant
push({ type: 'assistant/message', data: { content: [{type:'text', text: 'Here is the per-turn ledger with injects and compacts.'}], usage: { inputTokens: 8000, outputTokens: 320, thinking: 180 } } })
// steer intervention
push({ type: 'steering/message', data: { content: [{type:'text', text: 'Actually skip the recall section.'}] } })
// plugin inject
push({ type: 'context/message', data: { content: [{type:'text', text: 'time-context: 09:34 UTC'}], source: { kind: 'plugin', plugin: 'time-context' } } })
push({ type: 'context/message', data: { content: [{type:'text', text: 'guard hint fired'}], source: { kind: 'plugin', plugin: 'guard' } } })
// turn end
push({ type: 'turn/end', data: { turn: 1, reason: { kind: 'completed' } } })
// edit-rerun intervention on the next turn
push({ type: 'user/message', data: { content: [{type:'text', text: 'Show me the ledger and expand turn 1'}], editRerun: { origSeq: 4, reason: 'clarify intent' } } })
push({ type: 'assistant/reasoning', data: { content: [{type:'text', text: 'Re-running with expanded turn 1.'}] } })
push({ type: 'assistant/message', data: { content: [{type:'text', text: 'Sure. Turn 1 details attached.'}], usage: { inputTokens: 8500, outputTokens: 250, thinking: 90 } } })
push({ type: 'compact/summary', data: { summary: [{type:'text', text: 'Turn 1 summary retained.'}], model: 'deepseek-chat', maxTokens: 512, shadowedTokenCount: 6400, shadowedRange: {start:1, end:8} } })
push({ type: 'turn/end', data: { turn: 2, reason: { kind: 'completed' } } })
// fork intervention
push({ type: 'session/forked', data: { parentSeq: 14 } })
push({ type: 'context/message', data: { content: [{type:'text', text: 'assistant reloaded'}], source: { kind: 'system' } } })
push({ type: 'user/message', data: { content: [{type:'text', text: 'What happened in the fork?'}] } })
push({ type: 'assistant/message', data: { content: [{type:'text', text: 'The fork branched at seq 14.'}], usage: { inputTokens: 2100, outputTokens: 90 } } })
push({ type: 'turn/end', data: { turn: 3, reason: { kind: 'completed' } } })
// Install override for the Context page's read path.
const sid = 'ctx-deep-demo-session'
const chat = window.__dshChat = window.__dshChat || {}
const oldGetActive = chat.getActiveSessionId
const oldGetEvents = chat.getEventsForActive
chat.getActiveSessionId = () => sid
chat.getEventsForActive = () => events
chat.getSessions = () => [{ id: sid, title: 'ctx-deep demo', running: false }]
window.__ctxDeepDemoEvents = events
return { sid, count: events.length }
})()`)
await wait(700)
// -- SHOT 1 + 2: Context page top strip (window bar + intervention) ------
await evj(`(function(){
if (window.__dshTabs && window.__dshTabs.switchTo) { window.__dshTabs.switchTo('context'); }
return 'switched';
})()`)
await wait(500)
await evj(`window.__dshContextPage && window.__dshContextPage.refresh && window.__dshContextPage.refresh()`)
await wait(300)
const layout = await evj(`(function(){
const strip = document.querySelector('[data-context-topstrip]');
const bar = document.getElementById('context-window-bar-track');
const iv = document.getElementById('context-intervention-track');
const pane = document.querySelector('.pane[data-pane="context"]');
const chat = window.__dshChat;
return {
paneHidden: pane ? pane.hidden : 'NO-PANE',
paneHtmlLen: pane ? pane.innerHTML.length : 0,
paneChildren: pane ? Array.from(pane.children).map(c => c.tagName+':'+(c.className||c.id||'')) : null,
documentTitle: document.title,
windowLocation: String(window.location),
chatKeys: chat ? Object.keys(chat) : null,
chatGetEvents: chat && typeof chat.getEventsForActive === 'function',
chatGetSid: chat && typeof chat.getActiveSessionId === 'function' ? chat.getActiveSessionId() : null,
eventsLen: chat && chat.getEventsForActive ? chat.getEventsForActive().length : -1,
stripFound: !!strip,
stripHidden: strip ? strip.hidden : null,
segCount: bar ? bar.children.length : 0,
markerCount: iv ? iv.querySelectorAll('.context-intervention-marker').length : 0,
summary: (document.getElementById('context-window-bar-summary')||{}).textContent||'',
};
})()`)
console.error('ctx-deep layout ->', JSON.stringify(layout))
await shoot('ctx-deep-01-window-bar.png')
// Highlight the intervention strip: scroll to it if needed and take shot.
await evj(`(function(){
const strip = document.querySelector('.context-intervention-strip');
if (strip && strip.scrollIntoView) strip.scrollIntoView({ block: 'start', behavior: 'auto' });
return 'scrolled';
})()`)
await wait(300)
await shoot('ctx-deep-02-intervention.png')
// -- SHOT 3: Compact card Config tab -------------------------------------
// We render the compact card by direct DOM injection into the chat stream:
// the compact/summary event we seeded already flows through onSessionEvent
// path in real prod, but here we don't have a live session so we build a
// standalone compact card via __dshCompactCard.mountTabs.
await evj(`(function(){
if (window.__dshTabs && window.__dshTabs.switchTo) window.__dshTabs.switchTo('chat');
const panes = document.querySelectorAll('.pane[data-pane]');
for (const p of panes) p.hidden = (p.getAttribute('data-pane') !== 'chat');
const rail = document.getElementById('context-rail'); if (rail) rail.hidden = true;
const drawer = document.querySelector('.devtools-drawer'); if (drawer) drawer.style.display = 'none';
for (const sel of ['.onboarding','.mock-cards-menu','.dropdown-open','[data-mock-cards]']) {
for (const n of document.querySelectorAll(sel)) n.style.display = 'none';
}
return 'chat';
})()`)
await wait(400)
const compactMount = await evj(`(function(){
// Build a full-page overlay to keep the shot focused on the card.
const prevOverlay = document.getElementById('ctx-deep-demo-overlay');
if (prevOverlay) prevOverlay.remove();
const prev = document.getElementById('ctx-deep-demo-compact-card');
if (prev) prev.remove();
const overlay = document.createElement('div');
overlay.id = 'ctx-deep-demo-overlay';
overlay.style.cssText = 'position:fixed;left:0;top:0;right:0;bottom:0;background:var(--bg,#fff);z-index:9999;padding:48px 96px;overflow:auto;font-family:inherit;';
const heading = document.createElement('h1');
heading.style.cssText = 'font-size:24px;margin:0 0 16px 0;';
heading.textContent = 'Compact card — Config tab';
overlay.appendChild(heading);
const sub = document.createElement('div');
sub.className = 'muted';
sub.style.cssText = 'margin-bottom:24px;color:var(--muted,#5b6b7d);';
sub.textContent = 'Read-only view of the current compaction policy, threshold, and distance to next fire.';
overlay.appendChild(sub);
const stream = overlay;
const card = document.createElement('details');
card.id = 'ctx-deep-demo-compact-card';
card.className = 'compact-card compact-card-demo';
card.open = true;
card.style.margin = '24px auto';
card.style.maxWidth = '760px';
const sum = document.createElement('summary');
sum.className = 'summary';
const badge = document.createElement('span');
badge.className = 'compact-badge compact-badge-on-demand';
badge.textContent = 'on-demand';
sum.appendChild(badge);
const label = document.createElement('span');
label.textContent = '── context compacted ──';
sum.appendChild(label);
const tk = document.createElement('span');
tk.className = 'tokens'; tk.textContent = '6400 tokens'; sum.appendChild(tk);
const evc = document.createElement('span');
evc.className = 'events'; evc.textContent = 'compacted 8 events'; sum.appendChild(evc);
card.appendChild(sum);
const data = { model: 'deepseek-chat', maxTokens: 512, shadowedTokenCount: 6400,
shadowedRange: { start: 1, end: 8 },
summary: [{ type: 'text', text: 'Turn 1 summary retained (system prompt, tool defs, initial query, guardhint).' }],
};
const events = window.__ctxDeepDemoEvents || [];
if (window.__dshCompactCard && window.__dshCompactCard.mountTabs) {
window.__dshCompactCard.mountTabs(card, {
document,
initial: 'config',
fillPre(body){ body.textContent = '(see events 18; summary compressed to 1 line)'; body.style.padding = '8px'; },
fillPost(body){ body.textContent = data.summary[0].text; body.style.padding = '8px'; },
fillMeta(body){
const dl = document.createElement('dl'); dl.className = 'compact-card-tab-meta';
for (const [k,v] of Object.entries({ Trigger: 'on-demand', Model: data.model, 'Summary cap': '≤512 tok', 'Compacted range': 'seq 18', 'Compacted volume': '6400 tok' })) {
const dt = document.createElement('dt'); dt.textContent = k;
const dd = document.createElement('dd'); dd.textContent = v;
dl.appendChild(dt); dl.appendChild(dd);
}
body.appendChild(dl);
},
fillConfig(body){
if (!window.__dshCompactConfigModel) { body.textContent = 'model missing'; return; }
const v = window.__dshCompactConfigModel.buildCompactConfigView(events);
const dl = document.createElement('dl'); dl.className = 'compact-card-tab-meta compact-config-list';
const rows = [
['Threshold', v.thresholdTokens.toLocaleString() + ' tok' + (v.thresholdSource==='assumed'?' (assumed)':'')],
['Strategy', v.strategyName],
['Model', v.model || 'deepseek-chat'],
['Summary cap', v.maxSummaryTokens != null ? '≤' + v.maxSummaryTokens + ' tok' : 'unknown'],
['Triggers fired', v.triggersFired + ' this session'],
['Tokens since last compact', v.tokensSinceLastCompact.toLocaleString() + ' tok'],
['Tokens until next', v.tokensUntilNext.toLocaleString() + ' tok'],
];
for (const [k, val] of rows) {
const dt = document.createElement('dt'); dt.textContent = k;
const dd = document.createElement('dd'); dd.textContent = val;
dl.appendChild(dt); dl.appendChild(dd);
}
body.appendChild(dl);
const progWrap = document.createElement('div');
progWrap.className = 'compact-config-progress compact-config-progress--' + v.progressLevel;
const progHead = document.createElement('div');
progHead.className = 'compact-config-progress-head';
const pt = document.createElement('span');
pt.className = 'compact-config-progress-title'; pt.textContent = 'Progress to next compact';
const pp = document.createElement('span');
pp.className = 'compact-config-progress-pct muted small'; pp.textContent = Math.min(100, Math.round(v.progressPct)) + '%';
progHead.appendChild(pt); progHead.appendChild(pp);
const track = document.createElement('div'); track.className = 'compact-config-progress-track';
const fill = document.createElement('div'); fill.className = 'compact-config-progress-fill';
fill.style.setProperty('--fill-pct', Math.min(100, Math.max(0, v.progressPct)) + '%');
track.appendChild(fill); progWrap.appendChild(progHead); progWrap.appendChild(track);
body.appendChild(progWrap);
const note = document.createElement('div');
note.className = 'compact-config-note muted small';
note.textContent = 'Read-only view. Adjust in Settings Compaction (restart-required until session/set-compact-policy lands, gap G2).';
body.appendChild(note);
},
});
}
stream.appendChild(card);
document.body.appendChild(overlay);
return 'mounted';
})()`)
console.error('compact mount ->', compactMount)
await wait(400)
await shoot('ctx-deep-03-compact-config.png')
// -- SHOT 4: Subagent drill-down tabs -----------------------------------
// Make sure we're on the Chat tab and any drawers/rails are hidden so the
// subagent card owns the frame.
await evj(`(function(){
if (window.__dshTabs && window.__dshTabs.switchTo) window.__dshTabs.switchTo('chat');
// Force-show the chat pane and hide every other pane so a mount into
// #stream can't land off-screen.
const panes = document.querySelectorAll('.pane[data-pane]');
for (const p of panes) p.hidden = (p.getAttribute('data-pane') !== 'chat');
const rail = document.getElementById('context-rail'); if (rail) rail.hidden = true;
const drawer = document.querySelector('.devtools-drawer'); if (drawer) drawer.style.display = 'none';
return 'chat-clean';
})()`)
await wait(400)
const subaMount = await evj(`(function(){
// Build a full-page overlay that covers the whole viewport with a white
// background — this is a demo shot, so we want the subagent trace to be
// the whole story. This bypasses any pane routing quirks.
const prevOverlay = document.getElementById('ctx-deep-demo-overlay');
if (prevOverlay) prevOverlay.remove();
const prev = document.getElementById('ctx-deep-demo-subagent');
if (prev) prev.remove();
const prevCompact = document.getElementById('ctx-deep-demo-compact-card');
if (prevCompact) prevCompact.remove();
const overlay = document.createElement('div');
overlay.id = 'ctx-deep-demo-overlay';
overlay.style.cssText = 'position:fixed;left:0;top:0;right:0;bottom:0;background:var(--bg,#fff);z-index:9999;padding:48px 96px;overflow:auto;font-family:inherit;';
const heading = document.createElement('h1');
heading.style.cssText = 'font-size:24px;margin:0 0 16px 0;';
heading.textContent = 'Subagent trace — drill-down tabs';
overlay.appendChild(heading);
const sub = document.createElement('div');
sub.className = 'muted';
sub.style.cssText = 'margin-bottom:24px;color:var(--muted,#5b6b7d);';
sub.textContent = 'Tool defs / Inbound query surface at the foot of every subagent card.';
overlay.appendChild(sub);
const stream = overlay;
if (!window.__dshSubagentView || !window.__dshSubagentView.buildInlineSubagentTrace) {
return 'no-view';
}
const now = Date.now();
const childEvents = [
{ type: 'user/message', seq: 1, time: now, data: { content: [{type:'text', text:'Locate a file called ledger.md and summarise sections 24.'}], source: { kind: 'plugin', plugin: 'subagent-search' } } },
{ type: 'tool/call', seq: 2, time: now+10, data: { name: 'read_file', arguments: '{"path":"ledger.md"}' } },
{ type: 'tool/call', seq: 3, time: now+20, data: { name: 'search', arguments: '{"q":"section 2"}' } },
{ type: 'tool/call', seq: 4, time: now+30, data: { name: 'read_file', arguments: '{"path":"appendix.md"}' } },
{ type: 'turn/end', seq: 5, time: now+40, data: { turn: 0, reason: { kind: 'completed' } } },
];
const lastAssistantMessage = [{ type: 'text', text: '\`\`\`json\\n{"summary":"sections 2-4 cover the ledger schema","found":3}\\n\`\`\`' }];
const spec = {
parentSessionId: 'parent-sess-1',
childSessionId: 'child-sess-a',
status: 'done',
provider: 'stdio-echo',
stopReason: 'stop',
childEvents,
lastAssistantMessage,
};
const trace = window.__dshSubagentView.buildInlineSubagentTrace(document, spec, { collapsed: false });
trace.id = 'ctx-deep-demo-subagent';
trace.style.margin = '24px auto';
trace.style.maxWidth = '760px';
stream.appendChild(trace);
trace.open = true;
// Append the overlay LAST so it sits atop the rest of the page.
document.body.appendChild(overlay);
return { hasDrilldown: !!trace.querySelector('.subagent-drilldown') };
})()`)
console.error('subagent mount ->', JSON.stringify(subaMount))
await wait(400)
await shoot('ctx-deep-04-subagent.png')
ws.close()
}
main().catch((e) => { console.error(e); process.exit(1) })