From f2494aa33f519f00a7011c8efb9781401c459787 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 7 Apr 2026 10:04:27 +0100 Subject: [PATCH] feat: streamline qa lab live runs --- .agents/skills/openclaw-qa-testing/SKILL.md | 1 - extensions/qa-lab/src/cli.runtime.ts | 2 - extensions/qa-lab/src/cli.ts | 4 - extensions/qa-lab/src/gateway-child.ts | 2 - extensions/qa-lab/src/lab-server.ts | 1 - extensions/qa-lab/src/model-selection.ts | 40 ++++ .../qa-lab/src/qa-gateway-config.test.ts | 1 - extensions/qa-lab/src/qa-gateway-config.ts | 42 ++-- extensions/qa-lab/src/run-config.test.ts | 2 +- extensions/qa-lab/src/run-config.ts | 37 ++-- extensions/qa-lab/src/suite.ts | 18 +- extensions/qa-lab/web/src/app.ts | 31 +-- extensions/qa-lab/web/src/styles.css | 142 +++++++++++++ extensions/qa-lab/web/src/ui-render.ts | 187 +++++++++++------- 14 files changed, 367 insertions(+), 143 deletions(-) create mode 100644 extensions/qa-lab/src/model-selection.ts diff --git a/.agents/skills/openclaw-qa-testing/SKILL.md b/.agents/skills/openclaw-qa-testing/SKILL.md index 837c15b1923..2739ee57b1a 100644 --- a/.agents/skills/openclaw-qa-testing/SKILL.md +++ b/.agents/skills/openclaw-qa-testing/SKILL.md @@ -39,7 +39,6 @@ pnpm openclaw qa suite \ --provider-mode live-openai \ --model openai/gpt-5.4 \ --alt-model openai/gpt-5.4 \ - --fast \ --output-dir .artifacts/qa-e2e/run-all-live-openai- ``` diff --git a/extensions/qa-lab/src/cli.runtime.ts b/extensions/qa-lab/src/cli.runtime.ts index f373c32f8c7..424a48f7399 100644 --- a/extensions/qa-lab/src/cli.runtime.ts +++ b/extensions/qa-lab/src/cli.runtime.ts @@ -47,14 +47,12 @@ export async function runQaSuiteCommand(opts: { providerMode?: "mock-openai" | "live-openai"; primaryModel?: string; alternateModel?: string; - fastMode?: boolean; }) { const result = await runQaSuite({ outputDir: opts.outputDir ? path.resolve(opts.outputDir) : undefined, providerMode: opts.providerMode, primaryModel: opts.primaryModel, alternateModel: opts.alternateModel, - fastMode: opts.fastMode, }); process.stdout.write(`QA suite watch: ${result.watchUrl}\n`); process.stdout.write(`QA suite report: ${result.reportPath}\n`); diff --git a/extensions/qa-lab/src/cli.ts b/extensions/qa-lab/src/cli.ts index 326b02f643d..b07b5a1861c 100644 --- a/extensions/qa-lab/src/cli.ts +++ b/extensions/qa-lab/src/cli.ts @@ -19,7 +19,6 @@ async function runQaSuite(opts: { providerMode?: "mock-openai" | "live-openai"; primaryModel?: string; alternateModel?: string; - fastMode?: boolean; }) { const runtime = await loadQaLabCliRuntime(); await runtime.runQaSuiteCommand(opts); @@ -96,21 +95,18 @@ export function registerQaLabCli(program: Command) { .option("--provider-mode ", "Provider mode: mock-openai or live-openai", "mock-openai") .option("--model ", "Primary provider/model ref") .option("--alt-model ", "Alternate provider/model ref") - .option("--fast", "Enable provider fast mode where supported", false) .action( async (opts: { outputDir?: string; providerMode?: "mock-openai" | "live-openai"; model?: string; altModel?: string; - fast?: boolean; }) => { await runQaSuite({ outputDir: opts.outputDir, providerMode: opts.providerMode, primaryModel: opts.model, alternateModel: opts.altModel, - fastMode: opts.fast, }); }, ); diff --git a/extensions/qa-lab/src/gateway-child.ts b/extensions/qa-lab/src/gateway-child.ts index aec36b64857..18433727149 100644 --- a/extensions/qa-lab/src/gateway-child.ts +++ b/extensions/qa-lab/src/gateway-child.ts @@ -122,7 +122,6 @@ export async function startQaGatewayChild(params: { providerMode?: "mock-openai" | "live-openai"; primaryModel?: string; alternateModel?: string; - fastMode?: boolean; controlUiEnabled?: boolean; }) { const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-qa-suite-")); @@ -156,7 +155,6 @@ export async function startQaGatewayChild(params: { providerMode: params.providerMode, primaryModel: params.primaryModel, alternateModel: params.alternateModel, - fastMode: params.fastMode, controlUiEnabled: params.controlUiEnabled, }); await fs.writeFile(configPath, `${JSON.stringify(cfg, null, 2)}\n`, "utf8"); diff --git a/extensions/qa-lab/src/lab-server.ts b/extensions/qa-lab/src/lab-server.ts index 5759ae3b30d..524f08eb6b0 100644 --- a/extensions/qa-lab/src/lab-server.ts +++ b/extensions/qa-lab/src/lab-server.ts @@ -696,7 +696,6 @@ export async function startQaLabServer(params?: { providerMode: selection.providerMode, primaryModel: selection.primaryModel, alternateModel: selection.alternateModel, - fastMode: selection.fastMode, scenarioIds: selection.scenarioIds, }); runnerSnapshot = { diff --git a/extensions/qa-lab/src/model-selection.ts b/extensions/qa-lab/src/model-selection.ts new file mode 100644 index 00000000000..20eb2e7d125 --- /dev/null +++ b/extensions/qa-lab/src/model-selection.ts @@ -0,0 +1,40 @@ +export type QaProviderMode = "mock-openai" | "live-openai"; + +export type QaModelSelection = { + primaryModel: string; + alternateModel: string; +}; + +export function defaultQaModelForMode( + mode: QaProviderMode, + options?: { + alternate?: boolean; + preferredLiveModel?: string; + }, +) { + if (mode === "live-openai") { + return options?.preferredLiveModel ?? "openai/gpt-5.4"; + } + return options?.alternate ? "mock-openai/gpt-5.4-alt" : "mock-openai/gpt-5.4"; +} + +export function splitQaModelRef(ref: string) { + const slash = ref.indexOf("/"); + if (slash <= 0 || slash === ref.length - 1) { + return null; + } + return { + provider: ref.slice(0, slash), + model: ref.slice(slash + 1), + }; +} + +export function isQaFastModeModelRef(ref: string) { + return splitQaModelRef(ref)?.provider === "openai"; +} + +export function isQaFastModeEnabled(selection: QaModelSelection) { + return ( + isQaFastModeModelRef(selection.primaryModel) || isQaFastModeModelRef(selection.alternateModel) + ); +} diff --git a/extensions/qa-lab/src/qa-gateway-config.test.ts b/extensions/qa-lab/src/qa-gateway-config.test.ts index bd461ca5ffd..01d8a5ce3e0 100644 --- a/extensions/qa-lab/src/qa-gateway-config.test.ts +++ b/extensions/qa-lab/src/qa-gateway-config.test.ts @@ -38,7 +38,6 @@ describe("buildQaGatewayConfig", () => { qaBusBaseUrl: "http://127.0.0.1:43124", workspaceDir: "/tmp/qa-workspace", providerMode: "live-openai", - fastMode: true, primaryModel: "openai/gpt-5.4", alternateModel: "openai/gpt-5.4", }); diff --git a/extensions/qa-lab/src/qa-gateway-config.ts b/extensions/qa-lab/src/qa-gateway-config.ts index b602d555647..d38645dc803 100644 --- a/extensions/qa-lab/src/qa-gateway-config.ts +++ b/extensions/qa-lab/src/qa-gateway-config.ts @@ -1,5 +1,11 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/core"; import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-shared"; +import { + defaultQaModelForMode, + isQaFastModeModelRef, + splitQaModelRef, + type QaProviderMode, +} from "./model-selection.js"; const DISABLED_BUNDLED_CHANNELS = Object.freeze({ bluebubbles: { enabled: false }, @@ -33,21 +39,10 @@ export function buildQaGatewayConfig(params: { controlUiRoot?: string; controlUiAllowedOrigins?: string[]; controlUiEnabled?: boolean; - providerMode?: "mock-openai" | "live-openai"; + providerMode?: QaProviderMode; primaryModel?: string; alternateModel?: string; - fastMode?: boolean; }): OpenClawConfig { - const splitModelRef = (ref: string) => { - const slash = ref.indexOf("/"); - if (slash <= 0 || slash === ref.length - 1) { - return null; - } - return { - provider: ref.slice(0, slash), - model: ref.slice(slash + 1), - }; - }; const mockProviderBaseUrl = params.providerBaseUrl ?? "http://127.0.0.1:44080/v1"; const mockOpenAiProvider: ModelProviderConfig = { baseUrl: mockProviderBaseUrl, @@ -102,12 +97,9 @@ export function buildQaGatewayConfig(params: { ], }; const providerMode = params.providerMode ?? "mock-openai"; - const primaryModel = - params.primaryModel ?? - (providerMode === "live-openai" ? "openai/gpt-5.4" : "mock-openai/gpt-5.4"); + const primaryModel = params.primaryModel ?? defaultQaModelForMode(providerMode); const alternateModel = - params.alternateModel ?? - (providerMode === "live-openai" ? "openai/gpt-5.4" : "mock-openai/gpt-5.4-alt"); + params.alternateModel ?? defaultQaModelForMode(providerMode, { alternate: true }); const imageGenerationModelRef = providerMode === "live-openai" ? "openai/gpt-image-1" : "mock-openai/gpt-image-1"; const selectedProviderIds = @@ -115,7 +107,7 @@ export function buildQaGatewayConfig(params: { ? [ ...new Set( [primaryModel, alternateModel, imageGenerationModelRef] - .map((ref) => splitModelRef(ref)?.provider) + .map((ref) => splitQaModelRef(ref)?.provider) .filter((provider): provider is string => Boolean(provider)), ), ] @@ -130,15 +122,15 @@ export function buildQaGatewayConfig(params: { : ["memory-core", "qa-channel"]; const liveModelParams = providerMode === "live-openai" - ? { + ? (modelRef: string) => ({ transport: "sse", openaiWsWarmup: false, - ...(params.fastMode ? { fastMode: true } : {}), - } - : { + ...(isQaFastModeModelRef(modelRef) ? { fastMode: true } : {}), + }) + : (_modelRef: string) => ({ transport: "sse", openaiWsWarmup: false, - }; + }); const allowedOrigins = params.controlUiAllowedOrigins && params.controlUiAllowedOrigins.length > 0 ? params.controlUiAllowedOrigins @@ -181,10 +173,10 @@ export function buildQaGatewayConfig(params: { }, models: { [primaryModel]: { - params: liveModelParams, + params: liveModelParams(primaryModel), }, [alternateModel]: { - params: liveModelParams, + params: liveModelParams(alternateModel), }, }, subagents: { diff --git a/extensions/qa-lab/src/run-config.test.ts b/extensions/qa-lab/src/run-config.test.ts index b13e32443a7..b78ad4f0843 100644 --- a/extensions/qa-lab/src/run-config.test.ts +++ b/extensions/qa-lab/src/run-config.test.ts @@ -49,7 +49,7 @@ describe("qa run config", () => { providerMode: "live-openai", primaryModel: "openai/gpt-5.4", alternateModel: "openai/gpt-5.4", - fastMode: false, + fastMode: true, scenarioIds: ["thread-lifecycle"], }); }); diff --git a/extensions/qa-lab/src/run-config.ts b/extensions/qa-lab/src/run-config.ts index 638715ecefe..1d54a6c0e59 100644 --- a/extensions/qa-lab/src/run-config.ts +++ b/extensions/qa-lab/src/run-config.ts @@ -1,8 +1,11 @@ import path from "node:path"; +import { + defaultQaModelForMode, + isQaFastModeEnabled, + type QaProviderMode, +} from "./model-selection.js"; import type { QaSeedScenario } from "./scenario-catalog.js"; -export type QaProviderMode = "mock-openai" | "live-openai"; - export type QaLabRunSelection = { providerMode: QaProviderMode; primaryModel: string; @@ -28,22 +31,18 @@ export type QaLabRunnerSnapshot = { }; export function createDefaultQaRunSelection(scenarios: QaSeedScenario[]): QaLabRunSelection { + const providerMode: QaProviderMode = "mock-openai"; + const primaryModel = defaultQaModelForMode(providerMode); + const alternateModel = defaultQaModelForMode(providerMode, { alternate: true }); return { - providerMode: "mock-openai", - primaryModel: "mock-openai/gpt-5.4", - alternateModel: "mock-openai/gpt-5.4-alt", - fastMode: false, + providerMode, + primaryModel, + alternateModel, + fastMode: isQaFastModeEnabled({ primaryModel, alternateModel }), scenarioIds: scenarios.map((scenario) => scenario.id), }; } -function defaultModelForMode(mode: QaProviderMode, alternate = false) { - if (mode === "live-openai") { - return "openai/gpt-5.4"; - } - return alternate ? "mock-openai/gpt-5.4-alt" : "mock-openai/gpt-5.4"; -} - function normalizeProviderMode(input: unknown): QaProviderMode { return input === "live-openai" ? "live-openai" : "mock-openai"; } @@ -72,12 +71,16 @@ export function normalizeQaRunSelection( ): QaLabRunSelection { const payload = input && typeof input === "object" ? (input as Record) : {}; const providerMode = normalizeProviderMode(payload.providerMode); + const primaryModel = normalizeModel(payload.primaryModel, defaultQaModelForMode(providerMode)); + const alternateModel = normalizeModel( + payload.alternateModel, + defaultQaModelForMode(providerMode, { alternate: true }), + ); return { providerMode, - primaryModel: normalizeModel(payload.primaryModel, defaultModelForMode(providerMode)), - alternateModel: normalizeModel(payload.alternateModel, defaultModelForMode(providerMode, true)), - fastMode: - typeof payload.fastMode === "boolean" ? payload.fastMode : providerMode === "live-openai", + primaryModel, + alternateModel, + fastMode: isQaFastModeEnabled({ primaryModel, alternateModel }), scenarioIds: normalizeScenarioIds(payload.scenarioIds, scenarios), }; } diff --git a/extensions/qa-lab/src/suite.ts b/extensions/qa-lab/src/suite.ts index 0158730948a..fb2eb65dcdf 100644 --- a/extensions/qa-lab/src/suite.ts +++ b/extensions/qa-lab/src/suite.ts @@ -14,6 +14,11 @@ import { startQaGatewayChild } from "./gateway-child.js"; import { startQaLabServer } from "./lab-server.js"; import type { QaLabLatestReport, QaLabScenarioOutcome } from "./lab-server.js"; import { startQaMockOpenAiServer } from "./mock-openai-server.js"; +import { + defaultQaModelForMode, + isQaFastModeEnabled, + type QaProviderMode, +} from "./model-selection.js"; import { renderQaMarkdownReport, type QaReportCheck, type QaReportScenario } from "./report.js"; import { qaChannelPlugin, type QaBusMessage } from "./runtime-api.js"; import { readQaBootstrapScenarioCatalog } from "./scenario-catalog.js"; @@ -1752,22 +1757,18 @@ When the user asks for the drift skill marker exactly, reply with exactly: DRIFT export async function runQaSuite(params?: { outputDir?: string; - providerMode?: "mock-openai" | "live-openai"; + providerMode?: QaProviderMode; primaryModel?: string; alternateModel?: string; - fastMode?: boolean; scenarioIds?: string[]; lab?: Awaited>; }) { const startedAt = new Date(); const providerMode = params?.providerMode ?? "mock-openai"; - const fastMode = params?.fastMode ?? providerMode === "live-openai"; - const primaryModel = - params?.primaryModel ?? - (providerMode === "live-openai" ? "openai/gpt-5.4" : "mock-openai/gpt-5.4"); + const primaryModel = params?.primaryModel ?? defaultQaModelForMode(providerMode); const alternateModel = - params?.alternateModel ?? - (providerMode === "live-openai" ? "openai/gpt-5.4" : "mock-openai/gpt-5.4-alt"); + params?.alternateModel ?? defaultQaModelForMode(providerMode, { alternate: true }); + const fastMode = isQaFastModeEnabled({ primaryModel, alternateModel }); const outputDir = params?.outputDir ?? path.join(process.cwd(), ".artifacts", "qa-e2e", `suite-${Date.now().toString(36)}`); @@ -1795,7 +1796,6 @@ export async function runQaSuite(params?: { providerMode, primaryModel, alternateModel, - fastMode, controlUiEnabled: true, }); lab.setControlUi({ diff --git a/extensions/qa-lab/web/src/app.ts b/extensions/qa-lab/web/src/app.ts index 141fbadb8e1..c3fae637e33 100644 --- a/extensions/qa-lab/web/src/app.ts +++ b/extensions/qa-lab/web/src/app.ts @@ -1,3 +1,4 @@ +import { defaultQaModelForMode, isQaFastModeEnabled } from "../../src/model-selection.js"; import { formatErrorMessage } from "./errors.js"; import { type Bootstrap, @@ -43,18 +44,22 @@ function defaultModelsForProviderMode( mode: RunnerSelection["providerMode"], bootstrap?: Bootstrap | null, ): Pick { + const preferredLiveModel = bootstrap?.runnerCatalog.real[0]?.key; if (mode === "live-openai") { - const preferred = bootstrap?.runnerCatalog.real[0]?.key; + const primaryModel = defaultQaModelForMode(mode, { preferredLiveModel }); + const alternateModel = defaultQaModelForMode(mode, { alternate: true, preferredLiveModel }); return { - primaryModel: preferred ?? "openai/gpt-5.4", - alternateModel: preferred ?? "openai/gpt-5.4", - fastMode: true, + primaryModel, + alternateModel, + fastMode: isQaFastModeEnabled({ primaryModel, alternateModel }), }; } + const primaryModel = defaultQaModelForMode(mode); + const alternateModel = defaultQaModelForMode(mode, { alternate: true }); return { - primaryModel: "mock-openai/gpt-5.4", - alternateModel: "mock-openai/gpt-5.4-alt", - fastMode: false, + primaryModel, + alternateModel, + fastMode: isQaFastModeEnabled({ primaryModel, alternateModel }), }; } @@ -320,7 +325,6 @@ export async function createQaLabApp(root: HTMLDivElement) { providerMode: state.runnerDraft.providerMode, primaryModel: state.runnerDraft.primaryModel, alternateModel: state.runnerDraft.alternateModel, - fastMode: state.runnerDraft.fastMode, scenarioIds: state.runnerDraft.scenarioIds, }, ); @@ -531,19 +535,20 @@ export async function createQaLabApp(root: HTMLDivElement) { ...defaultModelsForProviderMode(mode, state.bootstrap), })); }); - root.querySelector("#fast-mode")?.addEventListener("change", (e) => { - updateRunnerDraft((d) => ({ ...d, fastMode: (e.currentTarget as HTMLInputElement).checked })); - }); root.querySelector("#primary-model")?.addEventListener("change", (e) => { + const primaryModel = (e.currentTarget as HTMLSelectElement).value; updateRunnerDraft((d) => ({ ...d, - primaryModel: (e.currentTarget as HTMLSelectElement).value, + primaryModel, + fastMode: isQaFastModeEnabled({ primaryModel, alternateModel: d.alternateModel }), })); }); root.querySelector("#alternate-model")?.addEventListener("change", (e) => { + const alternateModel = (e.currentTarget as HTMLSelectElement).value; updateRunnerDraft((d) => ({ ...d, - alternateModel: (e.currentTarget as HTMLSelectElement).value, + alternateModel, + fastMode: isQaFastModeEnabled({ primaryModel: d.primaryModel, alternateModel }), })); }); diff --git a/extensions/qa-lab/web/src/styles.css b/extensions/qa-lab/web/src/styles.css index 199dc162b39..e6c95953a81 100644 --- a/extensions/qa-lab/web/src/styles.css +++ b/extensions/qa-lab/web/src/styles.css @@ -1116,6 +1116,17 @@ select { min-width: 0; } +.inspector-layout { + display: grid; + grid-template-columns: minmax(0, 1.45fr) minmax(280px, 0.85fr); + gap: 24px; + align-items: start; +} + +.inspector-main { + min-width: 0; +} + .inspector-empty { display: flex; align-items: center; @@ -1189,6 +1200,117 @@ select { margin-bottom: 10px; } +.inspector-live { + position: sticky; + top: 0; + display: flex; + flex-direction: column; + gap: 12px; + min-width: 0; +} + +.inspector-live-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; +} + +.inspector-live-subtitle { + color: var(--text-secondary); + font-size: 12px; + line-height: 1.45; +} + +.inspector-live-feed { + display: flex; + flex-direction: column; + gap: 10px; + max-height: calc(100vh - 220px); + overflow-y: auto; + padding-right: 4px; +} + +.inspector-live-message { + border: 1px solid var(--border); + border-radius: 12px; + background: var(--bg-surface); + padding: 12px 14px; + box-shadow: var(--shadow); +} + +.inspector-live-message-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + margin-bottom: 6px; +} + +.inspector-live-message-identity { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; +} + +.inspector-live-avatar { + width: 22px; + height: 22px; + border-radius: 999px; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 12px; + color: #fff; + flex-shrink: 0; +} + +.inspector-live-sender { + font-size: 12px; + font-weight: 700; + color: var(--text); +} + +.inspector-live-direction { + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.08em; + padding: 2px 6px; + border-radius: 999px; +} + +.inspector-live-direction-inbound { + color: var(--msg-inbound-accent); + background: var(--msg-inbound-badge); +} + +.inspector-live-direction-outbound { + color: var(--msg-outbound-accent); + background: var(--accent-soft); +} + +.inspector-live-time { + font-size: 11px; + color: var(--text-tertiary); + white-space: nowrap; +} + +.inspector-live-channel { + color: var(--text-tertiary); + font-size: 11px; + margin-bottom: 6px; +} + +.inspector-live-text { + color: var(--text); + font-size: 13px; + line-height: 1.5; + white-space: pre-wrap; + word-break: break-word; +} + .criteria-list { list-style: none; display: flex; @@ -1409,6 +1531,18 @@ select { .results-list { width: 280px; } + + .inspector-layout { + grid-template-columns: 1fr; + } + + .inspector-live { + position: static; + } + + .inspector-live-feed { + max-height: 360px; + } } @media (max-width: 700px) { @@ -1434,6 +1568,14 @@ select { border-bottom: 1px solid var(--border); } + .results-inspector { + padding: 16px; + } + + .inspector-live-feed { + max-height: 280px; + } + .chat-sidebar { width: 180px; } diff --git a/extensions/qa-lab/web/src/ui-render.ts b/extensions/qa-lab/web/src/ui-render.ts index 2677cc7f089..f06c4c9910f 100644 --- a/extensions/qa-lab/web/src/ui-render.ts +++ b/extensions/qa-lab/web/src/ui-render.ts @@ -354,9 +354,6 @@ function renderSidebar(state: UiState): string { options: modelOptions, disabled: isRunning, })} -
- -
${ selection?.providerMode === "live-openai" ? `
${esc( @@ -634,6 +631,57 @@ function renderMessage(m: Message): string {
`; } +function recentInspectorMessages(state: UiState, limit = 18) { + return (state.snapshot?.messages ?? []).slice(-limit).toReversed(); +} + +function renderInspectorLiveMessage(message: Message): string { + const avatar = messageAvatar(message); + const conversationLabel = message.conversation.title || message.conversation.id; + const threadLabel = message.threadTitle || message.threadId; + + return ` +
+
+
+ ${avatar.emoji} + ${esc(message.senderName || message.senderId)} + ${message.direction === "inbound" ? "inbound" : "outbound"} +
+ ${formatTime(message.timestamp)} +
+
+ ${esc(conversationLabel)}${threadLabel ? ` · ${esc(threadLabel)}` : ""} +
+
${esc(message.text)}
+
`; +} + +function renderInspectorLiveTranscript(state: UiState): string { + const messages = recentInspectorMessages(state); + const isLive = state.bootstrap?.runner.status === "running"; + + return ` + `; +} + /* ===== Render: Results tab ===== */ function renderResultsView(state: UiState): string { @@ -671,71 +719,76 @@ function renderInspector(state: UiState, scenario: SeedScenario): string { const outcome = findScenarioOutcome(state, scenario); return ` -
-
-
${esc(scenario.title)}
- ${badgeHtml(outcome?.status ?? "pending")} +
+
+
+
+
${esc(scenario.title)}
+ ${badgeHtml(outcome?.status ?? "pending")} +
+
+
${esc(scenario.objective)}
+
+
Surface${esc(scenario.surface)}
+
Started${esc(formatIso(outcome?.startedAt))}
+
Finished${esc(formatIso(outcome?.finishedAt))}
+
Run${esc(state.scenarioRun?.kind ?? "seed only")}
+
+ +
+
Success Criteria
+
    + ${scenario.successCriteria.map((c) => `
  • ${esc(c)}
  • `).join("")} +
+
+ +
+
Observed Outcome
+ ${ + outcome + ? ` + ${outcome.details ? `
${esc(outcome.details)}
` : ""} +
+ ${ + outcome.steps?.length + ? outcome.steps + .map( + (step) => ` +
+
+ ${esc(step.name)} + ${badgeHtml(step.status)} +
+ ${step.details ? `
${esc(step.details)}
` : ""} +
`, + ) + .join("") + : '
No step data yet.
' + } +
` + : '
Not executed yet — seed plan only.
' + } +
+ + ${ + scenario.docsRefs?.length + ? `
+
Docs
+
${scenario.docsRefs.map((r) => `${esc(r)}`).join("")}
+
` + : "" + } + ${ + scenario.codeRefs?.length + ? `
+
Code
+
${scenario.codeRefs.map((r) => `${esc(r)}`).join("")}
+
` + : "" + }
-
-
${esc(scenario.objective)}
-
-
Surface${esc(scenario.surface)}
-
Started${esc(formatIso(outcome?.startedAt))}
-
Finished${esc(formatIso(outcome?.finishedAt))}
-
Run${esc(state.scenarioRun?.kind ?? "seed only")}
-
- -
-
Success Criteria
-
    - ${scenario.successCriteria.map((c) => `
  • ${esc(c)}
  • `).join("")} -
-
- -
-
Observed Outcome
- ${ - outcome - ? ` - ${outcome.details ? `
${esc(outcome.details)}
` : ""} -
- ${ - outcome.steps?.length - ? outcome.steps - .map( - (step) => ` -
-
- ${esc(step.name)} - ${badgeHtml(step.status)} -
- ${step.details ? `
${esc(step.details)}
` : ""} -
`, - ) - .join("") - : '
No step data yet.
' - } -
` - : '
Not executed yet — seed plan only.
' - } -
- - ${ - scenario.docsRefs?.length - ? `
-
Docs
-
${scenario.docsRefs.map((r) => `${esc(r)}`).join("")}
-
` - : "" - } - ${ - scenario.codeRefs?.length - ? `
-
Code
-
${scenario.codeRefs.map((r) => `${esc(r)}`).join("")}
-
` - : "" - }`; + ${renderInspectorLiveTranscript(state)} +
`; } /* ===== Render: Report tab ===== */