fix(desktop): usability wiring — rubric fixture ids + cell-jump listener + artifact seed + empty panel

+ docs/integration-final-check.md — 5-item integration gate PASS
This commit is contained in:
ZiyaZhang
2026-07-19 06:06:47 -07:00
parent 52a2493495
commit 084cbd1208
16 changed files with 844 additions and 70 deletions

View File

@@ -0,0 +1,93 @@
// Auto-inlined artifact-board fixture. Renderer runs at file:// so we
// inline docs/artifact-board-fixture.json here instead of relying on fetch().
// To refresh: node scripts/regen-artifact-board-seed.js
'use strict'
;(function () {
if (typeof window === 'undefined') return
window.__dshArtifactBoardSeed = {
"_note": "Fixture for lane-artifact-v2 (Artifacts evolution chain + Board view). Each entry is one artifact:event payload the renderer would receive. Multi-version entries share an artifactId; blob content is included so the evolution diff pane can render real per-hop line-diffs. The real ArtifactServer does not include content in its broadcast — that's the constraint the strip's fallback 'content not preserved' note is calibrated for.",
"artifacts": [
{
"artifactId": "session.md",
"kind": "md",
"version": 1,
"seenAt": 1747000000000,
"path": "/w/.artifacts/session.md",
"blob": "# Session log\n\nTurn 1: user asked to build a landing page.\n"
},
{
"artifactId": "session.md",
"kind": "md",
"version": 2,
"seenAt": 1747000060000,
"path": "/w/.artifacts/session.md",
"blob": "# Session log\n\nTurn 1: user asked to build a landing page.\nTurn 2: agent scaffolded landing.html and hero.svg.\n"
},
{
"artifactId": "session.md",
"kind": "md",
"version": 3,
"seenAt": 1747000180000,
"path": "/w/.artifacts/session.md",
"blob": "# Session log\n\nTurn 1: user asked to build a landing page.\nTurn 2: agent scaffolded landing.html and hero.svg.\nTurn 3: agent added responsive styles and OG tags.\nTurn 4: agent added README with build steps.\n"
},
{
"artifactId": "landing.html",
"kind": "html",
"version": 1,
"seenAt": 1747000030000,
"path": "/w/.artifacts/landing.html",
"blob": "<!doctype html>\n<html>\n<head><title>Landing</title></head>\n<body>\n <h1>Hello</h1>\n</body>\n</html>\n"
},
{
"artifactId": "landing.html",
"kind": "html",
"version": 2,
"seenAt": 1747000090000,
"path": "/w/.artifacts/landing.html",
"blob": "<!doctype html>\n<html>\n<head>\n <title>Landing</title>\n <meta name=\"description\" content=\"A demo landing page\">\n</head>\n<body>\n <h1>Hello</h1>\n <p>Welcome to our product.</p>\n</body>\n</html>\n"
},
{
"artifactId": "hero.svg",
"kind": "svg",
"version": 1,
"seenAt": 1747000045000,
"path": "/w/.artifacts/hero.svg",
"blob": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 200 100\">\n <rect width=\"200\" height=\"100\" fill=\"#eef\"/>\n</svg>\n"
},
{
"artifactId": "config.json",
"kind": "json",
"version": 1,
"seenAt": 1747000120000,
"path": "/w/.artifacts/config.json",
"blob": "{\n \"name\": \"landing\",\n \"version\": \"0.1.0\"\n}\n"
},
{
"artifactId": "config.json",
"kind": "json",
"version": 2,
"seenAt": 1747000200000,
"path": "/w/.artifacts/config.json",
"blob": "{\n \"name\": \"landing\",\n \"version\": \"0.2.0\",\n \"port\": 3000\n}\n"
},
{
"artifactId": "build.sh",
"kind": "sh",
"version": 1,
"seenAt": 1747000150000,
"path": "/w/.artifacts/build.sh",
"blob": "#!/bin/sh\nset -e\nnpm install\nnpm run build\n"
},
{
"artifactId": "README.md",
"kind": "md",
"version": 1,
"seenAt": 1747000210000,
"path": "/w/.artifacts/README.md",
"blob": "# Landing\n\nStatic landing page for the demo product.\n\n## Build\n\n```sh\nsh build.sh\n```\n"
}
]
};
})()

