Files
deepseek-harness/examples/desktop/scripts/qa-cdp-shoot-trace-parity-batch-b.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

432 lines
18 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-trace-parity-batch-b.mjs — 2026-07-17 batch B selfies.
//
// Two shots proving the HUMAN-card convergence + recursive JSON tree:
// batch-b-04 role convergence: user + assistant + tool bubbles rendered
// with inline dot + Titlecase word ("User" / "Assistant" /
// "Tool"), no uppercase HUMAN/USER hero heading.
// batch-b-05 recursive JSON tree: an assistant tool_call arguments
// block with a deep nested object rendered as a
// `<details>`-based collapsible tree (every level has an
// arrow, scalars carry a `·` dot).
//
// Usage:
// node scripts/qa-cdp-shoot-trace-parity-batch-b.mjs <port> <outdir>
// The Electron demo must be running with --remote-debugging-port=<port>
// and DSH_QA=1.
import { writeFileSync, mkdirSync } from 'node:fs'
import { resolve } from 'node:path'
const [,, portArg, outdir] = process.argv
const port = portArg || '9240'
if (!outdir) {
console.error('usage: node scripts/qa-cdp-shoot-trace-parity-batch-b.mjs <port> <outdir>')
process.exit(1)
}
mkdirSync(outdir, { recursive: true })
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()
ws.onmessage = (ev) => {
let msg; try { msg = JSON.parse(ev.data) } catch { 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))
return { call, evjs, sleep, close: () => ws.close() }
}
async function shot(c, name) {
const png = await c.call('Page.captureScreenshot', {
format: 'png', clip: { x: 0, y: 0, width: 1440, height: 900, scale: 1 },
})
const path = resolve(outdir, `${name}.png`)
writeFileSync(path, Buffer.from(png.data, 'base64'))
console.log(path)
}
async function main() {
const c = await cdp()
await c.call('Page.enable')
await c.evjs(`(async()=>{try{return window.dshQa && await window.dshQa.revealWindow()}catch(e){return {err:String(e)}}})()`)
await c.call('Emulation.setDeviceMetricsOverride', { width: 1440, height: 900, deviceScaleFactor: 1, mobile: false })
await c.evjs(`window.__dshTabs && window.__dshTabs.switchTo && window.__dshTabs.switchTo('chat')`)
await c.sleep(300)
await c.evjs(`(function(){
document.body.classList.add('onboarded')
const ob = document.querySelector('#onboarding, .onboarding, [data-onboarding]')
if (ob) ob.remove()
return 1
})()`)
// ─── SHOT 4: role convergence — inline titlecase, no HUMAN caps card ─
await c.evjs(`(async () => {
const s = document.getElementById('stream')
if (!s) return 'NO_STREAM'
s.innerHTML = ''
for (const stray of document.querySelectorAll('[data-parity-shot]')) stray.remove()
const host = document.createElement('div')
host.setAttribute('data-parity-shot', 'b04')
host.style.padding = '20px 24px'
host.style.maxWidth = '900px'
host.style.margin = '20px auto'
host.style.border = '1px solid rgba(0,0,0,0.09)'
host.style.borderRadius = '8px'
host.style.background = 'var(--surface, #fff)'
const label = document.createElement('div')
label.style.font = '600 13px system-ui, sans-serif'
label.style.marginBottom = '10px'
label.textContent = 'batch-b 04 — Role convergence: inline · + Titlecase (never HUMAN/USER caps hero)'
host.appendChild(label)
// Fake three msg bubbles matching real appendMessage output shape
// (role-glyph + role-label spans, .role container).
for (const [role, text] of [
['user', 'What Cognitive-behavioral therapy is?'],
['assistant', 'CBT is a talk-therapy approach that identifies and reshapes unhelpful thought patterns.'],
['tool', 'read({"path": "notes.txt"}) → 1024 bytes'],
]) {
const b = document.createElement('div')
b.className = 'msg ' + role
const r = document.createElement('div')
r.className = 'role'
r.dataset.role = role
const g = document.createElement('span'); g.className = 'role-glyph'; g.textContent = '·'
const l = document.createElement('span'); l.className = 'role-label'
l.textContent = role.charAt(0).toUpperCase() + role.slice(1)
r.appendChild(g); r.appendChild(l)
const body = document.createElement('div')
body.textContent = text
b.appendChild(r); b.appendChild(body)
host.appendChild(b)
}
// Callout naming the old shape being retired.
const callout = document.createElement('div')
callout.style.marginTop = '20px'
callout.style.padding = '12px 14px'
callout.style.background = 'rgba(220,53,69,0.06)'
callout.style.border = '1px dashed rgba(220,53,69,0.3)'
callout.style.borderRadius = '6px'
callout.style.font = '12px system-ui, sans-serif'
callout.style.color = '#8b1e2c'
callout.textContent = 'Retired: block-level "HUMAN"/"USER" uppercase caps card (无信息增量) — role now inline dot + Titlecase word.'
host.appendChild(callout)
s.appendChild(host)
return 1
})()`)
await c.sleep(600)
await c.evjs(`(function(){
for (const sel of ['#context-rail-drawer', '#context-rail', '.context-rail-drawer', '.context-rail', '.devtools-drawer', '#devtools-panel', '.debug-panel']) {
const n = document.querySelector(sel)
if (n) { n.hidden = true; n.style.display = 'none' }
}
return 1
})()`)
await c.sleep(200)
await shot(c, 'trace-parity-04-role-titlecase')
// ─── SHOT 5: Recursive JSON tree — deep nested tool_call arguments ─
await c.evjs(`(async () => {
const s = document.getElementById('stream')
if (!s) return 'NO_STREAM'
s.innerHTML = ''
for (const stray of document.querySelectorAll('[data-parity-shot]')) stray.remove()
const D = window.__dshTraceDetailPane
if (!D || typeof D.buildJsonTree !== 'function') return 'NO_TREE_MOD'
const host = document.createElement('div')
host.setAttribute('data-parity-shot', 'b05')
host.style.padding = '20px 24px'
host.style.maxWidth = '860px'
host.style.margin = '20px auto'
host.style.border = '1px solid rgba(0,0,0,0.09)'
host.style.borderRadius = '8px'
host.style.background = 'var(--surface, #fff)'
const label = document.createElement('div')
label.style.font = '600 13px system-ui, sans-serif'
label.style.marginBottom = '10px'
label.textContent = 'batch-b 05 — Recursive JSON tree: tool_call arguments fold at every depth (density = folding, not dropping)'
host.appendChild(label)
// Card mimicking the LangSmith fields-card wrapper.
const card = document.createElement('div')
card.style.border = '1px solid rgba(0,0,0,0.09)'
card.style.borderRadius = '6px'
card.style.padding = '12px 14px'
card.style.background = 'var(--bg-elev, #fafafa)'
const head = document.createElement('div')
head.style.display = 'flex'; head.style.alignItems = 'baseline'; head.style.gap = '8px'
head.style.marginBottom = '10px'
head.style.font = '600 12px system-ui, sans-serif'
const glyph = document.createElement('span')
glyph.textContent = '{ }'; glyph.style.fontFamily = 'var(--mono, monospace)'
glyph.style.color = 'rgba(0,0,0,0.4)'
const title = document.createElement('span'); title.textContent = 'arguments'
head.appendChild(glyph); head.appendChild(title)
card.appendChild(head)
// A deep fixture mirroring a realistic OpenAI-style tool_call JSON.
const fixture = {
choices: [
{
finish_reason: 'stop',
index: 0,
message: {
role: 'assistant',
content: null,
tool_calls: [
{
id: 'tool_call_1',
type: 'function',
function: {
name: 'search_web',
arguments: { query: 'CBT therapy overview', top_k: 5, include_snippets: true },
},
},
],
},
logprobs: null,
},
],
id: 'chatcmpl-abc123',
model: 'deepseek-v4',
object: 'chat.completion',
system_fingerprint: 'fp_9a71',
usage: { prompt_tokens: 128, completion_tokens: 42, total_tokens: 170 },
}
const tree = D.buildJsonTree(document, fixture, { openDepth: 2 })
card.appendChild(tree)
host.appendChild(card)
s.appendChild(host)
return 1
})()`)
await c.sleep(600)
await c.evjs(`(function(){
for (const sel of ['#context-rail-drawer', '#context-rail', '.context-rail-drawer', '.context-rail', '.devtools-drawer', '#devtools-panel', '.debug-panel']) {
const n = document.querySelector(sel)
if (n) { n.hidden = true; n.style.display = 'none' }
}
return 1
})()`)
await c.sleep(200)
await shot(c, 'trace-parity-05-fields-tree')
// ─── SHOT 6: ≥3-level nested Fields subtree — Output-row raw payload ─
//
// task #39: the row-level Fields subtree (buildRawFieldsSubtree, wired
// into every trace Output row) exposes the full assistant/message wire
// payload; the shot must show at least 3 levels of nested objects
// visibly expanded so the reader sees "each nested level has its own
// /▸ arrow" (density-layering-spec §7 positive-reference lock).
//
// Fixture: a Claude-style assistant/message with tool_use content
// blocks + usage — the recursion reaches depth 4 (data → content →
// [0] → input → {key/value}). buildRawFieldsSubtree opens the top,
// openDepth=2; we then programmatically flip deeper branches so the
// shot captures a fully-unfolded state.
await c.evjs(`(async () => {
const s = document.getElementById('stream')
if (!s) return 'NO_STREAM'
s.innerHTML = ''
for (const stray of document.querySelectorAll('[data-parity-shot]')) stray.remove()
const D = window.__dshTraceDetailPane
if (!D || typeof D.buildRawFieldsSubtree !== 'function') return 'NO_ROW_FIELDS_MOD'
const host = document.createElement('div')
host.setAttribute('data-parity-shot', 'b06')
host.style.padding = '20px 24px'
host.style.maxWidth = '900px'
host.style.margin = '20px auto'
host.style.border = '1px solid rgba(0,0,0,0.09)'
host.style.borderRadius = '8px'
host.style.background = 'var(--surface, #fff)'
const label = document.createElement('div')
label.style.font = '600 13px system-ui, sans-serif'
label.style.marginBottom = '10px'
label.textContent = 'batch-b 06 — Row-level Fields subtree, ≥3 depths visibly expanded (per-depth /▸ arrows)'
host.appendChild(label)
// Simulate a real trace-detail Output row so the subtree renders in
// its natural chrome (role + Fields + raw badge line).
const row = document.createElement('div')
row.className = 'trace-detail-output-row'
row.style.padding = '8px 12px'
row.style.border = '1px solid rgba(0,0,0,0.09)'
row.style.borderRadius = '6px'
row.style.background = 'var(--bg-elev, #fafafa)'
row.style.display = 'flex'
row.style.flexDirection = 'column'
row.style.gap = '6px'
const roleLine = document.createElement('div')
roleLine.style.display = 'flex'
roleLine.style.alignItems = 'baseline'
roleLine.style.gap = '12px'
const roleEl = document.createElement('span')
roleEl.className = 'trace-detail-role'
roleEl.dataset.role = 'assistant'
const g = document.createElement('span'); g.className = 'trace-detail-role-glyph mono'; g.textContent = '·'
const w = document.createElement('span'); w.className = 'trace-detail-role-label'; w.textContent = 'Assistant'
roleEl.appendChild(g); roleEl.appendChild(w)
const body = document.createElement('div')
body.className = 'trace-detail-message-body'
body.textContent = "I'll search the web for CBT therapy overviews and cite the top results."
roleLine.appendChild(roleEl); roleLine.appendChild(body)
row.appendChild(roleLine)
// Deep raw event fixture — 5-level recursion once you enter tool_use.
const rawEvent = {
seq: 42,
type: 'assistant/message',
time: 1710000000000,
data: {
role: 'assistant',
content: [
{ type: 'text', text: "I'll search the web for CBT therapy overviews and cite the top results." },
{
type: 'tool_use',
id: 'toolu_01ABC123',
name: 'search_web',
input: {
query: 'cognitive behavioral therapy overview',
filters: { language: 'en', region: 'US', top_k: 5, include_snippets: true },
},
},
],
finish_reason: 'tool_calls',
stop_sequence: null,
usage: {
prompt_tokens: 342,
completion_tokens: 78,
total_tokens: 420,
cache_read_input_tokens: 128,
cache_creation_input_tokens: 0,
},
provider_diagnostics: {
request_id: 'req_9f2a',
upstream: { latency_ms: 812, tokens_per_second: 96.3 },
},
},
}
const subtree = D.buildRawFieldsSubtree(document, rawEvent)
if (subtree) {
subtree.open = true
// Walk down + open every foldable branch so the ≥3-level state is
// captured; without this the shot would only show the top level.
const walk = subtree.querySelectorAll('.trace-detail-json-branch')
walk.forEach((d) => { d.open = true })
row.appendChild(subtree)
}
host.appendChild(row)
s.appendChild(host)
return 1
})()`)
await c.sleep(600)
await c.evjs(`(function(){
for (const sel of ['#context-rail-drawer', '#context-rail', '.context-rail-drawer', '.context-rail', '.devtools-drawer', '#devtools-panel', '.debug-panel']) {
const n = document.querySelector(sel)
if (n) { n.hidden = true; n.style.display = 'none' }
}
return 1
})()`)
await c.sleep(200)
await shot(c, 'trace-parity-06-row-fields-deep')
// ─── SHOT 7: trace-card right-side subtree-fold glyph (spec §7) ────
//
// task #38 selfie: the right-side glyph on trace-card summaries.
// Shoot two side-by-side cards — one open, one closed — so the reader
// sees the state transition + rotation cue.
await c.evjs(`(async () => {
const s = document.getElementById('stream')
if (!s) return 'NO_STREAM'
s.innerHTML = ''
for (const stray of document.querySelectorAll('[data-parity-shot]')) stray.remove()
const host = document.createElement('div')
host.setAttribute('data-parity-shot', 'b07')
host.style.padding = '20px 24px'
host.style.maxWidth = '900px'
host.style.margin = '20px auto'
host.style.border = '1px solid rgba(0,0,0,0.09)'
host.style.borderRadius = '8px'
host.style.background = 'var(--surface, #fff)'
const label = document.createElement('div')
label.style.font = '600 13px system-ui, sans-serif'
label.style.marginBottom = '10px'
label.textContent = 'batch-b 07 — Trace-card right-side subtree-fold glyph (open / closed states)'
host.appendChild(label)
// Build two mock trace-cards matching the real renderTraceCard shape.
function mockCard(open, stepPart, summaryText) {
const el = document.createElement('details')
el.className = 'trace-card'
el.open = open
const sum = document.createElement('summary')
const l = document.createElement('span')
l.className = 'trace-label'
l.textContent = summaryText ? '▸ ' + stepPart + ' — "' + summaryText + '"' : '▸ ' + stepPart
const badge = document.createElement('span')
badge.className = 'trace-usage-badge'
badge.textContent = '↑342 ↓78 ⚡128'
const dur = document.createElement('span')
dur.className = 'trace-duration'
dur.textContent = '812ms'
const foldGlyph = document.createElement('span')
foldGlyph.className = 'trace-card-fold-glyph mono'
foldGlyph.setAttribute('aria-hidden', 'true')
foldGlyph.textContent = ''
sum.appendChild(badge); sum.appendChild(l); sum.appendChild(dur); sum.appendChild(foldGlyph)
el.appendChild(sum)
const body = document.createElement('div')
body.className = 'trace-body'
body.textContent = '(step contents — inputs / outputs / events)'
body.style.padding = '8px 10px'
body.style.color = 'rgba(0,0,0,0.55)'
body.style.fontFamily = 'var(--mono, monospace)'
body.style.fontSize = '11px'
el.appendChild(body)
el.style.margin = '8px 0'
return el
}
host.appendChild(mockCard(true, 'step 0.1', 'search_web(query="CBT therapy overview")'))
host.appendChild(mockCard(false, 'step 0.2', 'assistant reply'))
const callout = document.createElement('div')
callout.style.marginTop = '14px'
callout.style.padding = '10px 12px'
callout.style.background = 'rgba(24,144,255,0.06)'
callout.style.border = '1px dashed rgba(24,144,255,0.35)'
callout.style.borderRadius = '6px'
callout.style.font = '12px system-ui, sans-serif'
callout.style.color = '#0e5aa7'
callout.textContent = 'Right-side rotates on [open] (0deg open, -90deg closed) — density-layering-spec §7 lock.'
host.appendChild(callout)
s.appendChild(host)
return 1
})()`)
await c.sleep(500)
await c.evjs(`(function(){
for (const sel of ['#context-rail-drawer', '#context-rail', '.context-rail-drawer', '.context-rail', '.devtools-drawer', '#devtools-panel', '.debug-panel']) {
const n = document.querySelector(sel)
if (n) { n.hidden = true; n.style.display = 'none' }
}
return 1
})()`)
await c.sleep(200)
await shot(c, 'trace-parity-07-card-fold-glyph')
c.close()
}
main().catch((e) => { console.error(e); process.exit(2) })