mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
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.
389 lines
21 KiB
JavaScript
389 lines
21 KiB
JavaScript
// 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 1–8; 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 1–8', '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 2–4.'}], 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) })
|