feat(desktop): trace signal detection + tri-view badges
Adds a small signal-detection layer over trace payloads and surfaces
three kinds of semantic signals inline in the trace tri-view (Timeline
/ Graph / turn footer chips) so a researcher can spot repetitive
tool-call loops, redundant identical calls, and plan-completion beats
without hand-scanning the raw log:
- loop-detected — three or more repeats of the same tool.method
signature inside a window, badge in Timeline gutter
+ colored ring on the Graph node
- redundant-call — identical (tool, arg-hash) call twice without any
state change in between, badge + subdued styling
so it stays advisory not alarmist
- plan-* chips — plan/subplan step-complete beats surfaced above the
assistant body in the turn footer, wired through
the finishTurnContainer tail
Pure rendering; no wire-format changes needed on the runtime side —
signal detection is fixture-driven off the same payload the tri-view
already consumes. Companion RFC L-2 in docs/upstream-ledger.md asks
the runtime to emit these signals natively rather than deriving them
in the shell, so once L-2 lands the shell will consume upstream signals
and this detector becomes a fallback.
Files:
src/renderer/trace-signal-detect.js new (loop/redundant/plan)
src/renderer/trace-tri-view.js +24 (wire detector output)
src/renderer/trace-timeline.js +39 (badge in gutter)
src/renderer/trace-graph.js +30 (colored ring)
src/renderer/renderer.js +81 (finishTurnContainer chips)
src/renderer/index.html +1 (one script tag)
src/renderer/style.css +58 (badge/ring/chip rules)
docs/upstream-ledger.md +121 (append L-2 RFC)
scripts/qa-trace-signals-fixture.mjs new (headless SVG proof)
scripts/qa-trace-signals-shoot.mjs new (CDP live shoot)
docs/trace-signals-shoot/*.html new (3 fixture-driven shots)
test/trace-signal-detect.test.js new (detector unit tests)
test/trace-signal-overlay.test.js new (renderer overlay tests)
Test suite: 1668/1668 pass (17 new). Fixture-driven signals-01-timeline-
loop.html regenerated byte-identical from the merged HEAD.
This commit is contained in:
@@ -372,6 +372,36 @@ function renderGraph(doc, input, opts) {
|
||||
class: `trace-graph-node-body family-${node.family}`,
|
||||
})
|
||||
nodeG.appendChild(circle)
|
||||
|
||||
// Signal ring — a colored outer stroke when this node's seq shows up
|
||||
// in the passed signals map. Multiple signals share one ring but the
|
||||
// tooltip enumerates them. See trace-signal-detect.js + L-2 RFC.
|
||||
if (options.signals && typeof options.signals.get === 'function' && node.seq !== null) {
|
||||
const sigs = options.signals.get(node.seq)
|
||||
if (sigs && sigs.length) {
|
||||
// Highest-priority signal wins the ring class (error > loop > redundant > plan).
|
||||
const priority = ['tool-error', 'loop-detected', 'plan-restart', 'redundant-call', 'plan-update']
|
||||
let winner = sigs[0]
|
||||
let winnerRank = 999
|
||||
for (const s of sigs) {
|
||||
const r = priority.indexOf(s.signal)
|
||||
if (r >= 0 && r < winnerRank) { winner = s; winnerRank = r }
|
||||
}
|
||||
const SD = (typeof window !== 'undefined' && window.__dshTraceSignalDetect)
|
||||
|| (typeof require !== 'undefined' ? require('./trace-signal-detect.js') : null)
|
||||
const cls = SD ? SD.classFor(winner.signal) : 'sig-generic'
|
||||
const ring = svgEl(doc, 'circle', {
|
||||
cx: 0, cy: 0, r: NODE_R + 4,
|
||||
class: `trace-graph-signal-ring ${cls}`,
|
||||
fill: 'none',
|
||||
})
|
||||
const title = svgEl(doc, 'title', {})
|
||||
title.textContent = SD ? sigs.map(s => SD.tooltipFor(s)).join('\n') : sigs.map(s => s.signal).join(', ')
|
||||
ring.appendChild(title)
|
||||
nodeG.appendChild(ring)
|
||||
}
|
||||
}
|
||||
|
||||
const glyph = svgEl(doc, 'text', {
|
||||
x: 0, y: 4, class: 'trace-graph-node-glyph',
|
||||
'text-anchor': 'middle',
|
||||
|
||||
Reference in New Issue
Block a user