View File

@@ -292,6 +292,20 @@
head.append(label, summary)
el.append(head)
// Provenance banner: the real ArtifactServer broadcasts fs writes
// with no blob content, so per-hop diffs collapse to the "content
// not preserved" fallback in production. When any hop is missing
// blob content we surface why up front rather than letting the
// researcher assume the diff pane is broken. Fixture supplies
// blobs so all hops render; production doesn't, yet.
const missingBlobs = chain.some((c) => typeof c.blob !== 'string')
if (missingBlobs) {
const banner = document.createElement('div')
banner.className = 'artifact-evolution-banner muted small'
banner.textContent = 'Version diff currently requires blob payload. The real ArtifactServer does not preserve older blobs (RFC L-3 follow-up: snapshot store). The fixture supplies blobs for demo.'
el.append(banner)
}
const chainEl = document.createElement('ol')
chainEl.className = 'artifact-evolution-chain'
for (let i = 0; i < chain.length; i++) {

View File

@@ -176,6 +176,12 @@
b.panelEl = buildPanel()
stream.appendChild(b.panelEl)
}
// Remove the cold-clone empty-state hint (if present) once we have
// a real artifact to show. Introduced by lane-usability-fix so
// seedBoardFixture's placeholder gets swept when a real artifact
// event arrives.
const emptyHint = b.panelEl.querySelector('[data-role="empty-state"]')
if (emptyHint && emptyHint.parentNode) emptyHint.parentNode.removeChild(emptyHint)
const groupHost = b.panelEl.querySelector('.artifact-group')
groupHost.appendChild(el)
}
@@ -458,32 +464,87 @@
setTimeout(() => el.classList.remove('artifact-flash'), 900)
}
// Debug menu button — mocks a write into the artifact dir via IPC.
// Debug menu button — seeds the board/timeline/evolution demo. The
// real ArtifactServer broadcasts fs writes with no blob content, so
// cold-clone researchers never see the Board/Timeline/Evolution
// views' actual affordances. We route the multi-kind, multi-version
// fixture through the same onArtifactEvent entry the real wire uses
// so all three tabs light up via the production code path. When the
// seed is unavailable we fall back to the original IPC single-file
// mock.
function bindMockButton() {
const btn = document.getElementById('mock-artifact')
if (!btn) return
btn.addEventListener('click', async () => {
btn.disabled = true
try {
await window.dsh.mockArtifact()
const seed = typeof window !== 'undefined' ? window.__dshArtifactBoardSeed : null
if (seed && Array.isArray(seed.artifacts) && seed.artifacts.length) {
seedBoardFixture(seed.artifacts)
} else if (window.dsh && typeof window.dsh.mockArtifact === 'function') {
await window.dsh.mockArtifact()
}
} catch (err) {
console.error('mockArtifact failed', err)
console.error('mockArtifact / seed failed', err)
} finally {
btn.disabled = false
}
})
}
// Feed the board fixture through the same onArtifactEvent path the
// real ArtifactServer uses. `seenAt` is re-anchored to "moments ago"
// so relative-time labels don't read as ~430d against the frozen
// fixture timestamps. Idempotent: ensureCard() de-dups on artifactId,
// recordHistory() de-dups on (artifactId, version).
function seedBoardFixture(list) {
if (!Array.isArray(list) || !list.length) return
const base = Date.now() - list.length * 60_000
list.forEach((raw, i) => {
onArtifactEvent({ ...raw, seenAt: base + i * 60_000 })
})
}
// Cold-clone empty state: mount the panel scaffold on stream init so
// researchers can see List/Board/Timeline tabs (and the "no artifacts
// yet" hint) exist even before the first artifact:event lands. Until
// now the panel only appeared after the first artifact — new users
// saw a blank pane and had no idea the surface existed. Scoped to the
// active session bucket so switching sessions before any artifact
// fires still gets its own empty panel per session.
function ensureEmptyPanel() {
const s = streamEl()
if (!s) return
const b = bucket()
if (b.panelEl && b.panelEl.isConnected) return
b.panelEl = buildPanel()
// Inject a muted empty-state hint into the List body. Removed on
// first appendGrouped() call so real artifacts land in a clean host.
const body = b.panelEl.querySelector('.artifact-panel-body')
if (body) {
const hint = document.createElement('div')
hint.className = 'artifact-panel-empty muted small'
hint.dataset.role = 'empty-state'
hint.textContent = 'No artifacts yet — try “artifact” in the Debug menu, or run a session that writes to .artifacts/.'
body.appendChild(hint)
}
s.appendChild(b.panelEl)
}
// Wire up once the DOM + preload bridge are ready. The renderer script
// tag is loaded after this one, so we just register the listener
// eagerly.
if (window.dsh && typeof window.dsh.onArtifact === 'function') {
window.dsh.onArtifact(onArtifactEvent)
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', bindMockButton)
} else {
function initDom() {
bindMockButton()
ensureEmptyPanel()
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initDom)
} else {
initDom()
}
// Route artifact events on a session id if the payload carries one.
@@ -510,6 +571,8 @@
cards,
history,
switchView,
seedBoardFixture,
ensureEmptyPanel,
getView: () => bucket().currentView,
setActiveSession,
getActiveSessionId: () => activeSessionId,

