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,112 @@
'use strict'
const test = require('node:test')
const assert = require('node:assert/strict')
const M = require('../src/renderer/compact-config-model.js')
let _seq = 0
function nextSeq() { _seq += 1; return _seq }
function reset() { _seq = 0 }
function userMsg(text = 'hi') {
return { type: 'user/message', seq: nextSeq(), data: { content: [{ type: 'text', text }] } }
}
function assistantMsg(text = 'ok', usage = null) {
const ev = { type: 'assistant/message', seq: nextSeq(), data: { content: [{ type: 'text', text }] } }
if (usage) ev.data.usage = usage
return ev
}
function compact(model = 'deepseek-chat', maxTokens = 512) {
return {
type: 'compact/summary',
seq: nextSeq(),
data: { summary: [{ type: 'text', text: 's' }], model, maxTokens, shadowedTokenCount: 8000 },
}
}
test('resolveThreshold: explicit override → server source', () => {
const r = M.resolveThreshold({ thresholdTokens: 50000 })
assert.equal(r.tokens, 50000)
assert.equal(r.source, 'server')
})
test('resolveThreshold: budget → 0.75 × budget, assumed source', () => {
const r = M.resolveThreshold({ budgetTokens: 128000 })
assert.equal(r.tokens, 96000)
assert.equal(r.source, 'assumed')
})
test('resolveThreshold: no info → default 96000', () => {
const r = M.resolveThreshold({})
assert.equal(r.tokens, M.DEFAULT_THRESHOLD_TOKENS)
assert.equal(r.source, 'assumed')
})
test('buildCompactConfigView: counts triggersFired from compact events', () => {
reset()
const events = [userMsg(), assistantMsg(), compact(), userMsg(), assistantMsg(), compact()]
const view = M.buildCompactConfigView(events)
assert.equal(view.triggersFired, 2)
})
test('buildCompactConfigView: lastCompactSeq points at final compact event', () => {
reset()
const events = [userMsg(), compact(), userMsg(), assistantMsg()]
const cLast = compact()
events.push(cLast)
const view = M.buildCompactConfigView(events)
assert.equal(view.lastCompactSeq, cLast.seq)
})
test('buildCompactConfigView: tokensSinceLastCompact resets on compact', () => {
reset()
const events = [userMsg('x'.repeat(4000)), compact(), userMsg('y'.repeat(400))]
const view = M.buildCompactConfigView(events)
assert.ok(view.tokensSinceLastCompact < 500, 'reset means we count only tokens after the compact')
assert.ok(view.tokensSinceLastCompact > 0, 'post-compact user msg still counted')
})
test('buildCompactConfigView: progressPct + level scale with threshold', () => {
reset()
const bigMsg = { type: 'user/message', seq: nextSeq(), data: { content: [{ type: 'text', text: 'x'.repeat(400000) }] } }
const view = M.buildCompactConfigView([bigMsg], { thresholdTokens: 96000 })
assert.ok(view.progressPct >= 95, `expected critical level, got ${view.progressPct}%`)
assert.equal(view.progressLevel, 'critical')
})
test('buildCompactConfigView: tokensUntilNext floors at 0', () => {
reset()
const bigMsg = { type: 'user/message', seq: nextSeq(), data: { content: [{ type: 'text', text: 'x'.repeat(500000) }] } }
const view = M.buildCompactConfigView([bigMsg], { thresholdTokens: 96000 })
assert.equal(view.tokensUntilNext, 0)
})
test('buildCompactConfigView: empty events → zeroed view', () => {
const view = M.buildCompactConfigView([])
assert.equal(view.triggersFired, 0)
assert.equal(view.currentTokens, 0)
assert.equal(view.lastCompactSeq, null)
assert.equal(view.progressLevel, 'nominal')
})
test('buildCompactConfigView: strategy override wins over inferred default', () => {
const view = M.buildCompactConfigView([], { strategyName: 'sliding-window' })
assert.equal(view.strategyName, 'sliding-window')
})
test('buildCompactConfigView: last policy carries model + maxSummaryTokens', () => {
reset()
const events = [compact('deepseek-chat-v3', 1024)]
const view = M.buildCompactConfigView(events)
assert.equal(view.model, 'deepseek-chat-v3')
assert.equal(view.maxSummaryTokens, 1024)
})
test('levelForPct thresholds', () => {
assert.equal(M.levelForPct(0), 'nominal')
assert.equal(M.levelForPct(49), 'nominal')
assert.equal(M.levelForPct(50), 'warn')
assert.equal(M.levelForPct(80), 'high')
assert.equal(M.levelForPct(95), 'critical')
})