Files
deepseek-harness/examples/desktop/src/renderer/compact-config-model.js
ZiyaZhang fc558c2db8 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.
2026-07-19 01:22:28 -07:00

188 lines
6.8 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.
// Pure model for the compact-card "Config" tab (lane-ctx-deep, task #51 F2).
//
// The Config tab is an info-only entrance to the compaction policy: it names
// the current threshold, the strategy, how many times the daemon has fired
// compact this session, and a "distance to next compact" progress bar. It
// is *not* an edit surface — a live editor belongs on the Settings page,
// and the tab tooltip points there ("Adjust in Settings Compaction").
//
// Model shape is a plain object so tests can lock it without a DOM harness.
// The `buildCompactConfigView` function is the single entry point; it takes
// a session's cached events and (optionally) a policy override the shell
// pulls from the Settings profile, and returns:
//
// {
// thresholdTokens, // e.g. 96000 (server-reported) or fallback 96k
// thresholdSource, // 'server'|'assumed'
// strategyName, // e.g. 'summarize-shadowed' / 'unknown'
// model, // summary model, e.g. 'deepseek-chat' or null
// maxSummaryTokens, // policy cap on the summary output
// triggersFired, // total compact/summary events observed
// lastCompactSeq, // seq of the last compact/summary, or null
// currentTokens, // running tokens at end of stream
// tokensSinceLastCompact,// tokens accumulated after the last compact
// tokensUntilNext, // max(threshold tokensSinceLastCompact, 0)
// progressPct, // tokensSinceLastCompact / threshold × 100
// progressLevel, // 'nominal'|'warn'|'high'|'critical'
// }
//
// Threshold source: we prefer the wire (`session/list` entry's
// `context.compact.threshold` if the daemon ever ships one), else fall back
// to 75% of the model's context window ("industry default"), else a hard
// fallback of 96000. The `thresholdSource` field marks which path we took
// so the tab tooltip can be honest.
'use strict'
const DEFAULT_THRESHOLD_TOKENS = 96000
/**
* Return the compact threshold (tokens) plus its provenance.
* Priority order:
* 1. `override.thresholdTokens` (Settings profile / test override).
* 2. `budgetTokens * 0.75` when a wire-reported budget is available.
* 3. DEFAULT_THRESHOLD_TOKENS (96000).
* @param {object} [opts]
* @param {number} [opts.thresholdTokens] explicit override
* @param {number} [opts.budgetTokens] wire-reported model context window
* @returns {{ tokens: number, source: 'server'|'assumed' }}
*/
function resolveThreshold(opts) {
const explicit = opts && Number.isFinite(opts.thresholdTokens) && opts.thresholdTokens > 0
if (explicit) return { tokens: Number(opts.thresholdTokens), source: 'server' }
const budget = opts && Number.isFinite(opts.budgetTokens) && opts.budgetTokens > 0
? Number(opts.budgetTokens) : null
if (budget) return { tokens: Math.round(budget * 0.75), source: 'assumed' }
return { tokens: DEFAULT_THRESHOLD_TOKENS, source: 'assumed' }
}
/**
* Roughly count tokens the same way context-meter's approx mode does:
* bytes ÷ 4 over `event.data` JSON. Duplicated here (not imported) so the
* config model stays a leaf — importing context-meter would require the
* caller to pass a tracker to keep coherent, and every callsite already
* has cachedEvents in hand.
* @param {object} event
* @returns {number}
*/
function approxTokensFor(event) {
if (!event || typeof event !== 'object') return 0
// Precise-mode signal: honour the usage envelope if present.
if (event.type === 'assistant/message') {
const u = event.data && event.data.usage
if (u && typeof u === 'object') {
const inp = Number(u.inputTokens)
const out = Number(u.outputTokens)
const sum = (Number.isFinite(inp) ? inp : 0) + (Number.isFinite(out) ? out : 0)
if (sum > 0) return sum
}
}
const payload = event.data !== undefined ? event.data : event
try { return Math.round(JSON.stringify(payload).length / 4) } catch (_) { return 0 }
}
function levelForPct(pct) {
if (!Number.isFinite(pct)) return 'nominal'
if (pct >= 95) return 'critical'
if (pct >= 80) return 'high'
if (pct >= 50) return 'warn'
return 'nominal'
}
/**
* Build the full config-tab view model.
*
* @param {Array<object>} events
* @param {object} [opts]
* @param {number} [opts.thresholdTokens]
* @param {number} [opts.budgetTokens]
* @param {string} [opts.strategyName]
* @returns {{
* thresholdTokens:number,
* thresholdSource:'server'|'assumed',
* strategyName:string,
* model:string|null,
* maxSummaryTokens:number|null,
* triggersFired:number,
* lastCompactSeq:number|null,
* currentTokens:number,
* tokensSinceLastCompact:number,
* tokensUntilNext:number,
* progressPct:number,
* progressLevel:'nominal'|'warn'|'high'|'critical',
* }}
*/
function buildCompactConfigView(events, opts) {
const threshold = resolveThreshold(opts || {})
let triggers = 0
let lastSeq = null
let lastPolicy = null
let tokensTotal = 0
let tokensSinceLast = 0
if (Array.isArray(events)) {
for (const ev of events) {
if (!ev || typeof ev !== 'object') continue
const tk = approxTokensFor(ev)
tokensTotal += tk
tokensSinceLast += tk
if (ev.type === 'compact/summary') {
triggers++
if (Number.isFinite(ev.seq)) lastSeq = ev.seq
const d = ev.data || {}
lastPolicy = {
model: typeof d.model === 'string' ? d.model : null,
maxTokens: Number.isFinite(d.maxTokens) ? d.maxTokens : null,
}
// A compact resets the "since last" counter; the shadowed range
// just replaced the running budget so tokens after should count
// from zero.
tokensSinceLast = 0
}
}
}
const progressPct = threshold.tokens > 0
? Math.round((tokensSinceLast / threshold.tokens) * 1000) / 10
: 0
const tokensUntilNext = Math.max(0, threshold.tokens - tokensSinceLast)
const strategyName = (opts && typeof opts.strategyName === 'string' && opts.strategyName)
|| (lastPolicy ? 'summarize-shadowed' : 'summarize-shadowed (default)')
return {
thresholdTokens: threshold.tokens,
thresholdSource: threshold.source,
strategyName,
model: lastPolicy && lastPolicy.model,
maxSummaryTokens: lastPolicy && lastPolicy.maxTokens,
triggersFired: triggers,
lastCompactSeq: lastSeq,
currentTokens: tokensTotal,
tokensSinceLastCompact: tokensSinceLast,
tokensUntilNext,
progressPct: Math.max(0, Math.min(progressPct, 999)),
progressLevel: levelForPct(progressPct),
}
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
buildCompactConfigView,
resolveThreshold,
approxTokensFor,
levelForPct,
DEFAULT_THRESHOLD_TOKENS,
}
}
if (typeof window !== 'undefined') {
window.__dshCompactConfigModel = {
buildCompactConfigView,
resolveThreshold,
approxTokensFor,
levelForPct,
DEFAULT_THRESHOLD_TOKENS,
}
}