View File

@@ -169,9 +169,20 @@ function renderSessionGraph(container, snapshot) {
label.setAttribute('y', String(pos.y + 4))
label.textContent = node.label
g.appendChild(label)
if (typeof snapshot?.onSelect === 'function' && node.turnId) {
// Bind click on any node that carries an actionable identifier:
// turn nodes → turnId, fork nodes → childSessionId, user nodes →
// seq. Only these get the pointer cursor; nodes without a target
// fall back to the default cursor so the affordance matches
// reality.
const actionable =
(node.turnId) ||
(node.kind === 'fork' && node.childSessionId) ||
(node.kind === 'user' && node.seq)
if (typeof snapshot?.onSelect === 'function' && actionable) {
g.addEventListener('click', () => snapshot.onSelect(node))
g.style && (g.style.cursor = 'pointer')
} else if (g.style) {
g.style.cursor = 'default'
}
svg.appendChild(g)
}

View File

@@ -1369,6 +1369,11 @@
Evolution renderers) and MUST load before artifacts.js so the L0 row
version-chip click handler + view switcher find the module ready. -->
<script src="./artifacts-board.js"></script>
<!-- Board fixture (multi-version + multi-kind demo data). Consumed by
the debug-menu "seed artifacts" seam so external researchers can
reach the Board/Timeline/Evolution views without waiting for a
real run to emit fs artifacts. -->
<script src="./artifact-board-seed.js"></script>
<script src="./artifacts.js"></script>
<script src="./plugin-runtime-fold.js"></script>
<script src="./mcp-tool-name.js"></script>

View File

