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:
199
examples/desktop/test/lane-ctx-deep-dom.test.js
Normal file
199
examples/desktop/test/lane-ctx-deep-dom.test.js
Normal file
@@ -0,0 +1,199 @@
|
||||
// DOM-shape tests for the four lane-ctx-deep enhancements (task #51).
|
||||
//
|
||||
// These assert on the rendered element trees without booting jsdom or the
|
||||
// Electron shell. Each test mocks the minimal DOM surface each builder
|
||||
// touches (createElement + appendChild + attribute + dataset), running
|
||||
// through the same code paths the production shell exercises.
|
||||
//
|
||||
// The four features covered:
|
||||
// F1 — window occupancy bar: renders 5 stacked segments.
|
||||
// F2 — compact-card 4th tab "Config": strip includes a `Config` button
|
||||
// AND the config body is populated with threshold+progress rows.
|
||||
// F3 — intervention marker strip: emits one marker per intervention.
|
||||
// F4 — subagent drill-down tabs: emits `Tool defs (N)` + `Inbound query`
|
||||
// strip and populated bodies.
|
||||
|
||||
'use strict'
|
||||
|
||||
const test = require('node:test')
|
||||
const assert = require('node:assert/strict')
|
||||
|
||||
// --- Handroll DOM stub ---------------------------------------------------
|
||||
//
|
||||
// The builders under test never touch layout, only tree structure + a few
|
||||
// attrs/dataset entries + textContent + eventListeners. This stub covers
|
||||
// exactly that.
|
||||
|
||||
function makeEl(tag) {
|
||||
return {
|
||||
tagName: String(tag).toUpperCase(),
|
||||
className: '',
|
||||
textContent: '',
|
||||
hidden: false,
|
||||
tabIndex: 0,
|
||||
style: (function () {
|
||||
const map = {}
|
||||
return {
|
||||
setProperty(k, v) { map[k] = v },
|
||||
getPropertyValue(k) { return map[k] },
|
||||
}
|
||||
})(),
|
||||
dataset: {},
|
||||
_attrs: {},
|
||||
_listeners: {},
|
||||
_children: [],
|
||||
ownerDocument: null, // set below
|
||||
appendChild(c) { this._children.push(c); return c },
|
||||
append(...kids) { for (const k of kids) this._children.push(k) },
|
||||
setAttribute(k, v) { this._attrs[k] = String(v) },
|
||||
getAttribute(k) { return this._attrs[k] },
|
||||
addEventListener(type, fn) {
|
||||
(this._listeners[type] = this._listeners[type] || []).push(fn)
|
||||
},
|
||||
querySelector() { return null },
|
||||
remove() { /* no-op */ },
|
||||
}
|
||||
}
|
||||
|
||||
function makeDoc() {
|
||||
const doc = {
|
||||
createElement(tag) {
|
||||
const el = makeEl(tag)
|
||||
el.ownerDocument = doc
|
||||
return el
|
||||
},
|
||||
body: null,
|
||||
getElementById() { return null },
|
||||
}
|
||||
doc.body = doc.createElement('body')
|
||||
return doc
|
||||
}
|
||||
|
||||
// Recursively find children matching a class prefix.
|
||||
function findAllByClass(root, cls, out) {
|
||||
out = out || []
|
||||
if (!root) return out
|
||||
if (root.className && String(root.className).split(/\s+/).includes(cls)) out.push(root)
|
||||
for (const c of root._children || []) findAllByClass(c, cls, out)
|
||||
return out
|
||||
}
|
||||
function findFirstByClass(root, cls) {
|
||||
const all = findAllByClass(root, cls, [])
|
||||
return all[0] || null
|
||||
}
|
||||
|
||||
// --- F2: compact-card 4-tab shell ----------------------------------------
|
||||
|
||||
test('F2: mountTabs with fillConfig adds a Config tab and populates its body', () => {
|
||||
const { mountTabs } = require('../src/renderer/compact-card.js')
|
||||
const doc = makeDoc()
|
||||
const parent = doc.createElement('div')
|
||||
|
||||
let filledConfig = null
|
||||
mountTabs(parent, {
|
||||
document: doc,
|
||||
initial: 'post',
|
||||
fillPre: (body) => { body.textContent = 'pre' },
|
||||
fillPost: (body) => { body.textContent = 'post' },
|
||||
fillMeta: (body) => { body.textContent = 'meta' },
|
||||
fillConfig: (body) => { body.textContent = 'CONFIG_HERE'; filledConfig = body },
|
||||
})
|
||||
|
||||
// Tab buttons: expect four in the strip.
|
||||
const strip = findFirstByClass(parent, 'compact-card-tabstrip')
|
||||
assert.ok(strip, 'tabstrip mounted')
|
||||
const tabButtons = strip._children.filter((c) => c.tagName === 'BUTTON')
|
||||
assert.equal(tabButtons.length, 4, 'expect 4 tabs when fillConfig is provided')
|
||||
const labels = tabButtons.map((b) => b.textContent)
|
||||
assert.deepEqual(labels, ['Diff', 'Summary', 'Policy & accounting', 'Config'])
|
||||
|
||||
// Config body populated.
|
||||
assert.equal(filledConfig.textContent, 'CONFIG_HERE')
|
||||
})
|
||||
|
||||
test('F2: mountTabs without fillConfig stays a 3-tab shell (back-compat)', () => {
|
||||
const { mountTabs } = require('../src/renderer/compact-card.js')
|
||||
const doc = makeDoc()
|
||||
const parent = doc.createElement('div')
|
||||
const out = mountTabs(parent, {
|
||||
document: doc,
|
||||
fillPre() {}, fillPost() {}, fillMeta() {},
|
||||
})
|
||||
const strip = findFirstByClass(parent, 'compact-card-tabstrip')
|
||||
const tabButtons = strip._children.filter((c) => c.tagName === 'BUTTON')
|
||||
assert.equal(tabButtons.length, 3)
|
||||
assert.equal(out.configBody, null)
|
||||
})
|
||||
|
||||
// --- F4: subagent drill-down tabs ---------------------------------------
|
||||
|
||||
test('F4: appendSubagentDrilldownTabs emits both panels with correct labels', () => {
|
||||
const { appendSubagentDrilldownTabs } = require('../src/renderer/subagent-view.js')
|
||||
const doc = makeDoc()
|
||||
const parent = doc.createElement('div')
|
||||
const spec = {
|
||||
childEvents: [
|
||||
{ type: 'user/message', seq: 1, data: { content: [{ type: 'text', text: 'seed' }], source: { kind: 'plugin', plugin: 'subagent-search' } } },
|
||||
{ type: 'tool/call', seq: 2, data: { name: 'search', arguments: '{"q":"x"}' } },
|
||||
{ type: 'tool/call', seq: 3, data: { name: 'read_file', arguments: '{"path":"a"}' } },
|
||||
],
|
||||
}
|
||||
appendSubagentDrilldownTabs(doc, parent, spec)
|
||||
// Two tab buttons expected.
|
||||
const strip = findFirstByClass(parent, 'subagent-drilldown-tabstrip')
|
||||
assert.ok(strip, 'drilldown tabstrip mounted')
|
||||
const buttons = strip._children.filter((c) => c.tagName === 'BUTTON')
|
||||
assert.equal(buttons.length, 2)
|
||||
const btnLabels = buttons.map((b) => b.textContent)
|
||||
assert.match(btnLabels[0], /Tool defs \(2\)/)
|
||||
assert.equal(btnLabels[1], 'Inbound query')
|
||||
// Tool list should have two entries.
|
||||
const toolRows = findAllByClass(parent, 'subagent-drilldown-toolrow', [])
|
||||
assert.equal(toolRows.length, 2)
|
||||
// Inbound query blockquote should carry the seed text.
|
||||
const query = findFirstByClass(parent, 'subagent-drilldown-query')
|
||||
assert.ok(query, 'inbound query rendered')
|
||||
assert.match(query.textContent, /seed/)
|
||||
})
|
||||
|
||||
test('F4: appendSubagentDrilldownTabs skips entirely on empty spec', () => {
|
||||
const { appendSubagentDrilldownTabs } = require('../src/renderer/subagent-view.js')
|
||||
const doc = makeDoc()
|
||||
const parent = doc.createElement('div')
|
||||
appendSubagentDrilldownTabs(doc, parent, {})
|
||||
assert.equal(parent._children.length, 0, 'no drilldown wrapper for empty spec')
|
||||
})
|
||||
|
||||
// --- F1 + F3 model-shape sanity ----------------------------------------
|
||||
// (Full DOM wire-up of context-page.js is exercised by the four real-machine
|
||||
// screenshots at task-end. Here we lock the shapes the DOM depends on.)
|
||||
|
||||
test('F1 model: computeWindowBreakdown returns 5 slices in FAMILY_ORDER', () => {
|
||||
const M = require('../src/renderer/context-window-breakdown.js')
|
||||
const events = [
|
||||
{ type: 'context/message', seq: 1, data: { content: [{ type: 'text', text: 'sys' }], source: { kind: 'system' } } },
|
||||
{ type: 'assistant/message', seq: 2, data: { content: [{ type: 'text', text: 'ok' }], usage: { inputTokens: 100, outputTokens: 40 } } },
|
||||
{ type: 'assistant/reasoning', seq: 3, data: { content: [{ type: 'text', text: 'thinking' }] } },
|
||||
]
|
||||
const view = M.computeWindowBreakdown(events)
|
||||
assert.equal(view.slices.length, 5)
|
||||
const totalPct = view.slices.reduce((s, sl) => s + sl.pct, 0)
|
||||
assert.ok(totalPct <= 100.5, `sum ${totalPct} <= 100`)
|
||||
})
|
||||
|
||||
test('F3 model: collectInterventions preserves seq order + kind counts', () => {
|
||||
const M = require('../src/renderer/intervention-timeline.js')
|
||||
const events = [
|
||||
{ type: 'steering/message', seq: 5, data: { content: [{ type: 'text', text: 'a' }] } },
|
||||
{ type: 'session/forked', seq: 10, data: { parentSeq: 8 } },
|
||||
{ type: 'user/message', seq: 15, data: { content: [{ type: 'text', text: 'r' }], editRerun: { origSeq: 12 } } },
|
||||
]
|
||||
const markers = M.collectInterventions(events)
|
||||
assert.equal(markers.length, 3)
|
||||
assert.deepEqual(markers.map((m) => m.kind), ['steer', 'fork', 'edit-rerun'])
|
||||
const roll = M.summariseInterventions(markers)
|
||||
const rollMap = new Map(roll.map((r) => [r.kind, r.count]))
|
||||
assert.equal(rollMap.get('steer'), 1)
|
||||
assert.equal(rollMap.get('fork'), 1)
|
||||
assert.equal(rollMap.get('edit-rerun'), 1)
|
||||
})
|
||||
Reference in New Issue
Block a user