feat(desktop): fold Rubrics/Growth/Runtimes into one Evals nav entry (3 tabs + shared rubric selector)
This commit is contained in:
BIN
examples/desktop/docs/qa-evals-merge/00-sidebar-full.png
Normal file
BIN
examples/desktop/docs/qa-evals-merge/00-sidebar-full.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 261 KiB |
BIN
examples/desktop/docs/qa-evals-merge/01-evals-rubrics.png
Normal file
BIN
examples/desktop/docs/qa-evals-merge/01-evals-rubrics.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 386 KiB |
BIN
examples/desktop/docs/qa-evals-merge/02-evals-growth.png
Normal file
BIN
examples/desktop/docs/qa-evals-merge/02-evals-growth.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 426 KiB |
BIN
examples/desktop/docs/qa-evals-merge/03-evals-runtime.png
Normal file
BIN
examples/desktop/docs/qa-evals-merge/03-evals-runtime.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 336 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 253 KiB |
274
examples/desktop/scripts/qa-cdp-shoot-evals-merge.mjs
Normal file
274
examples/desktop/scripts/qa-cdp-shoot-evals-merge.mjs
Normal file
@@ -0,0 +1,274 @@
|
||||
// Verification script for lane-evals-merge (2026-07-19):
|
||||
// launches an isolated Electron on CDP :9455 with its own --user-data-dir
|
||||
// and DSH_DESKTOP_HOME, opens the shell, clicks the new Evals nav item,
|
||||
// cycles the three inner tabs [ Rubrics | Growth | Runtime ], flips the
|
||||
// shared rubric selector, and shoots five screenshots into
|
||||
// docs/qa-evals-merge/:
|
||||
// 00-sidebar-full.png — left nav shows Evals (not three rows)
|
||||
// 01-evals-rubrics.png — Rubrics tab active
|
||||
// 02-evals-growth.png — Growth tab active
|
||||
// 03-evals-runtime.png — Runtime tab active (all rubrics)
|
||||
// 04-evals-selector-runtime.png — shared selector picks one rubric,
|
||||
// Runtime grid filters to that card
|
||||
//
|
||||
// Two-root isolation (per 2026-07-18 postmortem in
|
||||
// scripts/qa-cdp-shoot-affordance.mjs header):
|
||||
// 1. --user-data-dir=<tmp> isolates Chromium userdata
|
||||
// 2. DSH_DESKTOP_HOME=<tmp> isolates our shell's config root
|
||||
// Neither the user's running Electron on CDP 9333 nor their ~/.dsh-desktop
|
||||
// is touched.
|
||||
|
||||
import { spawn } from 'node:child_process'
|
||||
import { existsSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'
|
||||
import { resolve, join } from 'node:path'
|
||||
import { setTimeout as sleep } from 'node:timers/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
|
||||
const WORKTREE = resolve(process.env.DSH_WORKTREE || process.cwd())
|
||||
const PARENT = resolve(process.env.DSH_REPO || '/Users/ziya/harness/dsh-desktop-demo')
|
||||
const ELECTRON = join(PARENT, 'node_modules/.bin/electron')
|
||||
const CDP_PORT = Number(process.env.DSH_EVALS_MERGE_PORT || 9455)
|
||||
const USER_DATA = join(tmpdir(), 'dsh-evals-merge-userdata')
|
||||
const DSH_HOME = join(tmpdir(), 'dsh-evals-merge-home')
|
||||
const OUTDIR = join(WORKTREE, 'docs/qa-evals-merge')
|
||||
|
||||
if (!existsSync(ELECTRON)) {
|
||||
console.error(`electron binary not found at ${ELECTRON}`)
|
||||
process.exit(2)
|
||||
}
|
||||
mkdirSync(OUTDIR, { recursive: true })
|
||||
for (const dir of [USER_DATA, DSH_HOME]) {
|
||||
try { rmSync(dir, { recursive: true, force: true }) } catch {}
|
||||
mkdirSync(dir, { recursive: true })
|
||||
}
|
||||
// Minimal onboarding seed so we don't hit the first-run modal. Same shape
|
||||
// as scripts/qa-cdp-shoot-affordance.mjs — never touches the real ~/.dsh-desktop.
|
||||
const seedOverlay = [
|
||||
'# QA evals-merge seed overlay (tmp, per-run).',
|
||||
'plugins:',
|
||||
' - "@cordisjs/plugin-include":',
|
||||
` path: ${join(WORKTREE, 'config/daemon-echo.yml')}`,
|
||||
'',
|
||||
].join('\n')
|
||||
writeFileSync(join(DSH_HOME, 'user-overlay.cordis.yml'), seedOverlay)
|
||||
writeFileSync(join(DSH_HOME, 'config.json'), JSON.stringify({
|
||||
role: 'coding', approvalMode: 'never',
|
||||
}))
|
||||
writeFileSync(join(DSH_HOME, '.onboarded'), new Date().toISOString())
|
||||
|
||||
async function bootElectron() {
|
||||
const child = spawn(ELECTRON, [
|
||||
`--remote-debugging-port=${CDP_PORT}`,
|
||||
`--user-data-dir=${USER_DATA}`,
|
||||
'--disable-gpu',
|
||||
'--no-sandbox',
|
||||
'.',
|
||||
], {
|
||||
cwd: WORKTREE,
|
||||
env: {
|
||||
...process.env,
|
||||
DSH_DESKTOP_HOME: DSH_HOME,
|
||||
DSH_MAXIMIZE: '1',
|
||||
// DSH_QA is intentionally NOT set — it injects the `#qa` URL hash
|
||||
// that turns on qa-harness.js's click-sweep, which fires switchTo
|
||||
// in the middle of our own driver's shot sequence. We only need
|
||||
// isolation + a stable renderer, not the auto-walker.
|
||||
},
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
})
|
||||
const logs = []
|
||||
child.stdout.on('data', d => logs.push(String(d)))
|
||||
child.stderr.on('data', d => logs.push(String(d)))
|
||||
for (let i = 0; i < 40; i++) {
|
||||
await sleep(500)
|
||||
try {
|
||||
const r = await fetch(`http://localhost:${CDP_PORT}/json/list`)
|
||||
if (r.ok) return { child, logs }
|
||||
} catch {}
|
||||
}
|
||||
child.kill('SIGKILL')
|
||||
console.error('electron CDP did not come up in 20s. logs:\n' + logs.join(''))
|
||||
process.exit(3)
|
||||
}
|
||||
|
||||
async function newCdp() {
|
||||
const targets = await (await fetch(`http://localhost:${CDP_PORT}/json/list`)).json()
|
||||
const target = targets.find(t => t.type === 'page')
|
||||
if (!target) throw new Error('no page target on port ' + CDP_PORT)
|
||||
const ws = new WebSocket(target.webSocketDebuggerUrl)
|
||||
await new Promise((ok, err) => { ws.onopen = ok; ws.onerror = e => err(e) })
|
||||
let id = 1
|
||||
const pending = new Map()
|
||||
ws.onmessage = ev => {
|
||||
const msg = JSON.parse(typeof ev.data === 'string' ? ev.data : String(ev.data))
|
||||
if (msg.id != null && pending.has(msg.id)) {
|
||||
const [ok, err] = pending.get(msg.id); pending.delete(msg.id)
|
||||
if (msg.error) err(new Error(msg.error.message)); else ok(msg.result)
|
||||
}
|
||||
}
|
||||
const call = (m, p = {}, ms = 15000) => new Promise((ok, err) => {
|
||||
const _id = id++
|
||||
const t = setTimeout(() => { pending.delete(_id); err(new Error('cdp timeout: ' + m)) }, ms)
|
||||
pending.set(_id, [v => { clearTimeout(t); ok(v) }, e => { clearTimeout(t); err(e) }])
|
||||
ws.send(JSON.stringify({ id: _id, method: m, params: p }))
|
||||
})
|
||||
const evj = async expr => {
|
||||
const r = await call('Runtime.evaluate', {
|
||||
expression: `(async()=>{try{return (${expr})}catch(e){return {__err:String(e)}}})()`,
|
||||
returnByValue: true, awaitPromise: true,
|
||||
})
|
||||
if (r.exceptionDetails) throw new Error(r.exceptionDetails.exception?.description || r.exceptionDetails.text)
|
||||
return r.result?.value
|
||||
}
|
||||
return { ws, call, evj }
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const { child, logs } = await bootElectron()
|
||||
const kill = () => { try { child.kill('SIGKILL') } catch {} }
|
||||
try {
|
||||
const cdp = await newCdp()
|
||||
await cdp.call('Page.enable')
|
||||
await cdp.call('Runtime.enable')
|
||||
// Wait until the sidebar Evals button exists — proves index.html
|
||||
// wired the new nav and the renderer has painted.
|
||||
let ready = false
|
||||
for (let i = 0; i < 60; i++) {
|
||||
await sleep(200)
|
||||
ready = await cdp.evj(`!!document.querySelector('.tab-btn[data-tab="evals"]')`)
|
||||
if (ready) break
|
||||
}
|
||||
if (!ready) throw new Error('Evals nav button never mounted — check index.html + renderer.js')
|
||||
|
||||
const shoot = async (name) => {
|
||||
// Retry once — the Runtime tab paint kicks off async listProfiles/
|
||||
// runtimeStatus calls; on slow machines the first capture attempt
|
||||
// occasionally times out while the compositor is busy. A single
|
||||
// retry with a longer settle is enough in practice.
|
||||
await sleep(500)
|
||||
let attempts = 0
|
||||
let lastErr
|
||||
while (attempts < 2) {
|
||||
attempts++
|
||||
try {
|
||||
const r = await cdp.call('Page.captureScreenshot', { format: 'png' }, 90000)
|
||||
writeFileSync(join(OUTDIR, name + '.png'), Buffer.from(r.data, 'base64'))
|
||||
console.error('wrote', name + '.png')
|
||||
return
|
||||
} catch (e) {
|
||||
lastErr = e
|
||||
console.error(`shoot ${name} attempt ${attempts} failed: ${e.message}; retrying after 800ms`)
|
||||
await sleep(800)
|
||||
}
|
||||
}
|
||||
throw lastErr
|
||||
}
|
||||
|
||||
// 00 — sidebar full, before entering Evals. Prove: exactly ONE
|
||||
// Evals button; the three legacy ids (rubrics/growth/runtimes)
|
||||
// are gone from the nav.
|
||||
const sidebar = await cdp.evj(`(() => {
|
||||
const items = [...document.querySelectorAll('.sidebar-nav .tab-btn')]
|
||||
return items.map(b => ({ tab: b.dataset.tab, label: (b.textContent || '').trim() }))
|
||||
})()`)
|
||||
console.error('sidebar snapshot:', JSON.stringify(sidebar))
|
||||
const tabs = sidebar.map(s => s.tab)
|
||||
if (tabs.filter(t => t === 'evals').length !== 1) throw new Error('expected exactly one evals button, got ' + tabs.filter(t => t === 'evals').length)
|
||||
for (const legacy of ['rubrics', 'growth', 'runtimes']) {
|
||||
if (tabs.includes(legacy)) throw new Error(`legacy nav id still present: ${legacy}`)
|
||||
}
|
||||
await shoot('00-sidebar-full')
|
||||
|
||||
// 01 — click Evals, expect Rubrics tab active + rubrics-catalog painted.
|
||||
await cdp.evj(`document.querySelector('.tab-btn[data-tab="evals"]').click()`)
|
||||
await sleep(500)
|
||||
const rubricsActive = await cdp.evj(`(() => {
|
||||
const pane = document.querySelector('.pane[data-pane="evals"]')
|
||||
if (!pane) return { ok: false, reason: 'no evals pane' }
|
||||
if (pane.hidden) return { ok: false, reason: 'pane hidden after switchTo' }
|
||||
const active = pane.dataset.evalsActive
|
||||
const rubricsPane = pane.querySelector('.evals-tab-pane[data-evals-tab-pane="rubrics"]')
|
||||
const growthPane = pane.querySelector('.evals-tab-pane[data-evals-tab-pane="growth"]')
|
||||
const runtimePane = pane.querySelector('.evals-tab-pane[data-evals-tab-pane="runtime"]')
|
||||
return {
|
||||
ok: active === 'rubrics' && !rubricsPane.hidden && growthPane.hidden && runtimePane.hidden,
|
||||
active,
|
||||
rubricsHidden: rubricsPane.hidden,
|
||||
growthHidden: growthPane.hidden,
|
||||
runtimeHidden: runtimePane.hidden,
|
||||
catalogChildren: pane.querySelectorAll('#rubrics-catalog *').length,
|
||||
}
|
||||
})()`)
|
||||
console.error('rubrics-active state:', JSON.stringify(rubricsActive))
|
||||
if (!rubricsActive.ok) throw new Error('rubrics tab did not activate as expected: ' + JSON.stringify(rubricsActive))
|
||||
await shoot('01-evals-rubrics')
|
||||
|
||||
// 02 — click Growth tab, expect only Growth pane visible.
|
||||
await cdp.evj(`document.querySelector('.evals-tab[data-evals-tab="growth"]').click()`)
|
||||
await sleep(500)
|
||||
const growthActive = await cdp.evj(`(() => {
|
||||
const pane = document.querySelector('.pane[data-pane="evals"]')
|
||||
const active = pane.dataset.evalsActive
|
||||
const growthPane = pane.querySelector('.evals-tab-pane[data-evals-tab-pane="growth"]')
|
||||
const rubricsPane = pane.querySelector('.evals-tab-pane[data-evals-tab-pane="rubrics"]')
|
||||
const runtimePane = pane.querySelector('.evals-tab-pane[data-evals-tab-pane="runtime"]')
|
||||
return { ok: active === 'growth' && !growthPane.hidden && rubricsPane.hidden && runtimePane.hidden, active }
|
||||
})()`)
|
||||
console.error('growth-active state:', JSON.stringify(growthActive))
|
||||
if (!growthActive.ok) throw new Error('growth tab did not activate as expected: ' + JSON.stringify(growthActive))
|
||||
await shoot('02-evals-growth')
|
||||
|
||||
// 03 — click Runtime tab. Confirm the rollout-grid card container
|
||||
// for the fusion-seeded rubrics is populated.
|
||||
await cdp.evj(`document.querySelector('.evals-tab[data-evals-tab="runtime"]').click()`)
|
||||
await sleep(700)
|
||||
const runtimeState = await cdp.evj(`(() => {
|
||||
const evals = document.querySelector('.pane[data-pane="evals"]')
|
||||
const active = evals ? evals.dataset.evalsActive : null
|
||||
const runtimePane = evals ? evals.querySelector('.evals-tab-pane[data-evals-tab-pane="runtime"]') : null
|
||||
const gridCards = evals ? evals.querySelectorAll('[data-testid^="rubric-grid-card-"]').length : 0
|
||||
return {
|
||||
ok: !!evals && !evals.hidden && active === 'runtime' && runtimePane && !runtimePane.hidden,
|
||||
active, gridCards,
|
||||
}
|
||||
})()`)
|
||||
console.error('runtime-active state:', JSON.stringify(runtimeState))
|
||||
if (!runtimeState.ok) throw new Error('runtime tab did not activate as expected: ' + JSON.stringify(runtimeState))
|
||||
await shoot('03-evals-runtime')
|
||||
|
||||
// 04 — pick a rubric in the shared selector and confirm the Runtime
|
||||
// grid filters down. The fusion seed loads a handful of rubrics; we
|
||||
// take the first available id.
|
||||
const filterProof = await cdp.evj(`(async () => {
|
||||
const sel = document.getElementById('evals-shared-rubric')
|
||||
if (!sel) return { ok: false, reason: 'no shared selector' }
|
||||
// Skip the "All rubrics" sentinel option; pick the first real one.
|
||||
const opts = [...sel.querySelectorAll('option')].filter(o => o.value)
|
||||
if (!opts.length) return { ok: false, reason: 'no rubric options' }
|
||||
const pick = opts[0].value
|
||||
sel.value = pick
|
||||
sel.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
// Give the change listener + Runtime repaint a moment.
|
||||
await new Promise(r => setTimeout(r, 400))
|
||||
const gridCards = [...document.querySelectorAll('[data-testid^="rubric-grid-card-"]')]
|
||||
return {
|
||||
ok: gridCards.length === 1,
|
||||
picked: pick,
|
||||
gridCardIds: gridCards.map(el => el.getAttribute('data-testid')),
|
||||
}
|
||||
})()`)
|
||||
console.error('shared-selector filter proof:', JSON.stringify(filterProof))
|
||||
if (!filterProof.ok) throw new Error('shared-selector filter did not narrow the Runtime grid: ' + JSON.stringify(filterProof))
|
||||
await shoot('04-evals-selector-runtime')
|
||||
|
||||
console.error('all evals-merge checkpoints passed')
|
||||
kill()
|
||||
} catch (e) {
|
||||
console.error('shoot failed:', e.message)
|
||||
console.error('captured logs:\n' + logs.slice(-30).join(''))
|
||||
kill()
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(e => { console.error(e); process.exit(1) })
|
||||
@@ -104,35 +104,31 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="nav-group" data-nav-group="runtime" data-item-count="6">
|
||||
<div class="nav-group" data-nav-group="runtime" data-item-count="4">
|
||||
<div class="nav-group-header">runtime</div>
|
||||
<!-- lane-rubrics slot — live as of NAV delta merge (feat/lane-rubrics-annotation
|
||||
@ 70293ba already merged as a13e020; this merge is the flip point per team-
|
||||
lead rule "hub+rubrics flip in NAV delta since neither NAV nor RUB touched
|
||||
index.html on the merge path"). data-fixture-tier stays because the 28-task
|
||||
rubric catalog is still fixture-backed until the annotation-store producer
|
||||
wires up; the "· demo" chip decorateNav() paints from it stays truthful. -->
|
||||
<button class="tab-btn nav-item" data-tab="rubrics" data-fixture-tier="true"
|
||||
title="Rubrics — catalog-first scoring rubric library, 28-task groups (fixture-tier)">
|
||||
<!-- Evals entry (lane-evals-merge, 2026-07-19). Three former nav
|
||||
items — Rubrics, Growth, Runtimes — are collapsed into a
|
||||
single Evals door because they already share one rubric
|
||||
scoring event log (rubric-fusion-model.js) and mean much
|
||||
less on their own than they do together. Inner surfaces are
|
||||
unchanged; the pane just grows a top tab strip that mounts
|
||||
each existing controller under [ Rubrics | Growth | Runtime ].
|
||||
data-fixture-tier stays: the fusion event log is still
|
||||
fixture-backed (rubric-fusion-seed.js) until the annotation
|
||||
store and rubric library seams wire up. -->
|
||||
<button class="tab-btn nav-item" data-tab="evals" data-fixture-tier="true"
|
||||
title="Evals — three views over the shared rubric scoring event log (Rubrics catalog · Growth timeline · Runtime rollout grid).">
|
||||
<svg viewBox="0 0 20 20" width="16" height="16" aria-hidden="true"><path fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" d="M4 5h12M4 10h12M4 15h8M14 14l2 2 3-3"/></svg>
|
||||
<span>Rubrics</span>
|
||||
<span>Evals</span>
|
||||
</button>
|
||||
<button class="tab-btn nav-item" data-tab="plugins" title="Plugins">
|
||||
<svg viewBox="0 0 20 20" width="16" height="16" aria-hidden="true"><path fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round" d="M7 3v3H4v4a2 2 0 0 0 2 2h1v4h3v-4h2v4h3v-4a2 2 0 0 0 2-2V6h-3V3h-3v3h-2V3z"/></svg>
|
||||
<span>Plugins</span>
|
||||
</button>
|
||||
<button class="tab-btn nav-item" data-tab="runtimes" title="Local runtime profiles + isolated daemons">
|
||||
<svg viewBox="0 0 20 20" width="16" height="16" aria-hidden="true"><path fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round" d="M3 5h14v4H3zM3 11h14v4H3zM6 7h.01M6 13h.01M9 7h.01M9 13h.01"/></svg>
|
||||
<span>Runtimes</span>
|
||||
</button>
|
||||
<button class="tab-btn nav-item" data-tab="mission" title="Missions">
|
||||
<svg viewBox="0 0 20 20" width="16" height="16" aria-hidden="true"><circle cx="10" cy="10" r="6.5" fill="none" stroke="currentColor" stroke-width="1.6"/><circle cx="10" cy="10" r="2" fill="currentColor"/><path stroke="currentColor" stroke-width="1.6" stroke-linecap="round" d="M10 1.5V4M10 16v2.5M1.5 10H4M16 10h2.5"/></svg>
|
||||
<span>Missions</span>
|
||||
</button>
|
||||
<button class="tab-btn nav-item" data-tab="growth" title="Growth (self-evolution log)">
|
||||
<svg viewBox="0 0 20 20" width="16" height="16" aria-hidden="true"><path fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" d="M3 16.5h14M4.5 16V12M8 16V8.5M12 16V5M15.5 16V10M4.5 12l3.5-3.5L12 5l3.5 5"/></svg>
|
||||
<span>Growth</span>
|
||||
</button>
|
||||
<button class="tab-btn nav-item" data-tab="prs" title="Pull Requests">
|
||||
<svg viewBox="0 0 20 20" width="16" height="16" aria-hidden="true"><circle cx="5" cy="4" r="1.8" fill="none" stroke="currentColor" stroke-width="1.5"/><circle cx="5" cy="16" r="1.8" fill="none" stroke="currentColor" stroke-width="1.5"/><circle cx="15" cy="16" r="1.8" fill="none" stroke="currentColor" stroke-width="1.5"/><path fill="none" stroke="currentColor" stroke-width="1.5" d="M5 5.8v8.4M5 6.5c0 4 5 4 5 6.5V6M15 14.2V9l-2 2M15 9l2 2"/></svg>
|
||||
<span>PRs</span>
|
||||
@@ -237,35 +233,34 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Runtimes sidebar — page owns its own body; the sidebar just
|
||||
orients + provides a refresh button. See runtimes-page.js. -->
|
||||
<div class="tab-panel" data-tab-panel="runtimes" hidden>
|
||||
<!-- Evals sidebar (lane-evals-merge, 2026-07-19). Rubrics / Growth /
|
||||
Runtimes were three siblings until 2026-07-19; each had its own
|
||||
sidebar panel. They're now one door because the three views
|
||||
project the same rubric scoring event log. This single panel
|
||||
orients the researcher to the three tabs (top of the pane) and
|
||||
preserves both the rubrics-catalog Refresh button and the
|
||||
Export annotations action from the old Rubrics sidebar so no
|
||||
deeplink from Chat header / Recent rows loses its target. The
|
||||
Runtimes refresh button is preserved under the same id so
|
||||
runtimes-page.js click handlers keep working. -->
|
||||
<div class="tab-panel" data-tab-panel="evals" hidden>
|
||||
<div class="sidebar-section-head">
|
||||
<span class="section-label">Runtimes</span>
|
||||
<button id="runtimes-refresh" class="icon-btn ghost" title="Reread profile list + status" aria-label="Refresh">
|
||||
<span class="section-label">Evals</span>
|
||||
<button id="rubrics-refresh" class="icon-btn ghost" title="Reload the rubric catalog + fusion store" aria-label="Refresh">
|
||||
<svg viewBox="0 0 20 20" width="14" height="14" aria-hidden="true"><path fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" d="M15.5 4.5v4h-4M4.5 15.5v-4h4M5.2 7.6a6 6 0 0 1 10 .4M14.8 12.4a6 6 0 0 1-10-.4"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="runtimes-sidebar-note muted">
|
||||
Local profiles, adapters, and any isolated daemon currently up. Composed
|
||||
locally until <code>runtime/list</code> lands on the wire (gap G8).
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rubrics sidebar (task #188) — orients the user to the catalog. -->
|
||||
<div class="tab-panel" data-tab-panel="rubrics" hidden>
|
||||
<div class="sidebar-section-head">
|
||||
<span class="section-label">Rubrics</span>
|
||||
<button id="rubrics-refresh" class="icon-btn ghost" title="Reload the catalog" aria-label="Refresh">
|
||||
<svg viewBox="0 0 20 20" width="14" height="14" aria-hidden="true"><path fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" d="M15.5 4.5v4h-4M4.5 15.5v-4h4M5.2 7.6a6 6 0 0 1 10 .4M14.8 12.4a6 6 0 0 1-10-.4"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="rubrics-sidebar-note muted">
|
||||
Evaluator library — one SKILL.md file per rubric, grouped by task category.
|
||||
Every rubric attaches to Bench for scoring runs; "Create from scratch"
|
||||
lives at the bottom of the page as a fallback CTA.
|
||||
<div class="evals-sidebar-note muted">
|
||||
One rubric scoring event log, three views. Rubrics = catalog, Growth =
|
||||
timeline over sessions, Runtime = per-rubric rollout grid. Pick a rubric
|
||||
from the top selector to keep them in sync.
|
||||
</div>
|
||||
<div class="sidebar-foot">
|
||||
<!-- Runtimes refresh handler still bound by id; keeps the sidebar
|
||||
button contract stable for runtimes-page.js's click handler
|
||||
and any deeplink that targeted it. -->
|
||||
<button id="runtimes-refresh" class="ghost small" title="Reread profile list + status (Runtime tab).">Reload runtimes</button>
|
||||
<button id="growth-refresh" class="ghost small" title="Reread the growth log (Growth tab).">Reload growth</button>
|
||||
<button id="rubrics-annotate-open" class="ghost small" title="Open the export drawer for annotated sessions">Export annotations</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -281,20 +276,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Growth sidebar (kept minimal — this page is a self-contained page,
|
||||
the sidebar just orients the user to what it's for). -->
|
||||
<div class="tab-panel" data-tab-panel="growth" hidden>
|
||||
<div class="sidebar-section-head">
|
||||
<span class="section-label">Growth</span>
|
||||
<button id="growth-refresh" class="icon-btn ghost" title="Reread the growth log" aria-label="Refresh">
|
||||
<svg viewBox="0 0 20 20" width="14" height="14" aria-hidden="true"><path fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" d="M15.5 4.5v4h-4M4.5 15.5v-4h4M5.2 7.6a6 6 0 0 1 10 .4M14.8 12.4a6 6 0 0 1-10-.4"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="growth-sidebar-note muted">
|
||||
Every entry links to a real event — plugin install, overlay apply, vibe
|
||||
session. This is an audit trail, not a persona.
|
||||
</div>
|
||||
</div>
|
||||
<!-- Growth sidebar folded into the Evals sidebar above (2026-07-19,
|
||||
lane-evals-merge). The growth-refresh button lives in the new
|
||||
Evals sidebar-foot so growth-v2.js listeners keep binding by id.
|
||||
If a future lane splits Growth back out, restore this block
|
||||
with the original heading + refresh icon. -->
|
||||
</aside>
|
||||
<main class="main">
|
||||
<!-- Chat pane -->
|
||||
@@ -937,13 +923,67 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Rubrics pane (task #188) — catalog-first evaluator library.
|
||||
<!-- Evals pane (lane-evals-merge, 2026-07-19). Wraps three sub-panes —
|
||||
Rubrics catalog, Growth timeline, Runtime rollout grid — that
|
||||
share the same rubric scoring event log (rubric-fusion-model.js).
|
||||
The outer .pane is what switchTo('evals') toggles; only one of
|
||||
the three inner sub-panes shows at a time based on the
|
||||
data-evals-tab strip below. The inner sub-panes keep their
|
||||
original data-pane values because rubrics-page.js / growth-v2.js
|
||||
/ runtimes-page.js all query them by that selector — the
|
||||
renderer.js switchTo() loop is updated to iterate direct main
|
||||
children only so nested data-pane doesn't fight the outer
|
||||
hidden flag. Shared selector (rubric picker) sits above the
|
||||
tab strip so choosing a rubric syncs all three views. -->
|
||||
<section class="pane" data-pane="evals" hidden>
|
||||
<header class="header evals-header">
|
||||
<div class="header-lead">
|
||||
<div class="page-title">
|
||||
Evals
|
||||
<span class="demo-tier-chip" title="The rubric scoring event log is still fixture-backed (rubric-fusion-seed.js). User-authored rubrics and real annotation results land as the G1 library seam and the annotation-store producer wire up.">demo · G1 pending</span>
|
||||
</div>
|
||||
<div class="page-sub muted">Rubric library, evolution over time, and per-rubric rollout grid — three views over one scoring event log.</div>
|
||||
</div>
|
||||
<!-- Shared rubric selector. Populated on show() by
|
||||
renderer.js/evals-shared-selector from window.__dshRubricFusion.
|
||||
Empty by default so first paint is stable and the "all rubrics"
|
||||
scope is honest. Emits `dsh:evals-rubric-change` when the user
|
||||
picks — each inner page listens and re-scopes its view. -->
|
||||
<div class="evals-shared-selector" role="group" aria-label="Shared rubric selector">
|
||||
<label class="evals-shared-label muted small" for="evals-shared-rubric">Rubric focus</label>
|
||||
<select id="evals-shared-rubric" class="evals-shared-select"
|
||||
title="Filter Rubrics catalog · Growth curves · Runtime grid to one rubric. 'All rubrics' keeps the default per-page view.">
|
||||
<option value="">All rubrics</option>
|
||||
</select>
|
||||
</div>
|
||||
</header>
|
||||
<!-- Inner tab strip. Only one .evals-tab-pane is visible at a time;
|
||||
visibility is driven by [data-evals-active] on this .pane
|
||||
(CSS in style.css matches the attribute so no JS querySelector
|
||||
per pane is needed). Rubrics is the default because it's the
|
||||
catalog entry point new researchers see first. -->
|
||||
<div class="evals-tab-strip" role="tablist" aria-label="Evals view">
|
||||
<button type="button" class="evals-tab active" data-evals-tab="rubrics"
|
||||
role="tab" aria-selected="true"
|
||||
title="Catalog of scoring rubrics grouped by task class.">Rubrics</button>
|
||||
<button type="button" class="evals-tab" data-evals-tab="growth"
|
||||
role="tab" aria-selected="false"
|
||||
title="Score curves over harness versions / models / data mixes.">Growth</button>
|
||||
<button type="button" class="evals-tab" data-evals-tab="runtime"
|
||||
role="tab" aria-selected="false"
|
||||
title="Rollout × rubric-dim red/green grid for the current runtime.">Runtime</button>
|
||||
</div>
|
||||
<div class="evals-tab-body">
|
||||
<!-- Rubrics sub-pane (task #188) — catalog-first evaluator library.
|
||||
Rendered by src/renderer/rubrics-page.js; the pure model lives at
|
||||
rubrics-model.js and the fixture blobs are inlined via
|
||||
rubrics-seed.js. The detail drawer sits fixed to the right; the
|
||||
annotation flow (task #191) lives in annotation-panel.js and is
|
||||
reachable from Chat header + Recent list rows. -->
|
||||
<section class="pane" data-pane="rubrics" hidden>
|
||||
reachable from Chat header + Recent list rows. Kept as
|
||||
data-pane="rubrics" so page controllers that query it by that
|
||||
selector still bind; the class no longer includes 'pane' because
|
||||
only the outer wrapper is the main-child pane. -->
|
||||
<section class="evals-tab-pane" data-pane="rubrics" data-evals-tab-pane="rubrics">
|
||||
<header class="header">
|
||||
<div class="header-lead">
|
||||
<div class="page-title">
|
||||
@@ -976,10 +1016,15 @@
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
<!-- Growth pane. Rendered by src/renderer/growth-v2.js — the
|
||||
compact-window "harness evolution log" for researchers. See the
|
||||
header of growth-v2.js for pane structure + data source. -->
|
||||
<section class="pane" data-pane="growth" hidden>
|
||||
<!-- Growth sub-pane (2026-07-19, lane-evals-merge). Rendered by
|
||||
src/renderer/growth-v2.js — the compact-window "harness evolution
|
||||
log" for researchers. Section keeps data-pane="growth" so
|
||||
growth-v2.js's `document.querySelector('.pane[data-pane="growth"]')`
|
||||
still binds; the .pane class is preserved for the same reason
|
||||
(growth-v2.js scopes CSS on `.pane[data-pane="growth"]`). The
|
||||
outer .pane[data-pane="evals"] toggles overall visibility; this
|
||||
section shows only when data-evals-active="growth" via CSS. -->
|
||||
<section class="pane evals-tab-pane" data-pane="growth" data-evals-tab-pane="growth" hidden>
|
||||
<header class="header">
|
||||
<div class="header-lead">
|
||||
<div class="page-title">Growth</div>
|
||||
@@ -1020,14 +1065,17 @@
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<!-- Runtimes pane (task #189 / IA §3 Runtimes one-pager). Rendered by
|
||||
src/renderer/runtimes-page.js on switchTo('runtimes'). Body has
|
||||
two sections: the profile list (one row per registered profile,
|
||||
expandable into a capability matrix) and the isolated-daemon
|
||||
section (any Playground scratch daemon currently up). The
|
||||
legend chip up top names the wire seam this composes locally
|
||||
until `runtime/list` (G8) lands. -->
|
||||
<section class="pane" data-pane="runtimes" hidden id="runtimes-pane">
|
||||
<!-- Runtimes sub-pane (task #189 / IA §3 Runtimes one-pager), now
|
||||
the Runtime tab inside the Evals pane. Rendered by
|
||||
src/renderer/runtimes-page.js on switchTo('evals') + Runtime tab
|
||||
active. Body has two sections: the profile list (one row per
|
||||
registered profile, expandable into a capability matrix) and
|
||||
the isolated-daemon section (any Playground scratch daemon
|
||||
currently up). Section keeps data-pane="runtimes" + id so
|
||||
runtimes-page.js's `document.getElementById('runtimes-pane')`
|
||||
still finds it. .pane class preserved so the CSS geometry
|
||||
(flex column + min-height) still applies. -->
|
||||
<section class="pane evals-tab-pane" data-pane="runtimes" data-evals-tab-pane="runtime" hidden id="runtimes-pane">
|
||||
<header class="header">
|
||||
<div class="header-lead">
|
||||
<div class="page-title">Runtimes</div>
|
||||
@@ -1044,6 +1092,8 @@
|
||||
<div class="runtimes-isolated" data-runtimes-isolated></div>
|
||||
</section>
|
||||
</section>
|
||||
</div><!-- /.evals-tab-body -->
|
||||
</section><!-- /.pane[data-pane="evals"] -->
|
||||
|
||||
<!-- Settings pane (task #193). Two sections: Model pricing (editable
|
||||
table seeded from price-table.js DEFAULT_PRICE_TABLE, edits
|
||||
|
||||
@@ -22,6 +22,19 @@
|
||||
|
||||
const DEFAULT_HIDDEN = Object.freeze(['playground-shim', 'mission'])
|
||||
|
||||
// Legacy ids that were merged into the single 'evals' door on 2026-07-19
|
||||
// (lane-evals-merge). Any of these appearing in a user's persisted
|
||||
// hiddenPages array is remapped so their old config keeps hiding the
|
||||
// Evals door — otherwise a user who previously chose "hide Rubrics"
|
||||
// would see Evals reappear on the next launch, effectively rolling
|
||||
// their preference back. The keys must match the ORIGINAL data-tab
|
||||
// values as they existed pre-merge; the value is the new door id.
|
||||
const LEGACY_ID_ALIAS = Object.freeze({
|
||||
rubrics: 'evals',
|
||||
growth: 'evals',
|
||||
runtimes: 'evals',
|
||||
})
|
||||
|
||||
// Page ids that a user can opt into from the Settings > Optional pages
|
||||
// section. Kept as a small explicit list rather than "everything in the
|
||||
// default hidden set" because the Settings section is meant to be a
|
||||
@@ -44,7 +57,19 @@ function resolveHiddenPages(cfg) {
|
||||
// every optional page in. Filter out non-strings/blanks defensively so
|
||||
// a malformed entry can't crash the renderer filter.
|
||||
const cleaned = raw.filter((x) => typeof x === 'string' && x.length > 0)
|
||||
return cleaned
|
||||
// Remap legacy ids merged into 'evals' (lane-evals-merge, 2026-07-19).
|
||||
// Deduplicates so a config that hides all three of rubrics/growth/
|
||||
// runtimes still yields a single 'evals' entry in the resolved set.
|
||||
const out = []
|
||||
const seen = new Set()
|
||||
for (const id of cleaned) {
|
||||
const mapped = LEGACY_ID_ALIAS[id] || id
|
||||
if (!seen.has(mapped)) {
|
||||
seen.add(mapped)
|
||||
out.push(mapped)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Small helper the Settings page uses to compute the next hiddenPages
|
||||
@@ -57,7 +82,7 @@ function toggleOptionalPage(current, pageId, enable) {
|
||||
return Array.from(set)
|
||||
}
|
||||
|
||||
const navConfigApi = { DEFAULT_HIDDEN, OPTIONAL_PAGES, resolveHiddenPages, toggleOptionalPage }
|
||||
const navConfigApi = { DEFAULT_HIDDEN, OPTIONAL_PAGES, LEGACY_ID_ALIAS, resolveHiddenPages, toggleOptionalPage }
|
||||
if (typeof module !== 'undefined' && module.exports) module.exports = navConfigApi
|
||||
if (typeof window !== 'undefined') window.__dshNavConfigModel = navConfigApi
|
||||
|
||||
|
||||
@@ -7436,6 +7436,27 @@ if (runtimesRefreshBtn) {
|
||||
})
|
||||
}
|
||||
|
||||
// Evals sidebar rubric-refresh + growth-refresh (lane-evals-merge,
|
||||
// 2026-07-19). These ids used to belong to per-page sidebars; after
|
||||
// the merge they live in the shared Evals sidebar-foot. Forwarding
|
||||
// from here keeps the page controllers unchanged.
|
||||
const evalsRubricsRefreshBtn = document.getElementById('rubrics-refresh')
|
||||
if (evalsRubricsRefreshBtn) {
|
||||
evalsRubricsRefreshBtn.addEventListener('click', () => {
|
||||
if (window.__dshRubrics && typeof window.__dshRubrics.refresh === 'function') {
|
||||
void window.__dshRubrics.refresh()
|
||||
}
|
||||
})
|
||||
}
|
||||
const evalsGrowthRefreshBtn = document.getElementById('growth-refresh')
|
||||
if (evalsGrowthRefreshBtn) {
|
||||
evalsGrowthRefreshBtn.addEventListener('click', () => {
|
||||
if (window.__dshGrowthV2 && typeof window.__dshGrowthV2.show === 'function') {
|
||||
void window.__dshGrowthV2.show()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Populate profile selector and reflect current status.
|
||||
async function bootUi() {
|
||||
// instantiate the subagent lineage store once
|
||||
@@ -7626,8 +7647,34 @@ async function bootUi() {
|
||||
// sibling modules (plugins-ui.js calls `switchTo` after vibe).
|
||||
const tabButtons = document.querySelectorAll('.tab-btn')
|
||||
const tabPanels = document.querySelectorAll('[data-tab-panel]')
|
||||
const mainPanes = document.querySelectorAll('.main .pane')
|
||||
// Direct-child pane list, not descendants: the Evals pane
|
||||
// (lane-evals-merge) nests three sub-panes that still carry .pane +
|
||||
// data-pane, and iterating descendants would let their `hidden`
|
||||
// toggle fight the outer wrapper's. Direct child scope means the
|
||||
// outer Evals pane is the only main-level match; inner sub-panes are
|
||||
// managed by the tab strip below.
|
||||
const mainEl = document.querySelector('.main')
|
||||
const mainPanes = mainEl ? mainEl.querySelectorAll(':scope > .pane') : document.querySelectorAll('.main > .pane')
|
||||
// Legacy tab id → new evals tab mapping. Callers that still pass
|
||||
// 'rubrics' / 'growth' / 'runtimes' land on the Evals pane with the
|
||||
// correct inner tab active — no dead nav routes after the merge.
|
||||
const EVALS_TAB_ALIAS = Object.freeze({
|
||||
rubrics: 'rubrics',
|
||||
growth: 'growth',
|
||||
runtimes: 'runtime',
|
||||
})
|
||||
function switchTo(name) {
|
||||
// Alias legacy ids to the new evals door + inner tab. Keeps every
|
||||
// preexisting switchTo('rubrics'|'growth'|'runtimes') call site
|
||||
// wired without hunting them all down.
|
||||
if (EVALS_TAB_ALIAS[name]) {
|
||||
const innerTab = EVALS_TAB_ALIAS[name]
|
||||
const originalName = name
|
||||
name = 'evals'
|
||||
// Pre-set the inner tab so the show() branch below activates it.
|
||||
state._pendingEvalsTab = innerTab
|
||||
state._evalsLegacyAlias = originalName
|
||||
}
|
||||
// Bug D layer 5 (2026-07-18, team-lead directive from layout-audit
|
||||
// 187824e): three drawers historically stayed open across tab
|
||||
// switches — `.fork-compare-drawer`, `.playground-compare-drawer`,
|
||||
@@ -7676,17 +7723,38 @@ async function bootUi() {
|
||||
// one tab is reflected the next time the researcher visits Hub.
|
||||
void window.__dshHub.show()
|
||||
}
|
||||
if (name === 'growth' && window.__dshGrowthV2) {
|
||||
// Growth v2: reads the compact-window history fixture +
|
||||
// any user-written rubrics/errors under ~/.dsh/growth/. No session-
|
||||
// list dependency — the page owns its own data source now.
|
||||
void window.__dshGrowthV2.show()
|
||||
}
|
||||
if (name === 'runtimes' && window.__dshRuntimes) {
|
||||
// local composition — reads listProfiles + runtimeStatus
|
||||
// + serverCapabilities + Playground list. Async so the profile
|
||||
// fetch can complete before the row list paints.
|
||||
void window.__dshRuntimes.show()
|
||||
if (name === 'evals') {
|
||||
// Evals door (lane-evals-merge, 2026-07-19). Mounts the three inner
|
||||
// pages that share the rubric scoring event log:
|
||||
// - Rubrics catalog (rubrics-page.js)
|
||||
// - Growth timeline (growth-v2.js)
|
||||
// - Runtime rollout grid (runtimes-page.js)
|
||||
// Each sub-page's show() paints into its own [data-pane] section;
|
||||
// the tab strip decides which one the researcher sees. All three
|
||||
// are called on entry so switching tabs is a visibility toggle
|
||||
// (no re-mount cost). Pending-tab from the legacy-alias branch or
|
||||
// the shared-selector's last pick overrides the default 'rubrics'
|
||||
// when set. mountEvalsTabStrip() is idempotent and installs the
|
||||
// tab click + shared selector listeners on first switch.
|
||||
const evalsPane = document.querySelector('.pane[data-pane="evals"]')
|
||||
mountEvalsTabStrip(evalsPane)
|
||||
populateEvalsSharedSelector()
|
||||
const pending = state._pendingEvalsTab
|
||||
state._pendingEvalsTab = null
|
||||
const initialTab = pending || (evalsPane && evalsPane.dataset.evalsActive) || 'rubrics'
|
||||
setEvalsActiveTab(evalsPane, initialTab)
|
||||
// Mount all three so the first tab flip after this doesn't have to
|
||||
// wait on a fetch/paint. Rubrics is synchronous; Growth+Runtimes
|
||||
// fire-and-forget.
|
||||
if (window.__dshRubrics && typeof window.__dshRubrics.show === 'function') {
|
||||
try { window.__dshRubrics.show() } catch (_) { /* defensive: never let one page crash the door */ }
|
||||
}
|
||||
if (window.__dshGrowthV2 && typeof window.__dshGrowthV2.show === 'function') {
|
||||
try { void window.__dshGrowthV2.show() } catch (_) {}
|
||||
}
|
||||
if (window.__dshRuntimes && typeof window.__dshRuntimes.show === 'function') {
|
||||
try { void window.__dshRuntimes.show() } catch (_) {}
|
||||
}
|
||||
}
|
||||
if (name === 'settings' && window.__dshSettings) {
|
||||
// pricing table + key-presence chart. Reads the
|
||||
@@ -7694,12 +7762,10 @@ async function bootUi() {
|
||||
// and merges any localStorage overrides via settings-model.
|
||||
void window.__dshSettings.show()
|
||||
}
|
||||
if (name === 'rubrics' && window.__dshRubrics) {
|
||||
// Rubrics catalog. Reads fixture SKILL.md blobs via
|
||||
// window.__dshRubricsSeed; user overlay rubrics from .dsh/rubrics/
|
||||
// wait on the G1 library/put seam.
|
||||
window.__dshRubrics.show()
|
||||
}
|
||||
// The legacy 'rubrics' / 'growth' / 'runtimes' branches were folded
|
||||
// into the 'evals' door above (2026-07-19, lane-evals-merge). The
|
||||
// EVALS_TAB_ALIAS lookup at the top of switchTo() remaps any lingering
|
||||
// caller to 'evals' with the correct inner tab pre-set.
|
||||
if (name === 'bench' && window.__dshBench) {
|
||||
// Bench (#187): local-lite researcher experiment platform. The page
|
||||
// owns its own data source (inlined fixture batch); no session-list
|
||||
@@ -7758,7 +7824,88 @@ async function bootUi() {
|
||||
}
|
||||
switchTo(tab)
|
||||
})
|
||||
window.__dshTabs = { switchTo }
|
||||
window.__dshTabs = { switchTo, EVALS_TAB_ALIAS }
|
||||
|
||||
// --- Evals pane machinery (lane-evals-merge, 2026-07-19) --------------
|
||||
// Three helpers install the inner tab strip, the shared rubric picker,
|
||||
// and the active-tab visibility toggle. Kept in renderer.js because
|
||||
// they need to reach into window.__dshRubricFusion + the sub-page show()
|
||||
// functions without introducing a new script file (keeps the load-
|
||||
// order graph in index.html unchanged, gates cleaner).
|
||||
let _evalsTabStripMounted = false
|
||||
function mountEvalsTabStrip(paneEl) {
|
||||
if (_evalsTabStripMounted || !paneEl) return
|
||||
const strip = paneEl.querySelector('.evals-tab-strip')
|
||||
if (!strip) return
|
||||
strip.addEventListener('click', (ev) => {
|
||||
const btn = ev.target.closest('[data-evals-tab]')
|
||||
if (!btn) return
|
||||
const tab = btn.dataset.evalsTab
|
||||
setEvalsActiveTab(paneEl, tab)
|
||||
})
|
||||
// Shared selector emits a custom event so each sub-page can react
|
||||
// without a direct dependency on renderer.js internals. Fires once
|
||||
// on mount so first paint sees the current pick.
|
||||
const sel = paneEl.querySelector('#evals-shared-rubric')
|
||||
if (sel) {
|
||||
sel.addEventListener('change', () => {
|
||||
const rubricId = sel.value || null
|
||||
try {
|
||||
window.dispatchEvent(new CustomEvent('dsh:evals-rubric-change', { detail: { rubricId } }))
|
||||
} catch (_) { /* CustomEvent absent (older Electron) — swallow */ }
|
||||
})
|
||||
}
|
||||
_evalsTabStripMounted = true
|
||||
}
|
||||
function setEvalsActiveTab(paneEl, tab) {
|
||||
if (!paneEl || !tab) return
|
||||
paneEl.dataset.evalsActive = tab
|
||||
for (const b of paneEl.querySelectorAll('.evals-tab')) {
|
||||
const on = b.dataset.evalsTab === tab
|
||||
b.classList.toggle('active', on)
|
||||
b.setAttribute('aria-selected', on ? 'true' : 'false')
|
||||
}
|
||||
for (const p of paneEl.querySelectorAll('.evals-tab-body > .evals-tab-pane')) {
|
||||
p.hidden = p.dataset.evalsTabPane !== tab
|
||||
}
|
||||
// When the Runtime tab activates, kick a repaint — the page may
|
||||
// have painted while hidden and its measured metrics could be zero.
|
||||
if (tab === 'runtime' && window.__dshRuntimes && typeof window.__dshRuntimes.refresh === 'function') {
|
||||
try { void window.__dshRuntimes.refresh() } catch (_) {}
|
||||
}
|
||||
}
|
||||
function populateEvalsSharedSelector() {
|
||||
const sel = document.getElementById('evals-shared-rubric')
|
||||
if (!sel) return
|
||||
const fusion = window.__dshRubricFusion
|
||||
if (!fusion || typeof fusion.listRubrics !== 'function') return
|
||||
// Seed the fusion store once if the sub-pages haven't gotten to it
|
||||
// yet (opening Evals as the first surface). Uses the same fixture
|
||||
// ref they read from — the store's WeakSet dedupe means repeated
|
||||
// seeds are idempotent.
|
||||
if (typeof fusion.loadFixture === 'function' && window.__dshRubricFusionSeed) {
|
||||
try { fusion.loadFixture(window.__dshRubricFusionSeed) } catch (_) {}
|
||||
}
|
||||
const rubrics = fusion.listRubrics()
|
||||
const currentValue = sel.value
|
||||
// Rebuild the option list. Preserves "All rubrics" as the sentinel
|
||||
// first option; the sub-pages treat null/"" as "no shared filter".
|
||||
sel.innerHTML = ''
|
||||
const allOpt = document.createElement('option')
|
||||
allOpt.value = ''
|
||||
allOpt.textContent = 'All rubrics'
|
||||
sel.appendChild(allOpt)
|
||||
for (const r of rubrics) {
|
||||
const opt = document.createElement('option')
|
||||
opt.value = r.id
|
||||
opt.textContent = r.group ? `${r.group} · ${r.name}` : r.name
|
||||
sel.appendChild(opt)
|
||||
}
|
||||
// Restore the previous pick if the id still exists.
|
||||
if (currentValue && rubrics.some(r => r.id === currentValue)) {
|
||||
sel.value = currentValue
|
||||
}
|
||||
}
|
||||
|
||||
// Left-nav hidden-pages filter (lane-nav-optional). Reads the shell
|
||||
// config's `hiddenPages` array through the nav IPC and toggles a
|
||||
|
||||
@@ -690,6 +690,25 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Shared Evals-pane rubric selector (lane-evals-merge, 2026-07-19).
|
||||
// When the researcher picks a rubric from the top selector, open that
|
||||
// rubric's detail drawer so the catalog view reflects the shared pick.
|
||||
// "All rubrics" (empty value) closes any open drawer to reset scope.
|
||||
if (typeof window !== 'undefined' && typeof window.addEventListener === 'function') {
|
||||
window.addEventListener('dsh:evals-rubric-change', (ev) => {
|
||||
const rid = ev && ev.detail && ev.detail.rubricId
|
||||
if (!rid) {
|
||||
try { closeDetail() } catch (_) {}
|
||||
return
|
||||
}
|
||||
// Only act when the Rubrics tab is the active Evals tab, otherwise
|
||||
// opening the drawer would flash behind another tab's body.
|
||||
const evalsPane = document.querySelector('.pane[data-pane="evals"]')
|
||||
if (evalsPane && evalsPane.dataset.evalsActive !== 'rubrics') return
|
||||
try { openDetail(rid) } catch (_) {}
|
||||
})
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.__dshRubrics = { mount, show, refresh, renderCatalog, openDetail, closeDetail, openCreateForm, closeCreateForm, _state: state }
|
||||
if (document.readyState === 'loading') {
|
||||
|
||||
@@ -29,6 +29,12 @@
|
||||
const rootId = 'runtimes-pane'
|
||||
let currentTab = 'rubric-grid'
|
||||
let fusionSeeded = false
|
||||
// Shared Evals-pane rubric selector filter (lane-evals-merge). null =
|
||||
// "All rubrics"; a rubric id scopes the grid to a single card so a
|
||||
// researcher can compare that rubric's rollout matrix in isolation
|
||||
// when the Rubrics tab is showing its detail and Runtime tab is
|
||||
// still hydrating the same choice.
|
||||
let sharedRubricFilter = null
|
||||
|
||||
function fusion() {
|
||||
return typeof window !== 'undefined' ? window.__dshRubricFusion : null
|
||||
@@ -378,11 +384,17 @@
|
||||
host.appendChild(muted('Rubric fusion store not loaded.'))
|
||||
return
|
||||
}
|
||||
const rubrics = f.listRubrics()
|
||||
let rubrics = f.listRubrics()
|
||||
if (!rubrics.length) {
|
||||
host.appendChild(muted('No rubrics registered. Author one under Rubrics → Create from scratch, or load the fusion fixture.'))
|
||||
return
|
||||
}
|
||||
// If the shared Evals selector picked a rubric, scope the grid to
|
||||
// that one. Otherwise show a card per rubric (default behaviour).
|
||||
if (sharedRubricFilter) {
|
||||
const scoped = rubrics.filter(r => r.id === sharedRubricFilter)
|
||||
if (scoped.length) rubrics = scoped
|
||||
}
|
||||
// Header row: one card per rubric.
|
||||
for (const rubric of rubrics) {
|
||||
const grid = f.rolloutGridFor(rubric.id, null)
|
||||
@@ -491,6 +503,24 @@
|
||||
return s
|
||||
}
|
||||
|
||||
// Shared Evals-pane rubric selector (lane-evals-merge, 2026-07-19).
|
||||
// When the researcher picks a rubric from the top selector, scope
|
||||
// the rollout grid to that rubric only; "All rubrics" restores the
|
||||
// per-rubric card view.
|
||||
if (typeof window !== 'undefined' && typeof window.addEventListener === 'function') {
|
||||
window.addEventListener('dsh:evals-rubric-change', (ev) => {
|
||||
const rid = ev && ev.detail && ev.detail.rubricId
|
||||
sharedRubricFilter = rid || null
|
||||
// Only repaint when the Runtime tab is what the user is looking
|
||||
// at; otherwise the next setEvalsActiveTab('runtime') call in
|
||||
// renderer.js triggers refresh() and the filter takes effect.
|
||||
const evalsPane = document.querySelector('.pane[data-pane="evals"]')
|
||||
if (evalsPane && evalsPane.dataset.evalsActive === 'runtime') {
|
||||
try { void show() } catch (_) {}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.__dshRuntimes = { show, refresh: () => show() }
|
||||
}
|
||||
|
||||
@@ -12971,3 +12971,85 @@ button.artifact-version:hover {
|
||||
.tool-block:not([open]) > summary .tool-edit-rerun-trigger {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Evals pane (lane-evals-merge, 2026-07-19)
|
||||
* Three former panes — Rubrics, Growth, Runtimes — collapse into one
|
||||
* .pane[data-pane="evals"]. The outer .pane[data-pane="evals"] flex column
|
||||
* layout is inherited from `.main .pane`. Inside, header + tab strip sit
|
||||
* at the top; only one .evals-tab-pane is rendered at a time based on
|
||||
* [data-evals-active] on the outer .pane.
|
||||
*
|
||||
* The inner .evals-tab-pane sections keep their `.pane` class so
|
||||
* page-scoped CSS in this file (.pane[data-pane="growth"] {…} etc.)
|
||||
* still applies without a rewrite. The `hidden` attribute is what
|
||||
* hides inactive tabs; JS in renderer.js flips it on tab click. */
|
||||
.pane[data-pane="evals"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
.pane[data-pane="evals"] > .evals-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.evals-shared-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.evals-shared-label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.evals-shared-select {
|
||||
min-width: 180px;
|
||||
padding: 4px 8px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
font: inherit;
|
||||
}
|
||||
.evals-tab-strip {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
padding: 0 20px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.evals-tab {
|
||||
appearance: none;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
font: inherit;
|
||||
padding: 8px 14px;
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
.evals-tab:hover {
|
||||
color: var(--fg);
|
||||
}
|
||||
.evals-tab.active {
|
||||
color: var(--fg);
|
||||
border-bottom-color: var(--accent);
|
||||
}
|
||||
.evals-tab-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
.evals-tab-body > .evals-tab-pane {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
/* When the pane is hidden the browser handles it via [hidden]; when a
|
||||
* tab is inactive its inner section is hidden the same way. */
|
||||
.evals-tab-body > .evals-tab-pane[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -37,8 +37,37 @@ test('resolveHiddenPages: empty array → show everything', () => {
|
||||
|
||||
test('resolveHiddenPages: custom list → honored as-is', () => {
|
||||
assert.deepStrictEqual(
|
||||
M.resolveHiddenPages({ hiddenPages: ['prs', 'growth'] }),
|
||||
['prs', 'growth']
|
||||
M.resolveHiddenPages({ hiddenPages: ['prs', 'bench'] }),
|
||||
['prs', 'bench']
|
||||
)
|
||||
})
|
||||
|
||||
test('resolveHiddenPages: legacy rubrics/growth/runtimes ids remap to evals (lane-evals-merge)', () => {
|
||||
// 2026-07-19: three separate nav items were merged into one Evals
|
||||
// door. Old config values must keep hiding what the user asked for —
|
||||
// if they'd hidden Rubrics before, they should still see no Rubrics
|
||||
// surface after upgrade (which means Evals stays hidden).
|
||||
assert.deepStrictEqual(
|
||||
M.resolveHiddenPages({ hiddenPages: ['rubrics'] }),
|
||||
['evals']
|
||||
)
|
||||
assert.deepStrictEqual(
|
||||
M.resolveHiddenPages({ hiddenPages: ['growth'] }),
|
||||
['evals']
|
||||
)
|
||||
assert.deepStrictEqual(
|
||||
M.resolveHiddenPages({ hiddenPages: ['runtimes'] }),
|
||||
['evals']
|
||||
)
|
||||
// Multiple legacy ids collapse to a single 'evals' entry.
|
||||
assert.deepStrictEqual(
|
||||
M.resolveHiddenPages({ hiddenPages: ['rubrics', 'growth', 'runtimes'] }),
|
||||
['evals']
|
||||
)
|
||||
// Legacy ids mixed with unrelated ids preserve the unrelated ids.
|
||||
assert.deepStrictEqual(
|
||||
M.resolveHiddenPages({ hiddenPages: ['prs', 'growth', 'bench'] }),
|
||||
['prs', 'evals', 'bench']
|
||||
)
|
||||
})
|
||||
|
||||
@@ -50,8 +79,8 @@ test('resolveHiddenPages: non-array garbage → falls back to defaults (safe)',
|
||||
|
||||
test('resolveHiddenPages: filters out non-string / blank entries', () => {
|
||||
assert.deepStrictEqual(
|
||||
M.resolveHiddenPages({ hiddenPages: ['prs', '', null, 42, 'growth'] }),
|
||||
['prs', 'growth']
|
||||
M.resolveHiddenPages({ hiddenPages: ['prs', '', null, 42, 'bench'] }),
|
||||
['prs', 'bench']
|
||||
)
|
||||
})
|
||||
|
||||
@@ -219,13 +248,13 @@ function makeSidebar() {
|
||||
makeBtn('hub'),
|
||||
makeBtn('bench'),
|
||||
])
|
||||
// "runtime" group — mission (default hidden) sits here.
|
||||
// "runtime" group — mission (default hidden) sits here. Post
|
||||
// lane-evals-merge (2026-07-19) rubrics/growth/runtimes fold into
|
||||
// a single 'evals' button; the sidebar shim mirrors that new shape.
|
||||
makeGroup([
|
||||
makeBtn('rubrics'),
|
||||
makeBtn('evals'),
|
||||
makeBtn('plugins'),
|
||||
makeBtn('runtimes'),
|
||||
makeBtn('mission'),
|
||||
makeBtn('growth'),
|
||||
makeBtn('prs'),
|
||||
])
|
||||
return { buttons, groups }
|
||||
@@ -263,11 +292,13 @@ test('DOM filter (case 2): empty array → nothing hidden (all pages show)', ()
|
||||
|
||||
test('DOM filter (case 3): custom list → matching buttons hidden, others untouched', () => {
|
||||
const sb = makeSidebar()
|
||||
// 'growth' remaps to 'evals' via LEGACY_ID_ALIAS (lane-evals-merge);
|
||||
// 'prs' passes through unchanged. Both target actual sidebar buttons.
|
||||
applyFilter(sb, { hiddenPages: ['prs', 'growth'] })
|
||||
const hidden = sb.buttons.filter((b) => b.classList.contains('nav-item--hidden'))
|
||||
assert.deepStrictEqual(
|
||||
hidden.map((b) => b.dataset.tab).sort(),
|
||||
['growth', 'prs']
|
||||
['evals', 'prs']
|
||||
)
|
||||
// Playground + mission are NOT hidden because the researcher opted them in
|
||||
// via an explicit list; only the ids in the list are hidden.
|
||||
|
||||
@@ -41,10 +41,28 @@ test('no lane still carries pending — all four slots flipped', () => {
|
||||
`all four coordinated slots should be flipped (found ${buttonPending.length} still-pending)`)
|
||||
})
|
||||
|
||||
test('Runtimes tab and pane exist', () => {
|
||||
assert.match(HTML, /data-tab="runtimes"/)
|
||||
assert.match(HTML, /data-pane="runtimes"/)
|
||||
assert.match(HTML, /id="runtimes-pane"/)
|
||||
test('Runtimes surface preserved inside the Evals pane', () => {
|
||||
// Post lane-evals-merge (2026-07-19), Runtimes is a tab inside the
|
||||
// Evals door, not its own top-level nav. The nav button is 'evals';
|
||||
// the runtimes-pane id + data-pane="runtimes" stay because
|
||||
// runtimes-page.js queries them by that selector.
|
||||
assert.match(HTML, /data-tab="evals"/, 'Evals nav button missing')
|
||||
assert.match(HTML, /data-pane="runtimes"/, 'runtimes sub-pane still keeps its data-pane hook')
|
||||
assert.match(HTML, /id="runtimes-pane"/, 'runtimes-pane id preserved for runtimes-page.js binding')
|
||||
assert.match(HTML, /data-evals-tab-pane="runtime"/, 'Runtime tab pane marker present')
|
||||
})
|
||||
|
||||
test('Evals door hosts Rubrics/Growth/Runtime as tabs', () => {
|
||||
// Sanity: the three sub-pane markers are present and reachable via
|
||||
// their evals-tab id. Guards against a future edit that silently
|
||||
// orphans a tab pane.
|
||||
assert.match(HTML, /data-evals-tab="rubrics"/, 'Rubrics tab button present')
|
||||
assert.match(HTML, /data-evals-tab="growth"/, 'Growth tab button present')
|
||||
assert.match(HTML, /data-evals-tab="runtime"/, 'Runtime tab button present')
|
||||
assert.match(HTML, /data-evals-tab-pane="rubrics"/, 'Rubrics tab pane present')
|
||||
assert.match(HTML, /data-evals-tab-pane="growth"/, 'Growth tab pane present')
|
||||
assert.match(HTML, /data-evals-tab-pane="runtime"/, 'Runtime tab pane present')
|
||||
assert.match(HTML, /id="evals-shared-rubric"/, 'Shared rubric selector present')
|
||||
})
|
||||
|
||||
test('Settings tab and pane exist', () => {
|
||||
|
||||
Reference in New Issue
Block a user