@@ -4291,13 +4291,29 @@ function refreshChatSideDrawer() {
sessionId: state.activeSessionId || '',
model: meta && (meta.model || (meta.header && meta.header.model)) || '',
events,
selectedTurnId: null,
selectedTurnId: state.chatSelectedTurnId || null,
onSelect(row) {
if (!row || !row.turnId) return
if (!row) return
// User rows have no turnId — scroll to the matching data-seq
// anchor instead of the no-op early return. Cursor:pointer on
// the whole row promises interactivity; without this the row
// reads as a dead affordance.
if (row.kind === 'user' && row.seq) {
const seqTarget = streamEl && streamEl.querySelector(`[data-seq="${row.seq}"]`)
if (seqTarget && typeof seqTarget.scrollIntoView === 'function') {
seqTarget.scrollIntoView({ behavior: 'smooth', block: 'center' })
}
return
}
if (!row.turnId) return
state.chatSelectedTurnId = row.turnId
const target = streamEl && streamEl.querySelector(`[data-turn-id="${row.turnId}"]`)
if (target && typeof target.scrollIntoView === 'function') {
target.scrollIntoView({ behavior: 'smooth', block: 'center' })
}
// Re-render so the .active highlight lands on the row we just
// clicked. Keeps drawer state in sync with the stream focus.
refreshChatSideDrawerIfOpen()
},
})
}
@@ -4343,15 +4359,34 @@ function refreshSessionGraph() {
const events = (meta && Array.isArray(meta.cachedEvents)) ? meta.cachedEvents : []
api.renderSessionGraph(chatSessionGraphEl, {
sessionId: state.activeSessionId || '',
selectedTurnId: state.chatSelectedTurnId || null,
events,
onSelect(node) {
if (!node || !node.turnId) return
if (!node) return
// Fork nodes point at a child session — clicking follows the
// fork. Without this the cursor:pointer node reads as dead.
if (node.kind === 'fork' && node.childSessionId) {
try { selectSession(node.childSessionId) } catch (_) { /* stale id */ }
return
}
// User nodes have no turnId — scroll to the message via data-seq.
if (node.kind === 'user' && node.seq) {
setChatView('list')
const sTarget = streamEl && streamEl.querySelector(`[data-seq="${node.seq}"]`)
if (sTarget && typeof sTarget.scrollIntoView === 'function') {
sTarget.scrollIntoView({ behavior: 'smooth', block: 'center' })
}
return
}
if (!node.turnId) return
state.chatSelectedTurnId = node.turnId
// Jump to the turn in the List view and focus it.
setChatView('list')
const target = streamEl && streamEl.querySelector(`[data-turn-id="${node.turnId}"]`)
if (target && typeof target.scrollIntoView === 'function') {
target.scrollIntoView({ behavior: 'smooth', block: 'center' })
}
refreshChatSideDrawerIfOpen()
},
})
}

View File

@@ -178,13 +178,16 @@ function createStore() {
// so without dedupe here, two-page seeding would double-insert every
// event. Two-tier guard:
// 1. WeakSet on the fixture object ref (fast path for the common
// case where all pages read window.__dshRubricFusionSeed).
// case where all pages read window.__dshRubricFusionSeed). Return
// shape includes `deduped: true` on this fast-path hit so callers
// can distinguish "already seeded" from "empty fixture".
// 2. Per-event key dedupe inside addEvent() catches the case where a
// caller passes a fresh literal that happens to carry the same
// rows.
// caller passes a fresh literal (or a deep-cloned copy) that
// carries the same rows — the WeakSet won't help there because
// the object identity differs.
function loadFixture(json) {
if (!json || typeof json !== 'object') return { rubrics: 0, events: 0 }
if (state.loadedFixtures.has(json)) return { rubrics: 0, events: 0 }
if (state.loadedFixtures.has(json)) return { rubrics: 0, events: 0, deduped: true }
state.loadedFixtures.add(json)
let rn = 0, en = 0
for (const r of json.rubrics || []) { if (registerRubric(r)) rn++ }

View File

@@ -1,3 +1,7 @@
// Auto-inlined fusion fixture. Renderer runs at file:// so we inline
// docs/rubric-fusion-fixture.json here instead of relying on fetch().
// To refresh: node scripts/regen-rubric-fusion-seed.js
'use strict'
;(function () {
if (typeof window === 'undefined') return
@@ -5,7 +9,7 @@
"_note": "Rubric fusion fixture — feeds Rubrics/Growth/Runtime demo. All demo-tier data.",
"rubrics": [
{
"id": "svg-gen",
"id": "svg-generation",
"name": "SVG generation",
"group": "interaction-reasoning",
"description": "Iterative SVG editing over 3-5 turns. Judged for shape correctness, style consistency, and no-regression.",
@@ -97,6 +101,74 @@
"max": 5
}
]
},
{
"id": "code-review",
"name": "Code review",
"group": "se-process",
"description": "Reviewer rubric — style, correctness, test-coverage feedback quality.",
"dims": [
{
"id": "verdict",
"label": "Review verdict",
"type": "categorical",
"values": [
"reject",
"revise",
"approve"
]
}
]
},
{
"id": "correctness-score",
"name": "Correctness score",
"group": "fix-optimize",
"description": "Continuous 0-1 correctness on trajectory's stated goal.",
"dims": [
{
"id": "correctness",
"label": "Correctness",
"type": "continuous",
"min": 0,
"max": 1
}
]
},
{
"id": "intent-triage",
"name": "Intent triage",
"group": "interaction-reasoning",
"description": "Categorical verdict — bad · ok · good.",
"dims": [
{
"id": "verdict",
"label": "Triage verdict",
"type": "categorical",
"values": [
"bad",
"ok",
"good"
]
}
]
},
{
"id": "passes-bench",
"name": "Passes bench",
"group": "se-process",
"description": "Boolean pass/fail on bench probe.",
"dims": [
{
"id": "passes",
"label": "Passes bench probe",
"type": "boolean",
"labels": {
"true": "pass",
"false": "fail"
}
}
]
}
],
"similarClasses": [
@@ -117,7 +189,7 @@
"events": [
{
"ts": 1720915200000,
"rubricId": "svg-gen",
"rubricId": "svg-generation",
"dimId": "shape-correct",
"sessionId": "s-svg-1",
"turnId": "t1",
@@ -129,7 +201,7 @@
},
{
"ts": 1720915200000,
"rubricId": "svg-gen",
"rubricId": "svg-generation",
"dimId": "style-consistent",
"sessionId": "s-svg-1",
"turnId": "t1",
@@ -141,7 +213,7 @@
},
{
"ts": 1720915200000,
"rubricId": "svg-gen",
"rubricId": "svg-generation",
"dimId": "no-regress",
"sessionId": "s-svg-1",
"turnId": "t1",
@@ -153,7 +225,7 @@
},
{
"ts": 1721001600000,
"rubricId": "svg-gen",
"rubricId": "svg-generation",
"dimId": "shape-correct",
"sessionId": "s-svg-2",
"turnId": "t2",
@@ -165,7 +237,7 @@
},
{
"ts": 1721001600000,
"rubricId": "svg-gen",
"rubricId": "svg-generation",
"dimId": "style-consistent",
"sessionId": "s-svg-2",
"turnId": "t2",
@@ -177,7 +249,7 @@
},
{
"ts": 1721001600000,
"rubricId": "svg-gen",
"rubricId": "svg-generation",
"dimId": "no-regress",
"sessionId": "s-svg-2",
"turnId": "t2",
@@ -189,7 +261,7 @@
},
{
"ts": 1721088000000,
"rubricId": "svg-gen",
"rubricId": "svg-generation",
"dimId": "shape-correct",
"sessionId": "s-svg-3",
"turnId": "t3",
@@ -201,7 +273,7 @@
},
{
"ts": 1721088000000,
"rubricId": "svg-gen",
"rubricId": "svg-generation",
"dimId": "style-consistent",
"sessionId": "s-svg-3",
"turnId": "t3",
@@ -213,7 +285,7 @@
},
{
"ts": 1721088000000,
"rubricId": "svg-gen",
"rubricId": "svg-generation",
"dimId": "no-regress",
"sessionId": "s-svg-3",
"turnId": "t3",
@@ -225,7 +297,7 @@
},
{
"ts": 1721174400000,
"rubricId": "svg-gen",
"rubricId": "svg-generation",
"dimId": "shape-correct",
"sessionId": "s-svg-4",
"turnId": "t4",
@@ -237,7 +309,7 @@
},
{
"ts": 1721174400000,
"rubricId": "svg-gen",
"rubricId": "svg-generation",
"dimId": "style-consistent",
"sessionId": "s-svg-4",
"turnId": "t4",
@@ -249,7 +321,7 @@
},
{
"ts": 1721174400000,
"rubricId": "svg-gen",
"rubricId": "svg-generation",
"dimId": "no-regress",
"sessionId": "s-svg-4",
"turnId": "t4",
@@ -261,7 +333,7 @@
},
{
"ts": 1721260800000,
"rubricId": "svg-gen",
"rubricId": "svg-generation",
"dimId": "shape-correct",
"sessionId": "s-svg-5",
"turnId": "t5",
@@ -273,7 +345,7 @@
},
{
"ts": 1721260800000,
"rubricId": "svg-gen",
"rubricId": "svg-generation",
"dimId": "style-consistent",
"sessionId": "s-svg-5",
"turnId": "t5",
@@ -285,7 +357,7 @@
},
{
"ts": 1721260800000,
"rubricId": "svg-gen",
"rubricId": "svg-generation",
"dimId": "no-regress",
"sessionId": "s-svg-5",
"turnId": "t5",
@@ -297,7 +369,7 @@
},
{
"ts": 1721260800000,
"rubricId": "svg-gen",
"rubricId": "svg-generation",
"dimId": "shape-correct",
"sessionId": "s-svg-live",
"turnId": "tL",
@@ -309,7 +381,7 @@
},
{
"ts": 1721260800000,
"rubricId": "svg-gen",
"rubricId": "svg-generation",
"dimId": "style-consistent",
"sessionId": "s-svg-live",
"turnId": "tL",
@@ -321,7 +393,7 @@
},
{
"ts": 1721260800000,
"rubricId": "svg-gen",
"rubricId": "svg-generation",
"dimId": "no-regress",
"sessionId": "s-svg-live",
"turnId": "tL",
@@ -333,7 +405,7 @@
},
{
"ts": 1721260800000,
"rubricId": "svg-gen",
"rubricId": "svg-generation",
"dimId": "shape-correct",
"sessionId": "s-svg-live",
"turnId": "tL",
@@ -345,7 +417,7 @@
},
{
"ts": 1721260800000,
"rubricId": "svg-gen",
"rubricId": "svg-generation",
"dimId": "style-consistent",
"sessionId": "s-svg-live",
"turnId": "tL",
@@ -357,7 +429,7 @@
},
{
"ts": 1721260800000,
"rubricId": "svg-gen",
"rubricId": "svg-generation",
"dimId": "no-regress",
"sessionId": "s-svg-live",
"turnId": "tL",
@@ -369,7 +441,7 @@
},
{
"ts": 1721260800000,
"rubricId": "svg-gen",
"rubricId": "svg-generation",
"dimId": "shape-correct",
"sessionId": "s-svg-live",
"turnId": "tL",
@@ -381,7 +453,7 @@
},
{
"ts": 1721260800000,
"rubricId": "svg-gen",
"rubricId": "svg-generation",
"dimId": "style-consistent",
"sessionId": "s-svg-live",
"turnId": "tL",
@@ -393,7 +465,7 @@
},
{
"ts": 1721260800000,
"rubricId": "svg-gen",
"rubricId": "svg-generation",
"dimId": "no-regress",
"sessionId": "s-svg-live",
"turnId": "tL",
@@ -618,6 +690,150 @@
"harnessVersion": "v0.11",
"model": "deepseek-r1",
"dataMix": "mix-c"
},
{
"ts": 1721088000000,
"rubricId": "code-review",
"dimId": "verdict",
"sessionId": "s-cr-1",
"turnId": "t1",
"rolloutIdx": 1,
"score": "revise",
"harnessVersion": "v0.10",
"model": "deepseek-r1",
"dataMix": "mix-b"
},
{
"ts": 1721174400000,
"rubricId": "code-review",
"dimId": "verdict",
"sessionId": "s-cr-2",
"turnId": "t2",
"rolloutIdx": 1,
"score": "approve",
"harnessVersion": "v0.11",
"model": "deepseek-r1",
"dataMix": "mix-b"
},
{
"ts": 1721260800000,
"rubricId": "code-review",
"dimId": "verdict",
"sessionId": "s-cr-3",
"turnId": "t3",
"rolloutIdx": 1,
"score": "approve",
"harnessVersion": "v0.11",
"model": "deepseek-r1",
"dataMix": "mix-c"
},
{
"ts": 1721088000000,
"rubricId": "correctness-score",
"dimId": "correctness",
"sessionId": "s-cs-1",
"turnId": "t1",
"rolloutIdx": 1,
"score": 0.62,
"harnessVersion": "v0.10",
"model": "deepseek-r1",
"dataMix": "mix-b"
},
{
"ts": 1721174400000,
"rubricId": "correctness-score",
"dimId": "correctness",
"sessionId": "s-cs-2",
"turnId": "t2",
"rolloutIdx": 1,
"score": 0.78,
"harnessVersion": "v0.11",
"model": "deepseek-r1",
"dataMix": "mix-b"
},
{
"ts": 1721260800000,
"rubricId": "correctness-score",
"dimId": "correctness",
"sessionId": "s-cs-3",
"turnId": "t3",
"rolloutIdx": 1,
"score": 0.85,
"harnessVersion": "v0.11",
"model": "deepseek-r1",
"dataMix": "mix-c"
},
{
"ts": 1721088000000,
"rubricId": "intent-triage",
"dimId": "verdict",
"sessionId": "s-it-1",
"turnId": "t1",
"rolloutIdx": 1,
"score": "ok",
"harnessVersion": "v0.10",
"model": "deepseek-r1",
"dataMix": "mix-b"
},
{
"ts": 1721174400000,
"rubricId": "intent-triage",
"dimId": "verdict",
"sessionId": "s-it-2",
"turnId": "t2",
"rolloutIdx": 1,
"score": "good",
"harnessVersion": "v0.11",
"model": "deepseek-r1",
"dataMix": "mix-b"
},
{
"ts": 1721260800000,
"rubricId": "intent-triage",
"dimId": "verdict",
"sessionId": "s-it-3",
"turnId": "t3",
"rolloutIdx": 1,
"score": "good",
"harnessVersion": "v0.11",
"model": "deepseek-r1",
"dataMix": "mix-c"
},
{
"ts": 1721088000000,
"rubricId": "passes-bench",
"dimId": "passes",
"sessionId": "s-pb-1",
"turnId": "t1",
"rolloutIdx": 1,
"score": false,
"harnessVersion": "v0.10",
"model": "deepseek-r1",
"dataMix": "mix-b"
},
{
"ts": 1721174400000,
"rubricId": "passes-bench",
"dimId": "passes",
"sessionId": "s-pb-2",
"turnId": "t2",
"rolloutIdx": 1,
"score": true,
"harnessVersion": "v0.11",
"model": "deepseek-r1",
"dataMix": "mix-b"
},
{
"ts": 1721260800000,
"rubricId": "passes-bench",
"dimId": "passes",
"sessionId": "s-pb-3",
"turnId": "t3",
"rolloutIdx": 1,
"score": true,
"harnessVersion": "v0.11",
"model": "deepseek-r1",
"dataMix": "mix-c"
}
]
};

View File

@@ -12574,6 +12574,18 @@ button.artifact-version:hover {
border: 1px dashed var(--border); border-radius: 4px;
margin-top: 4px;
}
.artifact-evolution-banner {
padding: 6px 8px; font-size: 11px; line-height: 1.4;
border-left: 2px solid var(--border);
background: var(--bg-elev);
border-radius: 0 4px 4px 0;
margin: 4px 0 6px 0;
}
.artifact-panel-empty {
padding: 12px 10px;
text-align: center;
line-height: 1.5;
}
.artifact-evolution-diff-body {
margin-top: 4px;
border: 1px solid var(--border); border-radius: 4px;

View File

@@ -472,4 +472,17 @@
// tri-view instead of on the index. Auto-drill only fires when the caller
// has a valid sessionId; the internal path already handles refresh + rehydrate.
window.__dshTracingPage = { show, closeDrill, openDrill }
// Runtime → Rubric grid cell click dispatches `dsh:rubric-cell-jump`. The
// upstream fires with { sessionId, turnId } detail and calls __dshTabs to
// switch, but drops the ids. Wire the drill here so clicking a cell
// actually lands the user inside that session's trace — the whole value
// prop of the grid.
if (typeof window !== 'undefined' && typeof window.addEventListener === 'function') {
window.addEventListener('dsh:rubric-cell-jump', function (ev) {
const detail = ev && ev.detail
if (!detail || !detail.sessionId) return
try { openDrill(detail.sessionId) } catch (_) { /* drill unavailable — swallow */ }
})
}
})()