/* ===== Shared types (unchanged from the bus protocol) ===== */
export type Conversation = {
id: string;
kind:
"direct" |
"channel" ;
title?: string;
};
export type Attachment = {
id: string;
kind:
"image" |
"video" |
"audio" |
"file" ;
mimeType: string;
fileName?: string;
inline?:
boolean ;
url?: string;
contentBase64?: string;
width?: number;
height?: number;
durationMs?: number;
altText?: string;
transcript?: string;
};
export type
Thread = {
id: string;
conversationId: string;
title: string;
};
export type Message = {
id: string;
direction:
"inbound" |
"outbound" ;
conversation: Conversation;
senderId: string;
senderName?: string;
text: string;
timestamp: number;
threadId?: string;
threadTitle?: string;
deleted?:
boolean ;
editedAt?: number;
attachments?: Attachment[];
reactions: Array<{ emoji: string; senderId: string }>;
};
export type BusEvent =
| { cursor: number; kind:
"thread-created" ;
thread :
Thread }
| { cursor: number; kind: string; message?: Message; emoji?: string };
export type Snapshot = {
conversations: Conversation[];
threads:
Thread [];
messages: Message[];
events: BusEvent[];
};
export type ReportEnvelope = {
report:
null | {
outputPath: string;
markdown: string;
generatedAt: string;
};
};
export type SeedScenario = {
id: string;
title: string;
surface: string;
objective: string;
successCriteria: string[];
docsRefs?: string[];
codeRefs?: string[];
};
export type Bootstrap = {
baseUrl: string;
latestReport: ReportEnvelope[
"report" ];
controlUiUrl: string |
null ;
controlUiEmbeddedUrl: string |
null ;
kickoffTask: string;
scenarios: SeedScenario[];
defaults: {
conversationKind:
"direct" |
"channel" ;
conversationId: string;
senderId: string;
senderName: string;
};
runner: RunnerSnapshot;
runnerCatalog: {
status:
"loading" |
"ready" |
"failed" ;
real: RunnerModelOption[];
};
};
export type ScenarioStep = {
name: string;
status:
"pass" |
"fail" |
"skip" ;
details?: string;
};
export type ScenarioOutcome = {
id: string;
name: string;
status:
"pending" |
"running" |
"pass" |
"fail" |
"skip" ;
details?: string;
steps?: ScenarioStep[];
startedAt?: string;
finishedAt?: string;
};
export type ScenarioRun = {
kind:
"suite" |
"self-check" ;
status:
"idle" |
"running" |
"completed" ;
startedAt?: string;
finishedAt?: string;
scenarios: ScenarioOutcome[];
counts: {
total: number;
pending: number;
running: number;
passed: number;
failed: number;
skipped: number;
};
};
export type RunnerSelection = {
providerMode:
"mock-openai" |
"live-frontier" ;
primaryModel: string;
alternateModel: string;
fastMode:
boolean ;
scenarioIds: string[];
};
export type RunnerSnapshot = {
status:
"idle" |
"running" |
"completed" |
"failed" ;
selection: RunnerSelection;
startedAt?: string;
finishedAt?: string;
artifacts:
null | {
outputDir: string;
reportPath: string;
summaryPath: string;
watchUrl: string;
};
error: string |
null ;
};
export type RunnerModelOption = {
key: string;
name: string;
provider: string;
input: string;
preferred:
boolean ;
};
export type OutcomesEnvelope = {
run: ScenarioRun |
null ;
};
export type CaptureSessionSummary = {
id: string;
startedAt: number;
endedAt?: number;
mode: string;
sourceProcess: string;
proxyUrl?: string;
eventCount: number;
};
export type CaptureEventView = {
id?: number;
ts: number;
protocol: string;
direction: string;
kind: string;
flowId: string;
method?: string;
host?: string;
path?: string;
status?: number;
closeCode?: number;
contentType?: string;
headersJson?: string;
dataText?: string;
payloadPreview?: string;
dataBlobId?: string;
errorText?: string;
provider?: string;
api?: string;
model?: string;
captureOrigin?: string;
};
export type CaptureQueryPreset =
|
"none"
|
"double-sends"
|
"retry-storms"
|
"cache-busting"
|
"ws-duplicate-frames"
|
"missing-ack"
|
"error-bursts" ;
export type CaptureSessionsEnvelope = {
sessions: CaptureSessionSummary[];
};
export type CaptureEventsEnvelope = {
events: CaptureEventView[];
};
export type CaptureQueryEnvelope = {
rows: Array<Record<string, string | number |
null >>;
};
export type CaptureObservedDimension = {
value: string;
count: number;
};
export type CaptureCoverageSummary = {
sessionId: string;
totalEvents: number;
unlabeledEventCount: number;
providers: CaptureObservedDimension[];
apis: CaptureObservedDimension[];
models: CaptureObservedDimension[];
hosts: CaptureObservedDimension[];
localPeers: CaptureObservedDimension[];
};
export type CaptureCoverageEnvelope = {
coverage: CaptureCoverageSummary;
};
export type CaptureStartupProbeStatus = {
label: string;
url: string;
ok:
boolean ;
error?: string;
};
export type CaptureStartupStatus = {
proxy: CaptureStartupProbeStatus;
gateway: CaptureStartupProbeStatus;
qaLab: CaptureStartupProbeStatus;
};
export type CaptureStartupStatusEnvelope = {
status: CaptureStartupStatus;
};
export type CaptureSavedView = {
id: string;
name: string;
sessionIds: string[];
kindFilter: string[];
providerFilter: string[];
hostFilter: string[];
searchText: string;
headerMode:
"key" |
"all" |
"hidden" ;
viewMode:
"list" |
"timeline" ;
groupMode:
"none" |
"flow" |
"host-path" |
"burst" ;
timelineLaneMode:
"domain" |
"provider" |
"flow" ;
timelineLaneSort:
"most-events" |
"most-errors" |
"severity" |
"alphabetical" ;
timelineZoom:
75 |
100 |
150 |
200 |
300 ;
timelineSparklineMode:
"session-relative" |
"lane-relative" ;
errorsOnly:
boolean ;
detailPlacement:
"right" |
"bottom" ;
payloadLayout:
"formatted" |
"raw" |
null ;
payloadExtent:
"preview" |
"full" ;
};
export type TabId =
"chat" |
"results" |
"report" |
"events" |
"capture" ;
export type UiState = {
theme:
"light" |
"dark" ;
bootstrap: Bootstrap |
null ;
snapshot: Snapshot |
null ;
latestReport: ReportEnvelope[
"report" ];
scenarioRun: ScenarioRun |
null ;
captureSessions: CaptureSessionSummary[];
captureEvents: CaptureEventView[];
captureQueryPreset: CaptureQueryPreset;
captureQueryRows: Array<Record<string, string | number |
null >>;
captureKindFilter: string[];
captureProviderFilter: string[];
captureHostFilter: string[];
captureSearchText: string;
captureHeaderMode:
"key" |
"all" |
"hidden" ;
captureViewMode:
"list" |
"timeline" ;
captureGroupMode:
"none" |
"flow" |
"host-path" |
"burst" ;
captureTimelineLaneMode:
"domain" |
"provider" |
"flow" ;
captureTimelineLaneSort:
"most-events" |
"most-errors" |
"severity" |
"alphabetical" ;
captureTimelinePreviousLaneSort:
|
"most-events"
|
"most-errors"
|
"severity"
|
"alphabetical"
|
null ;
captureTimelineLaneSearch: string;
captureTimelineZoom:
75 |
100 |
150 |
200 |
300 ;
captureTimelineSparklineMode:
"session-relative" |
"lane-relative" ;
captureTimelineWindowStartPct: number |
null ;
captureTimelineWindowEndPct: number |
null ;
captureTimelineBrushAnchorPct: number |
null ;
captureTimelineBrushCurrentPct: number |
null ;
captureTimelineFocusSelectedFlow:
boolean ;
captureTimelineFocusedLaneMode:
"all" |
"only-matching" |
"collapse-background" ;
captureTimelineFocusedLaneThreshold:
"any" |
"events-2" |
"percent-10" |
"percent-25" ;
captureDetailPlacement:
"right" |
"bottom" ;
captureDetailSplitPct: number;
captureDetailSplitDragging:
boolean ;
captureDetailView:
"overview" |
"flow" |
"payload" |
"headers" ;
capturePreferredDetailView:
"overview" |
"flow" |
"payload" |
"headers" |
null ;
captureFlowDetailLayout:
"nav-first" |
"pair-first" |
null ;
capturePayloadDetailLayout:
"formatted" |
"raw" |
null ;
capturePayloadExtent:
"preview" |
"full" ;
capturePayloadEventSort:
"stream" |
"name" |
"size" ;
capturePayloadEventFilter: string;
captureErrorsOnly:
boolean ;
captureCoverage: CaptureCoverageSummary |
null ;
captureStartupStatus: CaptureStartupStatus |
null ;
captureControlsExpanded:
boolean ;
captureSummaryExpanded:
boolean ;
captureSavedViews: CaptureSavedView[];
captureSelectedSessionsExpanded:
boolean ;
sidebarCollapsed:
boolean ;
sidebarPanel:
"scenarios" |
"config" |
"run" ;
captureCollapsedLaneIds: string[];
capturePinnedLaneIds: string[];
selectedCaptureSessionIds: string[];
selectedCaptureEventKey: string |
null ;
selectedConversationId: string |
null ;
selectedThreadId: string |
null ;
selectedScenarioId: string |
null ;
activeTab: TabId;
runnerDraft: RunnerSelection |
null ;
runnerDraftDirty:
boolean ;
composer: {
conversationKind:
"direct" |
"channel" ;
conversationId: string;
senderId: string;
senderName: string;
text: string;
};
busy:
boolean ;
error: string |
null ;
};
/* ===== Helpers ===== */
export
function formatTime(timestamp: number) {
return new Date(timestamp).toLocaleTimeString([], {
hour:
"2-digit" ,
minute:
"2-digit" ,
second:
"2-digit" ,
});
}
function formatIso(iso?: string) {
if (!iso) {
return "—" ;
}
return new Date(iso).toLocaleString([], {
month:
"short" ,
day:
"numeric" ,
hour:
"2-digit" ,
minute:
"2-digit" ,
second:
"2-digit" ,
});
}
function formatDuration(ms: number): string {
if (ms <
1000 ) {
return `${Math.round(ms)}ms`;
}
const seconds = ms /
1000 ;
if (seconds <
60 ) {
return `${seconds.toFixed(seconds >=
10 ?
0 :
1 )}s`;
}
const minutes = Math.floor(seconds /
60 );
const remainingSeconds = Math.round(seconds %
60 );
return `${minutes}m ${remainingSeconds}s`;
}
function esc(text: string) {
return text
.replaceAll(
"&" ,
"&" )
.replaceAll(
"<" ,
"<" )
.replaceAll(
">" ,
">" )
.replaceAll(
'"' ,
""" );
}
function parseJsonObject(raw?: string): Record<string, unknown> |
null {
if (!raw || raw.trim().length ===
0 ) {
return null ;
}
try {
const parsed = JSON.parse(raw) as unknown;
return parsed &&
typeof parsed ===
"object" && !Array.isArray(parsed)
? (parsed as Record<string, unknown>)
:
null ;
}
catch {
return null ;
}
}
function renderCaptureKeyValueGrid(rows: Array<{ label: string; value: string }>): string {
if (rows.length ===
0 ) {
return '<div class="empty-state">No structured fields available.</div>' ;
}
return `<div
class =
"capture-kv-grid" >
${rows
.map(
(row) => `<div
class =
"capture-kv-row" >
<div
class =
"capture-kv-label" >${esc(row.label)}</div>
<div
class =
"capture-kv-value capture-mono" >${esc(row.value)}</div>
</div>`,
)
.join(
"" )}
</div>`;
}
function isImportantCaptureHeader(label: string):
boolean {
return /content-type|content-length|accept|cache-control|etag|last-modified|retry
-after|location|date|server|x-request-id|openai-processing-ms|cf-cache-status|vary|age|host|user-agent/i.test(
label,
);
}
function renderCaptureHeaders(raw: string | undefined, mode: UiState["captureHeaderMode" ]): string {
if (mode === "hidden" ) {
return '<div class="empty-state">Headers are hidden. Switch to key or all to inspect them.</div>' ;
}
const parsed = parseJsonObject(raw);
if (!parsed) {
return '<div class="empty-state">No captured headers for this event.</div>' ;
}
const sourceEntries =
mode === "key"
? Object.entries(parsed).filter(([label]) => isImportantCaptureHeader(label))
: Object.entries(parsed);
const groups: Array<{
key: string;
label: string;
match: (header: string) => boolean ;
}> = [
{ key: "auth" , label: "Auth & Session" , match: (header) => isSensitiveCaptureField(header) },
{
key: "content" ,
label: "Content" ,
match: (header) => /content-|accept|encoding|transfer-encoding/i.test(header),
},
{
key: "cache" ,
label: "Caching & Validation" ,
match: (header) => /cache|etag|if -|last-modified|vary|expires|age/i.test(header),
},
{
key: "routing" ,
label: "Routing & Network" ,
match: (header) =>
/host|origin|referer|x-forwarded|forwarded|cf-|traceparent|tracestate|via/i.test(header),
},
];
const remaining = new Map(sourceEntries);
const renderedGroups = groups
.map((group) => {
const rows = Array.from(remaining.entries())
.filter(([label]) => group.match(label))
.map(([label, value]) => {
remaining.delete (label);
return { label, value: formatCaptureFieldValue(value, label) };
})
.filter((row) => row.value.length > 0 )
.toSorted((left, right) => left.label.localeCompare(right.label));
if (rows.length === 0 ) {
return "" ;
}
return `<section class ="capture-inline-section" >
<div class ="capture-summary-label" >${esc(group.label)}</div>
${renderCaptureKeyValueGrid(rows)}
</section>`;
})
.filter(Boolean );
const otherRows = Array.from(remaining.entries())
.map(([label, value]) => ({
label,
value: formatCaptureFieldValue(value, label),
}))
.filter((row) => row.value.length > 0 )
.toSorted((left, right) => left.label.localeCompare(right.label));
if (otherRows.length > 0 ) {
renderedGroups.push(`<section class ="capture-inline-section" >
<div class ="capture-summary-label" >Other</div>
${renderCaptureKeyValueGrid(otherRows)}
</section>`);
}
return (
renderedGroups.join("" ) || '<div class="empty-state">No captured headers for this event.</div>'
);
}
function isSensitiveCaptureField(label: string): boolean {
return /authorization|proxy-authorization|cookie|set-cookie|api[-_]?key|x[-_]?api[-_]?key|token|secret|password|session/i.test(
label,
);
}
function redactCaptureScalar(value: string, label?: string): string {
const trimmed = value.trim();
if (!trimmed) {
return "" ;
}
if (label && isSensitiveCaptureField(label)) {
if (/^bearer\s+/i.test(trimmed)) {
return "Bearer [redacted]" ;
}
return "[redacted]" ;
}
if (trimmed.length > 400 ) {
return `${trimmed.slice(0 , 280 )}\n…\n${trimmed.slice(-80 )}`;
}
return trimmed;
}
function redactCaptureValue(value: unknown, label?: string): unknown {
if (typeof value === "string" ) {
return redactCaptureScalar(value, label);
}
if (Array.isArray(value)) {
return value.map((entry) => redactCaptureValue(entry, label));
}
if (!value || typeof value !== "object" ) {
return value;
}
const out: Record<string, unknown> = {};
for (const [key, entry] of Object.entries(value)) {
out[key] = redactCaptureValue(entry, key);
}
return out;
}
function formatCaptureFieldValue(value: unknown, label?: string): string {
const redacted = redactCaptureValue(value, label);
if (typeof redacted === "string" ) {
return redacted;
}
if (redacted == null ) {
return "" ;
}
if (Array.isArray(redacted)) {
return redacted
.map((entry) => (typeof entry === "string" ? entry : JSON.stringify(entry)))
.filter(Boolean )
.join(", " );
}
return JSON.stringify(redacted, null , 2 );
}
function renderCaptureFormPayload(payload: string): string {
const params = new URLSearchParams(payload.trim());
const rows = Array.from(params.entries()).map(([label, value]) => ({
label,
value: redactCaptureScalar(value, label),
}));
return rows.length > 0
? renderCaptureKeyValueGrid(rows)
: `<pre class ="report-pre capture-pre" >${esc(redactCaptureScalar(payload))}</pre>`;
}
function renderCaptureSsePayload(
payload: string,
options?: {
sort?: UiState["capturePayloadEventSort" ];
filterText?: string;
},
): { body: string; eventCount: number; visibleCount: number } {
const frames = payload
.split(/\n\n+/)
.map((frame) => frame.trim())
.filter(Boolean )
.slice(0 , 48 )
.map((frame, index) => {
const rows = frame
.split("\n" )
.map((line) => line.trim())
.filter(Boolean )
.map((line) => {
const separatorIndex = line.indexOf(":" );
const label =
separatorIndex >= 0 ? line.slice(0 , separatorIndex).trim() || "field" : "line" ;
const value = separatorIndex >= 0 ? line.slice(separatorIndex + 1 ).trim() : line;
return { label, value: redactCaptureScalar(value, label) };
});
const eventName = rows.find((row) => row.label === "event" )?.value || "message" ;
const dataText = rows
.filter((row) => row.label === "data" )
.map((row) => row.value)
.join("\n" );
const searchable = [eventName, dataText, ...rows.flatMap((row) => [row.label, row.value])]
.filter(Boolean )
.join(" " )
.toLowerCase();
return {
id: index,
index,
eventName,
rows,
byteLength: new TextEncoder().encode(frame).length,
searchable,
};
});
const normalizedFilter = options?.filterText?.trim().toLowerCase() ?? "" ;
const filteredFrames =
normalizedFilter.length === 0
? frames
: frames.filter((frame) => frame.searchable.includes(normalizedFilter));
const sortMode = options?.sort ?? "stream" ;
const sortedFrames = [...filteredFrames].toSorted((left, right) => {
if (sortMode === "name" ) {
return left.eventName.localeCompare(right.eventName) || left.index - right.index;
}
if (sortMode === "size" ) {
return right.byteLength - left.byteLength || left.index - right.index;
}
return left.index - right.index;
});
if (frames.length === 0 ) {
return {
body: `<pre class ="report-pre capture-pre" >${esc(redactCaptureScalar(payload))}</pre>`,
eventCount: 0 ,
visibleCount: 0 ,
};
}
return {
body:
sortedFrames.length === 0
? '<div class="empty-state">No SSE frames match the current payload filter.</div>'
: `<div class ="capture-sse-stack" >
${sortedFrames
.map(
(frame) => `<section class ="capture-inline-section capture-inline-section-compact" >
<div class ="capture-summary-header" >
<div class ="capture-summary-label" >Event ${frame.index + 1 }</div>
<div class ="capture-detail-mini-meta" >
<span class ="capture-chip" >${esc(frame.eventName)}</span>
<span class ="capture-chip capture-chip-muted" >${frame.byteLength.toLocaleString()} bytes</span>
</div>
</div>
${renderCaptureKeyValueGrid(frame.rows)}
</section>`,
)
.join("" )}
</div>`,
eventCount: frames.length,
visibleCount: sortedFrames.length,
};
}
function renderCapturePayload(
payload: string | undefined,
contentType?: string,
options?: {
payloadEventSort?: UiState["capturePayloadEventSort" ];
payloadEventFilter?: string;
},
): {
body: string;
mode: string;
byteLength: number;
looksStructured: boolean ;
itemCount?: number;
visibleItemCount?: number;
} {
if (!payload?.length) {
return {
body: '<div class="empty-state">No inline payload preview for this event.</div>' ,
mode: "none" ,
byteLength: 0 ,
looksStructured: false ,
};
}
const trimmed = payload.trim();
const byteLength = new TextEncoder().encode(payload).length;
if (contentType?.includes("application/x-www-form-urlencoded" )) {
return {
body: renderCaptureFormPayload(payload),
mode: "form" ,
byteLength,
looksStructured: true ,
};
}
if (contentType?.includes("text/event-stream" ) || /^event:|^data:/m.test(trimmed)) {
const sse = renderCaptureSsePayload(payload, {
sort: options?.payloadEventSort,
filterText: options?.payloadEventFilter,
});
return {
body: sse.body,
mode: "sse" ,
byteLength,
looksStructured: true ,
itemCount: sse.eventCount,
visibleItemCount: sse.visibleCount,
};
}
const isJsonLike =
contentType?.includes("json" ) || trimmed.startsWith("{" ) || trimmed.startsWith("[" );
if (isJsonLike) {
try {
const parsed = JSON.parse(trimmed) as unknown;
return {
body: `<pre class ="report-pre capture-pre capture-pre-json" >${esc(
JSON.stringify(redactCaptureValue(parsed), null , 2 ),
)}</pre>`,
mode: "json" ,
byteLength,
looksStructured: true ,
};
} catch {
// fall through to plain text
}
}
return {
body: `<pre class ="report-pre capture-pre" >${esc(redactCaptureScalar(payload))}</pre>`,
mode: "text" ,
byteLength,
looksStructured: false ,
};
}
function renderCaptureCommandBlock(label: string, command: string): string {
return `<div class ="capture-startup-command" >
<div class ="capture-summary-header" >
<div class ="capture-summary-label" >${esc(label)}</div>
<button
class ="btn-sm capture-copy-button"
type="button"
data-copy-text="${esc(command)}"
>Copy</button>
</div>
<pre class ="report-pre capture-pre capture-startup-pre" >${esc(command)}</pre>
</div>`;
}
function renderCaptureStartupStatusRow(status: CaptureStartupProbeStatus | null ): string {
if (!status) {
return '<div class="capture-startup-status-row text-dimmed text-sm">Status unavailable.</div>' ;
}
return `<div class ="capture-startup-status-row text-sm" >
<span class ="capture-chip ${status.ok ? " capture-chip-strong" : " capture-chip-danger"}" >${
status.ok ? "reachable" : "unreachable"
}</span>
<span class ="capture-startup-status-url capture-mono" >${esc(status.url)}</span>
${status.ok ? "" : `<span class ="text-dimmed" >${esc(status.error || "connection failed" )}</span>`}
</div>`;
}
function renderCaptureStartupInstructions(status: CaptureStartupStatus | null ): string {
const proxyStart = "pnpm proxy:start --port 7799" ;
const gatewayStart = `OPENCLAW_DEBUG_PROXY_ENABLED=1 \\
OPENCLAW_DEBUG_PROXY_REQUIRE=1 \\
OPENCLAW_DEBUG_PROXY_URL=http://127.0.0.1:7799 \\
pnpm openclaw gateway --port 18789 --bind loopback`;
const qaStart = "pnpm qa:lab:ui --port 43124 --control-ui-url http://127.0.0.1:18789/ ";
const caInstall = "pnpm proxy:install-ca" ;
const caTrust =
"sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain /Users/thoffman/.openclaw/debug-proxy/certs/root-ca.pem" ;
return `<div class ="capture-startup-state" >
<div class ="capture-startup-title" >Proxy capture is not running yet.</div>
<div class ="text-dimmed text-sm capture-startup-copy" >
Start the proxy, then the gateway through that proxy, then QA Lab. Each command is copyable.
</div>
<div class ="capture-startup-grid" >
<div>
${renderCaptureStartupStatusRow(status?.proxy ?? null )}
${renderCaptureCommandBlock("1. Start proxy" , proxyStart)}
</div>
<div>
${renderCaptureStartupStatusRow(status?.gateway ?? null )}
${renderCaptureCommandBlock("2. Start gateway through proxy" , gatewayStart)}
</div>
<div>
${renderCaptureStartupStatusRow(status?.qaLab ?? null )}
${renderCaptureCommandBlock("3. Start QA Lab" , qaStart)}
</div>
<div>
<div class ="capture-startup-status-row text-dimmed text-sm" >
Install the debug CA once on macOS if you want HTTPS/WSS clients to trust the proxy.
</div>
${renderCaptureCommandBlock("4. Generate/install debug CA helper" , caInstall)}
${renderCaptureCommandBlock("5. macOS system trust (if needed)" , caTrust)}
</div>
</div>
</div>`;
}
function captureEventKey(event: Pick<CaptureEventView, "id" | "flowId" | "ts" | "kind" >): string {
return `${event.id ?? "no-id" }:${event.flowId}:${event.ts}:${event.kind}`;
}
function captureEventGlyph(event: Pick<CaptureEventView, "kind" | "direction" >): {
label: string;
cls: string;
} {
switch (event.kind) {
case "request" :
return { label: "REQ" , cls: "req" };
case "response" :
return { label: "RES" , cls: "res" };
case "error" :
return { label: "ERR" , cls: "err" };
case "ws-frame" :
return { label: "WS" , cls: "ws" };
case "ws-open" :
return { label: "W+" , cls: "ws" };
case "ws-close" :
return { label: "W-" , cls: "ws" };
case "tls-handshake" :
return { label: "TLS" , cls: "sys" };
case "connect" :
return { label: "CON" , cls: "sys" };
case "retry-link" :
return { label: "RTY" , cls: "warn" };
default :
return { label: event.direction === "inbound" ? "IN" : "OUT" , cls: "sys" };
}
}
function findPairedCaptureEvent(
event: CaptureEventView | null ,
candidates: CaptureEventView[],
): { counterpart: CaptureEventView | null ; role: "request" | "response" | null } {
if (!event?.flowId || (event.kind !== "request" && event.kind !== "response" )) {
return { counterpart: null , role: null };
}
const flowEvents = candidates
.filter(
(candidate) =>
candidate.flowId === event.flowId &&
(candidate.kind === "request" || candidate.kind === "response" ) &&
captureEventKey(candidate) !== captureEventKey(event),
)
.toSorted(
(left, right) =>
left.ts - right.ts || captureEventKey(left).localeCompare(captureEventKey(right)),
);
if (event.kind === "request" ) {
return {
counterpart:
flowEvents.find((candidate) => candidate.kind === "response" && candidate.ts >= event.ts) ??
null ,
role: "response" ,
};
}
const requests = flowEvents.filter(
(candidate) => candidate.kind === "request" && candidate.ts <= event.ts,
);
return {
counterpart: requests.at(-1 ) ?? null ,
role: "request" ,
};
}
function attachmentSourceUrl(attachment: Attachment): string | null {
if (attachment.url?.trim()) {
return attachment.url;
}
if (attachment.contentBase64?.trim()) {
return `data:${attachment.mimeType};base64,${attachment.contentBase64}`;
}
return null ;
}
function renderMessageAttachments(message: Message): string {
const attachments = message.attachments ?? [];
if (attachments.length === 0 ) {
return "" ;
}
const items = attachments
.map((attachment) => {
const sourceUrl = attachmentSourceUrl(attachment);
const label = attachment.fileName || attachment.altText || attachment.mimeType;
if (attachment.kind === "image" && sourceUrl) {
return `<figure class ="msg-attachment msg-attachment-image" >
<img src="${esc(sourceUrl)}" alt="${esc(attachment.altText || label)}" loading="lazy" />
<figcaption>${esc(label)}</figcaption>
</figure>`;
}
if (attachment.kind === "video" && sourceUrl) {
return `<figure class ="msg-attachment msg-attachment-video" >
<video controls preload="metadata" src="${esc(sourceUrl)}" ></video>
<figcaption>${esc(label)}</figcaption>
</figure>`;
}
if (attachment.kind === "audio" && sourceUrl) {
return `<figure class ="msg-attachment msg-attachment-audio" >
<audio controls preload="metadata" src="${esc(sourceUrl)}" ></audio>
<figcaption>${esc(label)}</figcaption>
</figure>`;
}
const transcript = attachment.transcript?.trim()
? `<div class ="msg-attachment-transcript" >${esc(attachment.transcript)}</div>`
: "" ;
const href = sourceUrl ? ` href="${esc(sourceUrl)}" target="_blank" rel="noreferrer" ` : "" ;
return `<div class ="msg-attachment msg-attachment-file" >
<a class ="msg-attachment-link" ${href}>${esc(label)}</a>
${transcript}
</div>`;
})
.join("" );
return `<div class ="msg-attachments" >${items}</div>`;
}
const MOCK_MODELS: RunnerModelOption[] = [
{
key: "mock-openai/gpt-5.4" ,
name: "GPT-5.4 (mock)" ,
provider: "mock-openai" ,
input: "text" ,
preferred: true ,
},
{
key: "mock-openai/gpt-5.4-alt" ,
name: "GPT-5.4 Alt (mock)" ,
provider: "mock-openai" ,
input: "text" ,
preferred: false ,
},
];
export function deriveSelectedConversation(state: UiState): string | null {
return state.selectedConversationId ?? state.snapshot?.conversations[0 ]?.id ?? null ;
}
export function deriveSelectedThread(state: UiState): string | null {
return state.selectedThreadId ?? null ;
}
export function filteredMessages(state: UiState) {
const messages = state.snapshot?.messages ?? [];
return messages.filter((message) => {
if (state.selectedConversationId && message.conversation.id !== state.selectedConversationId) {
return false ;
}
if (state.selectedThreadId && message.threadId !== state.selectedThreadId) {
return false ;
}
return true ;
});
}
function findScenarioOutcome(state: UiState, scenario: SeedScenario) {
return (
state.scenarioRun?.scenarios.find((o) => o.id === scenario.id) ??
state.scenarioRun?.scenarios.find((o) => o.name === scenario.title) ??
null
);
}
function statusDotClass(status: ScenarioOutcome["status" ] | "pending" ): string {
return `scenario-item-dot scenario-item-dot-${status}`;
}
function badgeHtml(status: string): string {
const tone = status === "failed" ? "fail" : status === "completed" ? "pass" : status;
return `<span class ="badge badge-${esc(tone)}" >${esc(status)}</span>`;
}
function deriveSelection(state: UiState): RunnerSelection | null {
return state.runnerDraft ?? state.bootstrap?.runner.selection ?? null ;
}
/* ===== Render: Header ===== */
function renderHeader(state: UiState): string {
const runner = state.bootstrap?.runner ?? null ;
const run = state.scenarioRun;
const controlUrl = state.bootstrap?.controlUiUrl;
return `
<header class ="header" >
<div class ="header-left" >
<span class ="header-title" >QA Lab</span>
<div class ="header-status" >
${runner ? badgeHtml(runner.status) : "" }
${run ? `<span class ="badge badge-accent" >${run.counts.passed}/${run.counts.total} pass</span>` : "" }
${state.error ? `<span class ="badge badge-fail" >${esc(state.error)}</span>` : "" }
</div>
</div>
<div class ="header-right" >
${controlUrl ? `<a class ="header-link" href="${esc(controlUrl)}" target="_blank" rel="noreferrer" >Control UI</a>` : "" }
<button class ="btn-ghost btn-sm" data-action="toggle-sidebar" >${state.sidebarCollapsed ? "Show sidebar" : "Hide sidebar" }</button>
<button class ="btn-ghost btn-sm" data-action="refresh" ${state.busy ? " disabled" : "" }>Refresh</button>
<button class ="btn-ghost btn-sm" data-action="reset" ${state.busy ? " disabled" : "" }>Reset</button>
<button class ="theme-toggle" data-action="toggle-theme" title="Toggle theme" >${state.theme === "dark" ? "\u2600" : "\u263E" }</button>
</div>
</header>`;
}
/* ===== Render: Sidebar ===== */
function renderModelSelect(params: {
id: string;
label: string;
value: string;
options: RunnerModelOption[];
disabled: boolean ;
}): string {
const values = new Set(params.options.map((o) => o.key));
const options = [...params.options];
if (!values.has(params.value) && params.value.trim()) {
options.unshift({
key: params.value,
name: params.value,
provider: params.value.split("/" )[0 ] ?? "custom" ,
input: "text" ,
preferred: false ,
});
}
return `
<div class ="config-field" >
<span class ="config-label" >${esc(params.label)}</span>
<select id="${esc(params.id)}" ${params.disabled ? " disabled" : "" }>
${options
.map(
(o) =>
`<option value="${esc(o.key)}" ${o.key === params.value ? " selected" : "" }>${esc(o.key)}</option>`,
)
.join("" )}
</select>
</div>`;
}
function renderSidebar(state: UiState): string {
const scenarios = state.bootstrap?.scenarios ?? [];
const selection = deriveSelection(state);
const runner = state.bootstrap?.runner ?? null ;
const run = state.scenarioRun;
const isRunning = runner?.status === "running" ;
const realModels = state.bootstrap?.runnerCatalog.real ?? [];
const modelOptions =
selection?.providerMode === "live-frontier" && realModels.length > 0 ? realModels : MOCK_MODELS;
const selectedIds = new Set(selection?.scenarioIds ?? []);
return `
<aside class ="sidebar${state.sidebarCollapsed ? " is-collapsed" : " "}" >
<div class ="sidebar-panel-tabs" >
<button class ="btn-sm btn-ghost sidebar-panel-tab${state.sidebarPanel === " scenarios" ? " active" : " "}" data-sidebar-panel="scenarios" >Scenarios</button>
<button class ="btn-sm btn-ghost sidebar-panel-tab${state.sidebarPanel === " config" ? " active" : " "}" data-sidebar-panel="config" >Config</button>
<button class ="btn-sm btn-ghost sidebar-panel-tab${state.sidebarPanel === " run" ? " active" : " "}" data-sidebar-panel="run" >Run</button>
</div>
${
state.sidebarPanel === "config"
? `<div class ="sidebar-section sidebar-panel-body" >
<div class ="sidebar-section-title" ><h3>Configuration</h3></div>
<div class ="config-field" >
<span class ="config-label" >Provider lane</span>
<select id="provider-mode" ${isRunning ? " disabled" : "" }>
<option value="mock-openai" ${selection?.providerMode === "mock-openai" ? " selected" : "" }>Synthetic (mock)</option>
<option value="live-frontier" ${selection?.providerMode === "live-frontier" ? " selected" : "" }>Real frontier providers</option>
</select>
</div>
${renderModelSelect({
id: "primary-model" ,
label: "Primary model" ,
value: selection?.primaryModel ?? "" ,
options: modelOptions,
disabled: isRunning,
})}
${renderModelSelect({
id: "alternate-model" ,
label: "Alternate model" ,
value: selection?.alternateModel ?? "" ,
options: modelOptions,
disabled: isRunning,
})}
${
selection?.providerMode === "live-frontier"
? `<div class ="config-hint" >${esc(
state.bootstrap?.runnerCatalog.status === "loading"
? "Loading model catalog\u2026"
: state.bootstrap?.runnerCatalog.status === "failed"
? "Catalog unavailable; using manual input."
: `${realModels.length} models available`,
)}</div>`
: ""
}
</div>`
: state.sidebarPanel === "run"
? `<div class ="sidebar-panel-body" >${run || runner ? renderRunStatus(state) : '<div class="sidebar-section"><div class="text-dimmed text-sm">No run data yet.</div></div>' }</div>`
: `<div class ="sidebar-section sidebar-scenarios sidebar-panel-body" >
<div class ="sidebar-section-title" >
<h3>Scenarios (${selectedIds.size}/${scenarios.length})</h3>
<div class ="btn-group" >
<button class ="btn-sm btn-ghost" data-action="select-all-scenarios" ${isRunning ? " disabled" : "" }>All</button>
<button class ="btn-sm btn-ghost" data-action="clear-scenarios" ${isRunning ? " disabled" : "" }>None</button>
</div>
</div>
<div class ="scenario-scroll" >
${scenarios
.map((s) => {
const outcome = findScenarioOutcome(state, s);
const status = outcome?.status ?? "pending" ;
return `
<label class ="scenario-item" >
<input type="checkbox" data-scenario-toggle-id="${esc(s.id)}" ${selectedIds.has(s.id) ? " checked" : "" }${isRunning ? " disabled" : "" } />
<span class ="${statusDotClass(status)}" ></span>
<div class ="scenario-item-info" >
<span class ="scenario-item-title" >${esc(s.title)}</span>
<span class ="scenario-item-meta" >${esc(s.surface)} · ${esc(s.id)}</span>
</div>
</label>`;
})
.join("" )}
</div>
</div>`
}
<!-- Actions -->
<div class ="sidebar-actions" >
<button class ="btn-primary" data-action="run-suite" ${isRunning || !selectedIds.size || state.busy ? " disabled" : "" }>
Run ${selectedIds.size} scenario${selectedIds.size === 1 ? "" : "s" }
</button>
<div class ="btn-row" >
<button data-action="self-check" ${isRunning || state.busy ? " disabled" : "" }>Self-check</button>
<button data-action="kickoff" ${isRunning || state.busy ? " disabled" : "" }>Kickoff</button>
</div>
</div>
</aside>`;
}
function renderRunStatus(state: UiState): string {
const run = state.scenarioRun;
const runner = state.bootstrap?.runner ?? null ;
if (!run && !runner) {
return "" ;
}
return `
<div class ="sidebar-section run-status" >
<div class ="sidebar-section-title" >
<h3>Run Status</h3>
${runner ? badgeHtml(runner.status) : "" }
</div>
${
run
? `<div class ="run-counts" >
<div class ="run-count" ><span class ="run-count-value" >${run.counts.total}</span><span class ="run-count-label" >Total</span></div>
<div class ="run-count" ><span class ="run-count-value count-pass" >${run.counts.passed}</span><span class ="run-count-label" >Pass</span></div>
<div class ="run-count" ><span class ="run-count-value count-fail" >${run.counts.failed}</span><span class ="run-count-label" >Fail</span></div>
<div class ="run-count" ><span class ="run-count-value" >${run.counts.pending + run.counts.running}</span><span class ="run-count-label" >Left</span></div>
</div>`
: ""
}
<div class ="run-meta" >
${runner?.startedAt ? `Started ${esc(formatIso(runner.startedAt))}` : "" }
${runner?.finishedAt ? `<br>Finished ${esc(formatIso(runner.finishedAt))}` : "" }
${runner?.error ? `<br><span style="color:var(--danger)" >${esc(runner.error)}</span>` : "" }
</div>
</div>`;
}
/* ===== Render: Tab bar ===== */
function renderTabBar(state: UiState): string {
const tabs: Array<{ id: TabId; label: string }> = [
{ id: "chat" , label: "Chat" },
{ id: "results" , label: "Results" },
{ id: "report" , label: "Report" },
{ id: "events" , label: "Events" },
{ id: "capture" , label: "Capture" },
];
return `
<nav class ="tab-bar" >
${tabs
.map(
(t) =>
`<button class ="tab-btn${state.activeTab === t.id ? " active" : " "}" data-tab="${t.id}" >${t.label}</button>`,
)
.join("" )}
<div class ="tab-spacer" ></div>
</nav>`;
}
/* ===== Render: Chat tab ===== */
function renderChatView(state: UiState): string {
const conversations = state.snapshot?.conversations ?? [];
const channels = conversations.filter((c) => c.kind === "channel" );
const dms = conversations.filter((c) => c.kind === "direct" );
const threads = (state.snapshot?.threads ?? []).filter(
(t) => !state.selectedConversationId || t.conversationId === state.selectedConversationId,
);
const selectedConv = deriveSelectedConversation(state);
const selectedThread = deriveSelectedThread(state);
const activeConversation = conversations.find((c) => c.id === selectedConv);
const messages = filteredMessages({
...state,
selectedConversationId: selectedConv,
selectedThreadId: selectedThread,
});
return `
<div class ="chat-view" >
<!-- Channel / DM sidebar -->
<aside class ="chat-sidebar" >
<div class ="chat-sidebar-scroll" >
<div class ="chat-sidebar-section" >
<div class ="chat-sidebar-heading" >Channels</div>
<div class ="chat-sidebar-list" >
${
channels.length === 0
? '<div class="chat-sidebar-item" style="color:var(--text-tertiary);font-size:12px;cursor:default">No channels</div>'
: channels
.map(
(c) => `
<button class ="chat-sidebar-item${c.id === selectedConv ? " active" : " "}" data-conversation-id="${esc(c.id)}" >
<span class ="chat-sidebar-icon" >#</span>
<span class ="chat-sidebar-label" >${esc(c.title || c.id)}</span>
</button>`,
)
.join("" )
}
</div>
</div>
<div class ="chat-sidebar-section" >
<div class ="chat-sidebar-heading" >Direct Messages</div>
<div class ="chat-sidebar-list" >
${
dms.length === 0
? '<div class="chat-sidebar-item" style="color:var(--text-tertiary);font-size:12px;cursor:default">No DMs</div>'
: dms
.map(
(c) => `
<button class ="chat-sidebar-item${c.id === selectedConv ? " active" : " "}" data-conversation-id="${esc(c.id)}" >
<span class ="chat-sidebar-icon" >\u25CF</span>
<span class ="chat-sidebar-label" >${esc(c.title || c.id)}</span>
</button>`,
)
.join("" )
}
</div>
</div>
${
threads.length > 0
? `<div class ="chat-sidebar-section" >
<div class ="chat-sidebar-heading" >Threads</div>
<div class ="chat-sidebar-list" >
<button class ="chat-sidebar-item${!selectedThread ? " active" : " "}" data-thread -select="root" >
<span class ="chat-sidebar-icon" >\u2302</span>
<span class ="chat-sidebar-label" >Main timeline</span>
</button>
${threads
.map(
(t) => `
<button class ="chat-sidebar-item${t.id === selectedThread ? " active" : " "}" data-thread -select="${esc(t.id)}" data-thread -conv="${esc(t.conversationId)}" >
<span class ="chat-sidebar-icon" >\u21B3</span>
<span class ="chat-sidebar-label" >${esc(t.title)}</span>
</button>`,
)
.join("" )}
</div>
</div>`
: ""
}
</div>
</aside>
<!-- Main chat area -->
<div class ="chat-main" >
<!-- Channel header -->
<div class ="chat-channel-header" >
<span class ="chat-channel-name" >${esc(activeConversation?.title || selectedConv || "No conversation" )}</span>
${activeConversation ? `<span class ="chat-channel-kind" >${activeConversation.kind}</span>` : "" }
${state.bootstrap?.runner.status === "running" ? '<span class="live-indicator"><span class="live-dot"></span>LIVE</span>' : "" }
</div>
<!-- Messages -->
<div class ="chat-messages" id="chat-messages" >
${
messages.length === 0
? '<div class="chat-empty">No messages yet. Run scenarios or send a message below.</div>'
: messages.map((m) => renderMessage(m)).join("" )
}
</div>
<!-- Composer -->
<div class ="chat-composer" >
<div class ="composer-context" >
<select id="conversation-kind" >
<option value="direct" ${state.composer.conversationKind === "direct" ? " selected" : "" }>DM</option>
<option value="channel" ${state.composer.conversationKind === "channel" ? " selected" : "" }>Channel</option>
</select>
<span>as</span>
<input id="sender-name" value="${esc(state.composer.senderName)}" placeholder="Name" />
<span>in</span>
<input id="conversation-id" value="${esc(state.composer.conversationId)}" placeholder="Conversation" />
<input id="sender-id" type="hidden" value="${esc(state.composer.senderId)}" />
</div>
<div class ="composer-input" >
<textarea id="composer-text" rows="1" placeholder="Type a message\u2026 (Enter to send, Shift+Enter for newline)" >${esc(state.composer.text)}</textarea>
<button class ="btn-primary composer-send" data-action="send" ${state.busy ? " disabled" : "" }>Send</button>
</div>
</div>
</div>
</div>`;
}
function messageAvatar(m: Message): { emoji: string; bg: string; role: string } {
if (m.direction === "outbound" ) {
return { emoji: "\uD83E\uDD80" , bg: "#7c6cff" , role: "Claw" }; //
}
return { emoji: "\uD83E\uDD9E" , bg: "#d97706" , role: "Clawfather" }; //
}
function renderMessage(m: Message): string {
const name = m.senderName || m.senderId;
const avatar = messageAvatar(m);
const dirClass = m.direction === "inbound" ? "msg-direction-inbound" : "msg-direction-outbound" ;
const metaTags: string[] = [];
if (m.threadId) {
metaTags.push(`<span class ="msg-tag" >thread ${esc(m.threadId)}</span>`);
}
if (m.editedAt) {
metaTags.push('<span class="msg-tag">edited</span>' );
}
if (m.deleted) {
metaTags.push('<span class="msg-tag">deleted</span>' );
}
const reactions =
m.reactions.length > 0
? `<span class ="msg-reactions" >${m.reactions.map((r) => `<span class ="msg-reaction" >${esc(r.emoji)}</span>`).join("" )}</span>`
: "" ;
return `
<div class ="msg msg-${m.direction}" >
<div class ="msg-avatar" style="background:${avatar.bg}" >${avatar.emoji}</div>
<div class ="msg-body" >
<div class ="msg-header" >
<span class ="msg-sender" >${esc(name)}</span>
<span class ="msg-role" >${esc(avatar.role)}</span>
<span class ="msg-direction ${dirClass}" >${m.direction === "inbound" ? "\u2B06" : "\u2B07" }</span>
<span class ="msg-time" >${formatTime(m.timestamp)}</span>
</div>
<div class ="msg-text" >${esc(m.text)}</div>
${renderMessageAttachments(m)}
${metaTags.length > 0 || reactions ? `<div class ="msg-meta" >${metaTags.join("" )}${reactions}</div>` : "" }
</div>
</div>`;
}
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 `
<div class ="inspector-live-message" >
<div class ="inspector-live-message-head" >
<div class ="inspector-live-message-identity" >
<span class ="inspector-live-avatar" style="background:${avatar.bg}" >${avatar.emoji}</span>
<span class ="inspector-live-sender" >${esc(message.senderName || message.senderId)}</span>
<span class ="inspector-live-direction inspector-live-direction-${message.direction}" >${message.direction === "inbound" ? "inbound" : "outbound" }</span>
</div>
<span class ="inspector-live-time" >${formatTime(message.timestamp)}</span>
</div>
<div class ="inspector-live-channel" >
${esc(conversationLabel)}${threadLabel ? ` · ${esc(threadLabel)}` : "" }
</div>
<div class ="inspector-live-text" >${esc(message.text)}</div>
</div>`;
}
function renderInspectorLiveTranscript(state: UiState): string {
const messages = recentInspectorMessages(state);
const isLive = state.bootstrap?.runner.status === "running" ;
return `
<aside class ="inspector-live" >
<div class ="inspector-live-header" >
<div>
<div class ="inspector-section-title" >Live Transcript</div>
<div class ="inspector-live-subtitle" >
${isLive ? "Latest QA bus messages as the run progresses." : "Latest observed QA bus messages." }
</div>
</div>
${isLive ? '<span class="live-indicator"><span class="live-dot"></span>LIVE</span>' : "" }
</div>
<div class ="inspector-live-feed" >
${
messages.length > 0
? messages.map((message) => renderInspectorLiveMessage(message)).join("" )
: '<div class="empty-state">No transcript yet. Start a run or send a message.</div>'
}
</div>
</aside>`;
}
/* ===== Render: Results tab ===== */
function renderResultsView(state: UiState): string {
const scenarios = state.bootstrap?.scenarios ?? [];
const selected = scenarios.find((s) => s.id === state.selectedScenarioId) ?? scenarios[0 ] ?? null ;
return `
<div class ="results-view" >
<div class ="results-list" >
${scenarios.length === 0 ? '<div class="empty-state">No scenarios loaded.</div>' : "" }
${scenarios
.map((s) => {
const outcome = findScenarioOutcome(state, s);
const status = outcome?.status ?? "pending" ;
const isSelected = s.id === (selected?.id ?? null );
return `
<button class ="result-card${isSelected ? " selected" : " "}" data-scenario-id="${esc(s.id)}" >
<span class ="result-card-dot scenario-item-dot-${status}" ></span>
<div class ="result-card-info" >
<span class ="result-card-title" >${esc(s.title)}</span>
<span class ="result-card-sub" >${esc(s.surface)} · ${outcome?.steps?.length ?? s.successCriteria.length} steps</span>
</div>
${badgeHtml(status)}
</button>`;
})
.join("" )}
</div>
<div class ="results-inspector" >
${selected ? renderInspector(state, selected) : '<div class="inspector-empty">Select a scenario</div>' }
</div>
</div>`;
}
function renderInspector(state: UiState, scenario: SeedScenario): string {
const outcome = findScenarioOutcome(state, scenario);
return `
<div class ="inspector-layout" >
<div class ="inspector-main" >
<div class ="inspector-header" >
<div>
<div class ="inspector-title" >${esc(scenario.title)}</div>
${badgeHtml(outcome?.status ?? "pending" )}
</div>
</div>
<div class ="inspector-objective" >${esc(scenario.objective)}</div>
<div class ="inspector-meta" >
<div class ="inspector-meta-item" ><span class ="inspector-meta-label" >Surface</span><span class ="inspector-meta-value" >${esc(scenario.surface)}</span></div>
<div class ="inspector-meta-item" ><span class ="inspector-meta-label" >Started</span><span class ="inspector-meta-value" >${esc(formatIso(outcome?.startedAt))}</span></div>
<div class ="inspector-meta-item" ><span class ="inspector-meta-label" >Finished</span><span class ="inspector-meta-value" >${esc(formatIso(outcome?.finishedAt))}</span></div>
<div class ="inspector-meta-item" ><span class ="inspector-meta-label" >Run</span><span class ="inspector-meta-value" >${esc(state.scenarioRun?.kind ?? "seed only" )}</span></div>
</div>
<div class ="inspector-section" >
<div class ="inspector-section-title" >Success Criteria</div>
<ul class ="criteria-list" >
${scenario.successCriteria.map((c) => `<li class ="criteria-item" ><span class ="criteria-bullet" ></span>${esc(c)}</li>`).join("" )}
</ul>
</div>
<div class ="inspector-section" >
<div class ="inspector-section-title" >Observed Outcome</div>
${
outcome
? `
${outcome.details ? `<div style="margin-bottom:12px;color:var(--text-secondary);font-size:13px" >${esc(outcome.details)}</div>` : "" }
<div class ="step-list" >
${
outcome.steps?.length
? outcome.steps
.map(
(step) => `
<div class ="step-card" >
<div class ="step-card-header" >
<span class ="step-card-name" >${esc(step.name)}</span>
${badgeHtml(step.status)}
</div>
${step.details ? `<div class ="step-card-details" >${esc(step.details)}</div>` : "" }
</div>`,
)
.join("" )
: '<div class="empty-state">No step data yet.</div>'
}
</div>`
: '<div class="empty-state">Not executed yet — seed plan only.</div>'
}
</div>
${
scenario.docsRefs?.length
? `<div class ="inspector-section" >
<div class ="inspector-section-title" >Docs</div>
<div class ="ref-list" >${scenario.docsRefs.map((r) => `<span class ="ref-tag" >${esc(r)}</span>`).join("" )}</div>
</div>`
: ""
}
${
scenario.codeRefs?.length
? `<div class ="inspector-section" >
<div class ="inspector-section-title" >Code</div>
<div class ="ref-list" >${scenario.codeRefs.map((r) => `<span class ="ref-tag" >${esc(r)}</span>`).join("" )}</div>
</div>`
: ""
}
</div>
${renderInspectorLiveTranscript(state)}
</div>`;
}
/* ===== Render: Report tab ===== */
function renderReportView(state: UiState): string {
return `
<div class ="report-view" >
<div class ="report-toolbar" >
<span class ="report-toolbar-title" >Protocol Report</span>
<button class ="btn-sm" data-action="download-report" ${state.latestReport ? "" : " disabled" }>Export Markdown</button>
</div>
<div class ="report-content" >
<pre class ="report-pre" >${esc(state.latestReport?.markdown ?? "Run the suite or self-check to generate a report." )}</pre>
</div>
</div>`;
}
/* ===== Render: Events tab ===== */
function renderEventsView(state: UiState): string {
const events = (state.snapshot?.events ?? []).slice(-60 ).toReversed();
return `
<div class ="events-view" >
<div class ="events-header" >
<span class ="events-header-title" >Event Stream</span>
<span class ="text-dimmed text-sm" >${events.length} events (newest first)</span>
</div>
<div class ="events-scroll" >
${
events.length === 0
? '<div class="empty-state" style="padding:20px">No events yet.</div>'
: events
.map((e) => {
const detail =
"thread" in e
? `${e.thread .conversationId}/${e.thread .id}`
: e.message
? `${e.message.senderId}: ${e.message.text}`
: "" ;
return `
<div class ="event-row" >
<span class ="event-kind" >${esc(e.kind)}</span>
<span class ="event-cursor" >#${e.cursor}</span>
<span class ="event-detail" >${esc(detail)}</span>
</div>`;
})
.join("" )
}
</div>
</div>`;
}
function renderCaptureView(state: UiState): string {
const sessionIds =
state.selectedCaptureSessionIds.length > 0
? state.selectedCaptureSessionIds
: state.captureSessions[0 ]?.id
? [state.captureSessions[0 ].id]
: [];
const sessions = state.captureSessions;
const rows = state.captureQueryRows;
const events = state.captureEvents;
const availableKinds = [
...new Set(
events.map((event) => event.kind).filter((value): value is string => Boolean (value)),
),
].toSorted();
const availableProviders = [
...new Set(
events.map((event) => event.provider).filter((value): value is string => Boolean (value)),
),
].toSorted();
const availableHosts = [
...new Set(
events.map((event) => event.host).filter((value): value is string => Boolean (value)),
),
].toSorted();
const normalizedSearch = state.captureSearchText.trim().toLowerCase();
const activeFilters: string[] = [];
if (state.captureKindFilter.length > 0 ) {
activeFilters.push(`kind: ${state.captureKindFilter.join(", " )}`);
}
if (state.captureProviderFilter.length > 0 ) {
activeFilters.push(`provider: ${state.captureProviderFilter.join(", " )}`);
}
if (state.captureHostFilter.length > 0 ) {
activeFilters.push(`host: ${state.captureHostFilter.join(", " )}`);
}
if (normalizedSearch) {
activeFilters.push(`search: ${state.captureSearchText.trim()}`);
}
if (state.captureHeaderMode !== "key" ) {
activeFilters.push(`headers: ${state.captureHeaderMode}`);
}
if (state.captureViewMode === "list" && state.captureGroupMode !== "none" ) {
activeFilters.push(`group: ${state.captureGroupMode}`);
}
if (state.captureViewMode === "timeline" ) {
activeFilters.push(`lanes: ${state.captureTimelineLaneMode}`);
activeFilters.push(`lane sort: ${state.captureTimelineLaneSort}`);
activeFilters.push(`zoom: ${state.captureTimelineZoom}%`);
if (state.captureTimelineFocusSelectedFlow) {
activeFilters.push("focus selected flow" );
if (state.captureTimelineFocusedLaneMode !== "all" ) {
activeFilters.push(`focused lanes: ${state.captureTimelineFocusedLaneMode}`);
}
if (state.captureTimelineFocusedLaneThreshold !== "any" ) {
activeFilters.push(`focus threshold: ${state.captureTimelineFocusedLaneThreshold}`);
}
}
if (state.captureTimelineLaneSearch.trim()) {
activeFilters.push(`lane search: ${state.captureTimelineLaneSearch.trim()}`);
}
if (state.capturePinnedLaneIds.length > 0 ) {
activeFilters.push(`pinned lanes: ${state.capturePinnedLaneIds.length}`);
}
if (state.captureTimelineSparklineMode !== "session-relative" ) {
activeFilters.push(`sparkline: ${state.captureTimelineSparklineMode}`);
}
}
if (state.captureErrorsOnly) {
activeFilters.push("errors only" );
}
const baseFilteredEvents = events.filter((event) => {
if (state.captureKindFilter.length > 0 && !state.captureKindFilter.includes(event.kind)) {
return false ;
}
if (
state.captureProviderFilter.length > 0 &&
!state.captureProviderFilter.includes(event.provider || "" )
) {
return false ;
}
if (state.captureHostFilter.length > 0 && !state.captureHostFilter.includes(event.host || "" )) {
return false ;
}
if (state.captureErrorsOnly && !event.errorText && (event.status ?? 0 ) < 400 ) {
return false ;
}
if (normalizedSearch) {
const haystack = [
event.kind,
event.protocol,
event.direction,
event.provider,
event.api,
event.model,
event.method,
event.host,
event.path,
event.status == null ? "" : String(event.status),
event.errorText,
event.payloadPreview,
event.flowId,
event.closeCode == null ? "" : String(event.closeCode),
]
.filter(Boolean )
.join(" " )
.toLowerCase();
if (!haystack.includes(normalizedSearch)) {
return false ;
}
}
return true ;
});
const minTs =
baseFilteredEvents.length > 0 ? Math.min(...baseFilteredEvents.map((event) => event.ts)) : 0 ;
const maxTs =
baseFilteredEvents.length > 0 ? Math.max(...baseFilteredEvents.map((event) => event.ts)) : 0 ;
const totalSpanMs = Math.max(1 , maxTs - minTs);
const activeWindowStartPct =
state.captureTimelineWindowStartPct != null && state.captureTimelineWindowEndPct != null
? Math.min(state.captureTimelineWindowStartPct, state.captureTimelineWindowEndPct)
: null ;
const activeWindowEndPct =
state.captureTimelineWindowStartPct != null && state.captureTimelineWindowEndPct != null
? Math.max(state.captureTimelineWindowStartPct, state.captureTimelineWindowEndPct)
: null ;
const draftWindowStartPct =
state.captureTimelineBrushAnchorPct != null && state.captureTimelineBrushCurrentPct != null
? Math.min(state.captureTimelineBrushAnchorPct, state.captureTimelineBrushCurrentPct)
: null ;
const draftWindowEndPct =
state.captureTimelineBrushAnchorPct != null && state.captureTimelineBrushCurrentPct != null
? Math.max(state.captureTimelineBrushAnchorPct, state.captureTimelineBrushCurrentPct)
: null ;
const activeWindowStartTs =
activeWindowStartPct == null ? null : minTs + totalSpanMs * (activeWindowStartPct / 100 );
const activeWindowEndTs =
activeWindowEndPct == null ? null : minTs + totalSpanMs * (activeWindowEndPct / 100 );
const activeWindowLabel =
activeWindowStartTs == null || activeWindowEndTs == null
? null
: `${formatTime(activeWindowStartTs)} → ${formatTime(activeWindowEndTs)} · ${formatDuration(
Math.max(0 , activeWindowEndTs - activeWindowStartTs),
)}`;
if (activeWindowLabel && state.captureViewMode === "timeline" ) {
activeFilters.push(`window: ${activeWindowLabel}`);
}
const filteredEvents =
state.captureViewMode === "timeline" &&
activeWindowStartPct != null &&
activeWindowEndPct != null
? baseFilteredEvents.filter((event) => {
const percent = ((event.ts - minTs) / totalSpanMs) * 100 ;
return percent >= activeWindowStartPct && percent <= activeWindowEndPct;
})
: baseFilteredEvents;
const analysisEnabled = state.captureQueryPreset !== "none" ;
const selectedSessions = sessions.filter((session) => sessionIds.includes(session.id));
const singleSelectedSession =
selectedSessions.length === 1 ? (selectedSessions[0 ] ?? null ) : null ;
const selectedSessionEventCount = selectedSessions.reduce(
(sum, session) => sum + session.eventCount,
0 ,
);
const selectedEvent =
filteredEvents.find((event) => {
const key = captureEventKey(event);
return key === state.selectedCaptureEventKey;
}) ??
filteredEvents[0 ] ??
null ;
const selectedEventKey = selectedEvent == null ? null : captureEventKey(selectedEvent);
const kindCounts = new Map<string, number>();
for (const event of filteredEvents) {
kindCounts.set(event.kind, (kindCounts.get(event.kind) ?? 0 ) + 1 );
}
const topKinds = [...kindCounts.entries()].toSorted((a, b) => b[1 ] - a[1 ]).slice(0 , 4 );
const topProviders = state.captureCoverage?.providers.slice(0 , 4 ) ?? [];
const topModels = state.captureCoverage?.models.slice(0 , 3 ) ?? [];
const selectedFlowId = selectedEvent?.flowId?.trim() || "" ;
const selectedFlowEvents =
selectedFlowId.length > 0
? events
.filter((event) => event.flowId === selectedFlowId)
.toSorted(
(left, right) =>
left.ts - right.ts || captureEventKey(left).localeCompare(captureEventKey(right)),
)
: [];
const selectedFlowIndex =
selectedEvent == null
? -1
: selectedFlowEvents.findIndex(
(event) => captureEventKey(event) === captureEventKey(selectedEvent),
);
const previousFlowEvent =
selectedFlowIndex > 0 ? selectedFlowEvents[selectedFlowIndex - 1 ] : null ;
const nextFlowEvent =
selectedFlowIndex >= 0 && selectedFlowIndex < selectedFlowEvents.length - 1
? selectedFlowEvents[selectedFlowIndex + 1 ]
: null ;
const selectedPairing = findPairedCaptureEvent(selectedEvent, events);
const pairedEvent = selectedPairing.counterpart;
const pairedEventKey = pairedEvent ? captureEventKey(pairedEvent) : null ;
const pairedEventVisible =
pairedEventKey != null &&
filteredEvents.some((event) => captureEventKey(event) === pairedEventKey);
const pairingLatencyMs =
selectedEvent && pairedEvent ? Math.max(0 , Math.abs(pairedEvent.ts - selectedEvent.ts)) : null ;
const previousFlowEventVisible =
previousFlowEvent != null &&
filteredEvents.some((event) => captureEventKey(event) === captureEventKey(previousFlowEvent));
const nextFlowEventVisible =
nextFlowEvent != null &&
filteredEvents.some((event) => captureEventKey(event) === captureEventKey(nextFlowEvent));
const timelineTrackWidthPx = Math.round(960 * (state.captureTimelineZoom / 100 ));
const timelineWidthStyle = `--capture-timeline-track-width:${timelineTrackWidthPx}px`;
const renderTimelineWindow = (
startPct: number | null ,
endPct: number | null ,
className: string,
): string => {
if (startPct == null || endPct == null ) {
return "" ;
}
const left = Math.max(0 , Math.min(100 , startPct));
const width = Math.max(0 , Math.min(100 , endPct) - left);
return `<div class ="${className}" style="left:${left.toFixed(2)}%;width:${width.toFixed(2)}%" ></div>`;
};
const timelineAxisTicks = Array.from({ length: 5 }, (_, index) => {
const pct = (index / 4 ) * 100 ;
const ts = minTs + (totalSpanMs * pct) / 100 ;
return {
pct,
label: formatTime(ts),
edgeClass:
index === 0
? "capture-timeline-axis-tick-start"
: index === 4
? "capture-timeline-axis-tick-end"
: "" ,
};
});
const renderLaneSparkline = (eventsForLane: CaptureEventView[], laneId: string) => {
if (eventsForLane.length === 0 ) {
return "" ;
}
const binCount = 18 ;
const bins = Array.from({ length: binCount }, () => 0 );
const laneMinTs = eventsForLane.reduce(
(min, event) => Math.min(min, event.ts),
eventsForLane[0 ]?.ts ?? minTs,
);
const laneMaxTs = eventsForLane.reduce(
(max, event) => Math.max(max, event.ts),
eventsForLane[0 ]?.ts ?? maxTs,
);
const laneSpanMs = Math.max(1 , laneMaxTs - laneMinTs);
for (const event of eventsForLane) {
const spanStart = state.captureTimelineSparklineMode === "lane-relative" ? laneMinTs : minTs;
const spanMs =
state.captureTimelineSparklineMode === "lane-relative" ? laneSpanMs : totalSpanMs;
const rawIndex = spanMs <= 0 ? 0 : Math.floor(((event.ts - spanStart) / spanMs) * binCount);
const index = Math.max(0 , Math.min(binCount - 1 , rawIndex));
bins[index] += 1 ;
}
const maxBin = Math.max(...bins, 1 );
return `<div class ="capture-timeline-sparkline" >
${bins
.map((count, index) => {
const height = Math.max(12 , Math.round((count / maxBin) * 100 ));
const spanStartTs =
state.captureTimelineSparklineMode === "lane-relative"
? laneMinTs + (laneSpanMs * index) / binCount
: minTs + (totalSpanMs * index) / binCount;
const spanEndTs =
state.captureTimelineSparklineMode === "lane-relative"
? laneMinTs + (laneSpanMs * (index + 1 )) / binCount
: minTs + (totalSpanMs * (index + 1 )) / binCount;
const startPct = ((spanStartTs - minTs) / Math.max(1 , totalSpanMs)) * 100 ;
const endPct = ((spanEndTs - minTs) / Math.max(1 , totalSpanMs)) * 100 ;
const binLabel = `${laneId} · ${formatTime(spanStartTs)} → ${formatTime(spanEndTs)} · ${count} events`;
return `<button
class ="capture-timeline-sparkline-bar"
data-capture-sparkline-window="${esc(laneId)}:${index}"
data-capture-window-start="${startPct.toFixed(4)}"
data-capture-window-end="${endPct.toFixed(4)}"
type="button"
title="${esc(`${binLabel} · click/drag: custom window · Shift+drag: wider context`)}"
style="height:${height}%"
></button>`;
})
.join("" )}
</div>`;
};
const computeLaneSeverity = (eventsForLane: CaptureEventView[]) => {
const total = eventsForLane.length;
const errorCount = eventsForLane.filter(
(event) => Boolean (event.errorText) || (event.status ?? 0 ) >= 400 ,
).length;
const focusedCount = selectedFlowId
? eventsForLane.filter((event) => event.flowId === selectedFlowId).length
: 0 ;
const recencyScore =
total === 0
? 0
: eventsForLane.reduce((max, event) => Math.max(max, event.ts), 0 ) / Math.max(1 , maxTs);
const errorShare = total > 0 ? errorCount / total : 0 ;
const focusedShare = total > 0 ? focusedCount / total : 0 ;
return (
errorCount * 10 +
errorShare * 30 +
focusedShare * 35 +
recencyScore * 8 +
Math.min(total, 40 ) * 0 .2
);
};
const describeLaneSeverity = (eventsForLane: CaptureEventView[]) => {
const total = eventsForLane.length;
const errorCount = eventsForLane.filter(
(event) => Boolean (event.errorText) || (event.status ?? 0 ) >= 400 ,
).length;
const focusedCount = selectedFlowId
? eventsForLane.filter((event) => event.flowId === selectedFlowId).length
: 0 ;
const newestTs =
total === 0 ? 0 : eventsForLane.reduce((max, event) => Math.max(max, event.ts), 0 );
const recencyMinutes =
newestTs > 0 ? Math.max(0 , Math.round((maxTs - newestTs) / 60000 )) : null ;
const focusedPercent = total > 0 ? Math.round((focusedCount / total) * 100 ) : 0 ;
const errorPercent = total > 0 ? Math.round((errorCount / total) * 100 ) : 0 ;
const reasons: string[] = [];
if (errorCount > 0 ) {
reasons.push(`${errorCount} errors (${errorPercent}%)`);
}
if (selectedFlowId && focusedCount > 0 ) {
reasons.push(`focused flow ${focusedPercent}%`);
}
if (recencyMinutes != null ) {
reasons.push(recencyMinutes === 0 ? "active now" : `${recencyMinutes}m old`);
}
if (total > 0 ) {
reasons.push(`${total} events`);
}
return {
score: computeLaneSeverity(eventsForLane),
summary: reasons.join(" · " ),
};
};
const unsortedTimelineLanes = Array.from(
filteredEvents.reduce((lanes, event) => {
const providerLabel = event.provider || "unlabeled" ;
const flowLabel = event.flowId || "(no flow id)" ;
const laneConfig =
state.captureTimelineLaneMode === "provider"
? {
id: providerLabel,
label: providerLabel,
meta: [event.host, event.api, event.model].filter(Boolean ).join(" · " ),
}
: state.captureTimelineLaneMode === "flow"
? {
id: flowLabel,
label: flowLabel,
meta: [event.provider, event.host, event.path].filter(Boolean ).join(" · " ),
}
: {
id: event.host || "(no host)" ,
label: event.host || "(no host)" ,
meta: [event.provider, event.model].filter(Boolean ).join(", " ),
};
const laneId = laneConfig.id;
const existing = lanes.get(laneId);
if (existing) {
existing.events.push(event);
return lanes;
}
lanes.set(laneId, {
id: laneId,
label: laneConfig.label,
meta: laneConfig.meta,
events: [event],
});
return lanes;
}, new Map()),
).map(([, lane]) => lane);
const sortTimelineLanes = (
lanes: Array<{ id: string; label: string; meta: string; events: CaptureEventView[] }>,
mode: UiState["captureTimelineLaneSort" ],
) =>
[...lanes].toSorted((a, b) => {
const aErrorCount = a.events.filter(
(event) => Boolean (event.errorText) || (event.status ?? 0 ) >= 400 ,
).length;
const bErrorCount = b.events.filter(
(event) => Boolean (event.errorText) || (event.status ?? 0 ) >= 400 ,
).length;
if (mode === "severity" ) {
return (
computeLaneSeverity(b.events) - computeLaneSeverity(a.events) ||
bErrorCount - aErrorCount ||
b.events.length - a.events.length ||
a.label.localeCompare(b.label)
);
}
if (mode === "most-errors" ) {
return (
bErrorCount - aErrorCount ||
b.events.length - a.events.length ||
a.label.localeCompare(b.label)
);
}
if (mode === "alphabetical" ) {
return a.label.localeCompare(b.label);
}
return (
b.events.length - a.events.length ||
bErrorCount - aErrorCount ||
a.label.localeCompare(b.label)
);
});
const timelineLanes = sortTimelineLanes(unsortedTimelineLanes, state.captureTimelineLaneSort);
const previousTimelineLanes =
state.captureTimelinePreviousLaneSort == null
? null
: sortTimelineLanes(unsortedTimelineLanes, state.captureTimelinePreviousLaneSort);
const previousLanePosition = new Map<string, number>(
(previousTimelineLanes ?? []).map((lane, index) => [lane.id, index]),
);
const laneSearch = state.captureTimelineLaneSearch.trim().toLowerCase();
const collapsedLaneIds = new Set(state.captureCollapsedLaneIds);
const pinnedLaneIds = new Set(state.capturePinnedLaneIds);
const focusedLaneMode =
state.captureTimelineFocusSelectedFlow && selectedFlowId
? state.captureTimelineFocusedLaneMode
: "all" ;
const focusedLaneThreshold =
state.captureTimelineFocusSelectedFlow && selectedFlowId
? state.captureTimelineFocusedLaneThreshold
: "any" ;
const laneMeetsFocusedThreshold = (focusedCount: number, laneTotal: number) => {
if (focusedLaneThreshold === "events-2" ) {
return focusedCount >= 2 ;
}
if (focusedLaneThreshold === "percent-10" ) {
return laneTotal > 0 && focusedCount / laneTotal >= 0 .1 ;
}
if (focusedLaneThreshold === "percent-25" ) {
return laneTotal > 0 && focusedCount / laneTotal >= 0 .25 ;
}
return focusedCount > 0 ;
};
const visibleTimelineLanes = timelineLanes.filter((lane) => {
const focusedCount = selectedFlowId
? lane.events.filter((event) => event.flowId === selectedFlowId).length
: 0 ;
if (
focusedLaneMode === "only-matching" &&
!laneMeetsFocusedThreshold(focusedCount, lane.events.length) &&
!pinnedLaneIds.has(lane.id)
) {
return false ;
}
if (pinnedLaneIds.size > 0 && pinnedLaneIds.has(lane.id)) {
return true ;
}
if (!laneSearch) {
return pinnedLaneIds.size === 0 || !pinnedLaneIds.has(lane.id);
}
const haystack = [lane.label, lane.meta].filter(Boolean ).join(" " ).toLowerCase();
return haystack.includes(laneSearch);
});
const summaryChips = [
sessionIds.length > 1 ? `sessions: ${sessionIds.length}` : null ,
analysisEnabled ? `analysis: ${state.captureQueryPreset}` : "raw only" ,
state.captureViewMode === "timeline"
? `timeline: ${state.captureTimelineLaneMode}`
: "view: list" ,
state.captureViewMode === "timeline" ? `sort: ${state.captureTimelineLaneSort}` : null ,
state.captureViewMode === "timeline" ? `zoom: ${state.captureTimelineZoom}%` : null ,
activeFilters.length > 0 ? `filters: ${activeFilters.length}` : null ,
normalizedSearch ? `search` : null ,
].filter((value): value is string => Boolean (value));
const summaryMeta = [
`${filteredEvents.length} visible`,
selectedSessions.length > 0 ? `${selectedSessionEventCount} stored` : null ,
state.captureViewMode === "timeline" && activeWindowLabel
? `window ${activeWindowLabel}`
: null ,
state.captureViewMode === "timeline"
? `${visibleTimelineLanes.length}/${timelineLanes.length} lanes${pinnedLaneIds.size > 0 ? ` · ${pinnedLaneIds.size} pinned` : "" }`
: null ,
state.captureTimelineFocusSelectedFlow && selectedEvent?.flowId
? `focus ${selectedEvent.flowId}`
: null ,
].filter((value): value is string => Boolean (value));
const groupedEvents =
state.captureGroupMode === "none" || state.captureGroupMode === "burst"
? [{ id: "__all__" , label: "All Events" , meta: "" , events: filteredEvents }]
: Array.from(
filteredEvents.reduce((groups, event) => {
const key =
state.captureGroupMode === "flow"
? event.flowId || "(no flow)"
: [event.host || "(no host)" , event.path || "/" ].join(" " );
const label =
state.captureGroupMode === "flow"
? event.flowId || "(no flow id)"
: [event.host || "(no host)" , event.path || "/" ].join(" " );
const existing = groups.get(key);
if (existing) {
existing.events.push(event);
return groups;
}
groups.set(key, {
id: key,
label,
meta:
state.captureGroupMode === "flow"
? [event.host, event.path].filter(Boolean ).join(" " )
: event.flowId || "" ,
events: [event],
});
return groups;
}, new Map()),
).map(([, group]) => group);
const clusterEventBursts = (eventsForGroup: CaptureEventView[]) => {
const sorted = [...eventsForGroup].toSorted(
(left, right) =>
left.ts - right.ts || captureEventKey(left).localeCompare(captureEventKey(right)),
);
const clusters: Array<{
key: string;
representative: CaptureEventView;
events: CaptureEventView[];
count: number;
startTs: number;
endTs: number;
}> = [];
for (const event of sorted) {
const previous = clusters.at(-1 );
const sameShape =
previous &&
previous.representative.kind === event.kind &&
previous.representative.direction === event.direction &&
(previous.representative.provider || "" ) === (event.provider || "" ) &&
(previous.representative.host || "" ) === (event.host || "" ) &&
(previous.representative.path || "" ) === (event.path || "" ) &&
(previous.representative.method || "" ) === (event.method || "" ) &&
(previous.representative.status || 0 ) === (event.status || 0 ) &&
event.ts - previous.endTs <= 1500 ;
if (!sameShape) {
clusters.push({
key: captureEventKey(event),
representative: event,
events: [event],
count: 1 ,
startTs: event.ts,
endTs: event.ts,
});
continue ;
}
previous.events.push(event);
previous.count += 1 ;
previous.endTs = event.ts;
previous.representative = event;
}
return clusters;
};
const selectedHeaders = parseJsonObject(selectedEvent?.headersJson);
const selectedHeaderCount = selectedHeaders ? Object.keys(selectedHeaders).length : 0 ;
const selectedSensitiveHeaderCount = selectedHeaders
? Object.keys(selectedHeaders).filter((label) => isSensitiveCaptureField(label)).length
: 0 ;
const selectedPayload = renderCapturePayload(
selectedEvent?.dataText,
selectedEvent?.contentType,
{
payloadEventSort: state.capturePayloadEventSort,
payloadEventFilter: state.capturePayloadEventFilter,
},
);
const selectedMetaRows = selectedEvent
? [
{ label: "provider" , value: selectedEvent.provider ?? "unlabeled" },
{ label: "model" , value: selectedEvent.model ?? "n/a" },
{ label: "api" , value: selectedEvent.api ?? "n/a" },
{ label: "peer host" , value: selectedEvent.host ?? "n/a" },
{ label: "path" , value: selectedEvent.path ?? "n/a" },
{ label: "flow id" , value: selectedEvent.flowId },
{ label: "capture origin" , value: selectedEvent.captureOrigin ?? "runtime/default" },
{ label: "content-type" , value: selectedEvent.contentType ?? "n/a" },
].filter((row) => row.value.trim().length > 0 )
: [];
const rawPayloadBody = selectedEvent?.dataText?.length
? `<pre class ="report-pre capture-pre" >${esc(redactCaptureScalar(selectedEvent.dataText))}</pre>`
: '<div class="empty-state">No inline payload preview for this event.</div>' ;
const availableDetailViews: Array<{
value: UiState["captureDetailView" ];
label: string;
available: boolean ;
recommended: boolean ;
}> = [
{ value: "overview" , label: "Overview" , available: true , recommended: false },
{
value: "flow" ,
label: "Flow" ,
available: selectedFlowEvents.length > 0 || pairedEvent != null ,
recommended:
(selectedEvent?.kind === "request" || selectedEvent?.kind === "response" ) &&
(selectedFlowEvents.length > 1 || pairedEvent != null ),
},
{
value: "payload" ,
label: "Payload" ,
available: Boolean (selectedEvent?.dataText?.length || selectedEvent?.dataBlobId),
recommended: selectedPayload.byteLength > 0 ,
},
{
value: "headers" ,
label: "Headers" ,
available: selectedHeaderCount > 0 ,
recommended: !selectedPayload.byteLength && selectedHeaderCount > 0 ,
},
];
if (!availableDetailViews.some((view) => view.recommended && view.available)) {
availableDetailViews[0 ].recommended = true ;
}
const preferredDetailView = state.capturePreferredDetailView;
const effectiveDetailView = availableDetailViews.some(
(view) => view.value === preferredDetailView && view.available,
)
? (preferredDetailView ?? "overview" )
: availableDetailViews.some((view) => view.value === state.captureDetailView && view.available)
? state.captureDetailView
: (availableDetailViews.find((view) => view.recommended && view.available)?.value ??
"overview" );
const effectiveFlowLayout =
state.captureFlowDetailLayout ??
((selectedEvent?.kind === "request" || selectedEvent?.kind === "response" ) && pairedEvent
? "pair-first"
: "nav-first" );
const effectivePayloadLayout =
state.capturePayloadDetailLayout ?? (selectedPayload.looksStructured ? "formatted" : "raw" );
const effectivePayloadExtent = state.capturePayloadExtent;
const flowSections = {
navigation:
selectedFlowEvents.length > 0
? `<section class ="capture-detail-section" >
<div class ="capture-summary-header" >
<div class ="capture-summary-label" >Flow Navigation</div>
<div class ="capture-detail-mini-meta" >
<span class ="capture-chip" >${selectedFlowIndex + 1 } / ${selectedFlowEvents.length}</span>
<span class ="capture-chip capture-chip-muted" >${esc(selectedEvent?.flowId || "" )}</span>
</div>
</div>
<div class ="capture-nav-row" >
${
previousFlowEvent
? `<button class ="capture-nav-button" data-capture-event="${esc(captureEventKey(previousFlowEvent))}" type="button" >
<span class ="capture-nav-label" >Previous on flow</span>
<span class ="capture-nav-meta" >${esc(previousFlowEvent.kind)} · ${esc(new Date(previousFlowEvent.ts).toLocaleTimeString())}${
previousFlowEventVisible ? "" : " · outside current view"
}</span>
</button>`
: '<div class="capture-nav-placeholder">No earlier event on this flow.</div>'
}
${
nextFlowEvent
? `<button class ="capture-nav-button" data-capture-event="${esc(captureEventKey(nextFlowEvent))}" type="button" >
<span class ="capture-nav-label" >Next on flow</span>
<span class ="capture-nav-meta" >${esc(nextFlowEvent.kind)} · ${esc(new Date(nextFlowEvent.ts).toLocaleTimeString())}${
nextFlowEventVisible ? "" : " · outside current view"
}</span>
</button>`
: '<div class="capture-nav-placeholder">No later event on this flow.</div>'
}
</div>
</section>`
: '<div class="empty-state">This event does not have a usable flow.</div>' ,
pair: pairedEvent
? `<section class ="capture-detail-section" >
<div class ="capture-summary-header" >
<div class ="capture-summary-label" >Paired ${esc(selectedPairing.role || "counterpart" )}</div>
<div class ="capture-detail-mini-meta" >
${pairingLatencyMs != null ? `<span class ="capture-chip" >${formatDuration(pairingLatencyMs)}</span>` : "" }
${
pairedEventVisible
? '<span class="capture-chip capture-chip-strong">visible now</span>'
: '<span class="capture-chip capture-chip-muted">outside current window/filter</span>'
}
</div>
</div>
<button class ="capture-pair-card" data-capture-event="${esc(captureEventKey(pairedEvent))}" type="button" >
<div class ="capture-pair-card-top" >
<strong>${esc(pairedEvent.kind)}</strong>
<span class ="text-dimmed text-sm" >${esc(new Date(pairedEvent.ts).toLocaleTimeString())}</span>
${pairedEvent.status ? `<span class ="text-dimmed text-sm" >status ${pairedEvent.status}</span>` : "" }
</div>
<div class ="capture-pair-card-target" >${esc(
[pairedEvent.method, pairedEvent.host, pairedEvent.path]
.filter(Boolean )
.join(" " ) || pairedEvent.flowId,
)}</div>
<div class ="text-dimmed text-sm" >${esc(
[pairedEvent.provider, pairedEvent.model, pairedEvent.api]
.filter(Boolean )
.join(" · " ) || "same flow" ,
)}</div>
</button>
</section>`
: selectedEvent?.kind === "request" || selectedEvent?.kind === "response"
? `<section class ="capture-detail-section" >
<div class ="capture-summary-label" >Paired ${esc(
selectedEvent.kind === "request" ? "response" : "request" ,
)}</div>
<div class ="empty-state" >No unambiguous counterpart was found on this flow.</div>
</section>`
: "" ,
};
const renderDetailView = () => {
if (!selectedEvent) {
return "" ;
}
if (effectiveDetailView === "flow" ) {
return `
<div class ="capture-detail-stack" >
<div class ="capture-subview-switch" role="radiogroup" aria-label="Flow layout" >
<label class ="capture-detail-view-option" >
<input type="radio" name="capture-flow-layout" value="nav-first" ${
effectiveFlowLayout === "nav-first" ? " checked" : ""
} />
<span>Nav first</span>
</label>
<label class ="capture-detail-view-option" >
<input type="radio" name="capture-flow-layout" value="pair-first" ${
effectiveFlowLayout === "pair-first" ? " checked" : ""
} />
<span>Pair first</span>
</label>
</div>
${effectiveFlowLayout === "pair-first" ? flowSections.pair + flowSections.navigation : flowSections.navigation + flowSections.pair}
</div>`;
}
if (effectiveDetailView === "payload" ) {
return `
<section class ="capture-detail-section" >
<div class ="capture-subview-switch" role="radiogroup" aria-label="Payload layout" >
<label class ="capture-detail-view-option" >
<input type="radio" name="capture-payload-layout" value="formatted" ${
effectivePayloadLayout === "formatted" ? " checked" : ""
} />
<span>Formatted</span>
</label>
<label class ="capture-detail-view-option" >
<input type="radio" name="capture-payload-layout" value="raw" ${
effectivePayloadLayout === "raw" ? " checked" : ""
} />
<span>Raw preview</span>
</label>
</div>
<div class ="capture-subview-switch" role="radiogroup" aria-label="Payload extent" >
<label class ="capture-detail-view-option" >
<input type="radio" name="capture-payload-extent" value="preview" ${
effectivePayloadExtent === "preview" ? " checked" : ""
} />
<span>Preview</span>
</label>
<label class ="capture-detail-view-option" >
<input type="radio" name="capture-payload-extent" value="full" ${
effectivePayloadExtent === "full" ? " checked" : ""
} />
<span>Full inline</span>
</label>
</div>
<div class ="capture-summary-header" >
<div class ="capture-summary-label" >Payload</div>
<div class ="capture-detail-mini-meta" >
<span class ="capture-chip" >${esc(selectedPayload.mode)}</span>
<span class ="capture-chip" >${esc(selectedEvent.contentType || "unknown content-type" )}</span>
${selectedPayload.byteLength > 0 ? `<span class ="capture-chip" >${selectedPayload.byteLength.toLocaleString()} bytes previewed</span>` : "" }
${
selectedPayload.mode === "sse" && selectedPayload.itemCount != null
? `<span class ="capture-chip capture-chip-muted" >${selectedPayload.visibleItemCount ?? selectedPayload.itemCount}/${selectedPayload.itemCount} frames</span>`
: ""
}
${selectedEvent.dataBlobId ? '<span class="capture-chip capture-chip-strong">blob-backed</span>' : "" }
</div>
</div>
${
selectedPayload.mode === "sse"
? `<div class ="capture-payload-toolbar" >
<div class ="capture-detail-radio-row" role="radiogroup" aria-label="Payload event sort" >
<label class ="capture-detail-view-option" >
<input type="radio" name="capture-payload-event-sort" value="stream" ${
state.capturePayloadEventSort === "stream" ? " checked" : ""
} />
<span>Stream order</span>
</label>
<label class ="capture-detail-view-option" >
<input type="radio" name="capture-payload-event-sort" value="name" ${
state.capturePayloadEventSort === "name" ? " checked" : ""
} />
<span>Name</span>
</label>
<label class ="capture-detail-view-option" >
<input type="radio" name="capture-payload-event-sort" value="size" ${
state.capturePayloadEventSort === "size" ? " checked" : ""
} />
<span>Largest first</span>
</label>
</div>
<label class ="capture-search-field capture-payload-filter-field" >Filter
<input
id="capture-payload-event-filter"
type="search"
value="${esc(state.capturePayloadEventFilter)}"
placeholder="event name, field, payload text..."
spellcheck="false"
/>
</label>
</div>`
: ""
}
<div class ="capture-detail-payload capture-detail-payload--${effectivePayloadExtent}" >
${effectivePayloadLayout === "raw" ? rawPayloadBody : selectedPayload.body}
${
effectivePayloadLayout !== "raw" && selectedPayload.looksStructured
? '<div class="text-dimmed text-sm capture-detail-note">Structured payloads are pretty-printed and secret-like fields are redacted for the UI.</div>'
: ""
}
</div>
</section>
${
selectedEvent.dataBlobId
? `<section class ="capture-detail-section" >
<div class ="capture-summary-header" >
<div class ="capture-summary-label" >Stored Blob</div>
<div class ="capture-detail-mini-meta" >
<span class ="capture-chip" >full payload</span>
</div>
</div>
<div class ="capture-detail-actions" >
<span class ="capture-mono" >${esc(selectedEvent.dataBlobId)}</span>
<a class ="btn-sm" href="/api/capture/blob?id=${encodeURIComponent(selectedEvent.dataBlobId)}" target="_blank" rel="noreferrer" >Open blob</a>
</div>
<div class ="text-dimmed text-sm capture-detail-note" >Blob access is intentionally raw and may contain unredacted content.</div>
</section>`
: ""
}`;
}
if (effectiveDetailView === "headers" ) {
return `
<section class ="capture-detail-section" >
<div class ="capture-summary-header" >
<div class ="capture-summary-label" >Headers</div>
<div class ="capture-detail-mini-meta" >
<span class ="capture-chip" >${selectedHeaderCount} captured</span>
${selectedSensitiveHeaderCount > 0 ? `<span class ="capture-chip capture-chip-warn" >${selectedSensitiveHeaderCount} redacted</span>` : "" }
<span class ="capture-chip capture-chip-muted" >${esc(state.captureHeaderMode)}</span>
</div>
</div>
${renderCaptureHeaders(selectedEvent.headersJson, state.captureHeaderMode)}
${
state.captureHeaderMode !== "hidden" && selectedSensitiveHeaderCount > 0
? '<div class="text-dimmed text-sm capture-detail-note">Sensitive header values are redacted in the UI.</div>'
: ""
}
</section>
${
selectedHeaders && state.captureHeaderMode !== "hidden"
? `<details class ="capture-detail-raw" >
<summary class ="text-dimmed text-sm" >Redacted headers JSON</summary>
<pre class ="report-pre capture-pre capture-pre-json" >${esc(
JSON.stringify(redactCaptureValue(selectedHeaders), null , 2 ),
)}</pre>
</details>`
: ""
}`;
}
return `
<div class ="capture-detail-stack" >
<section class ="capture-detail-section" >
<div class ="capture-summary-label" >Overview</div>
${renderCaptureKeyValueGrid([
{ label: "time" , value: new Date(selectedEvent.ts).toLocaleString() },
{
label: "target" ,
value:
[selectedEvent.method, selectedEvent.host, selectedEvent.path]
.filter(Boolean )
.join(" " ) || "n/a" ,
},
{
label: "provider route" ,
value:
[selectedEvent.provider, selectedEvent.model, selectedEvent.api]
.filter(Boolean )
.join(" · " ) || "unlabeled" ,
},
{ label: "capture origin" , value: selectedEvent.captureOrigin || "runtime/default" },
])}
</section>
<section class ="capture-detail-section" >
<div class ="capture-summary-label" >Fields</div>
${renderCaptureKeyValueGrid(selectedMetaRows)}
</section>
${
selectedEvent.errorText
? `<section class ="capture-detail-section" ><div class ="capture-summary-label" >Error</div><div class ="capture-error" >${esc(selectedEvent.errorText)}</div></section>`
: ""
}
</div>`;
};
return `
<div class ="events-view" >
<div class ="events-header" >
<span class ="events-header-title" >Proxy Capture</span>
<span class ="text-dimmed text-sm" >${sessions.length} sessions · ${filteredEvents.length}/${events.length} events shown</span>
</div>
<div class ="text-dimmed text-sm" style="margin-bottom:14px" >
Raw traffic always appears in <strong>Recent Events</strong>. The preset only controls the optional analysis panel.
</div>
<div class ="capture-controls-shell" >
<div class ="capture-controls-toolbar" >
<div class ="capture-controls-summary" >
<span class ="capture-chip capture-chip-muted" >${selectedSessions.length || 0 } session${selectedSessions.length === 1 ? "" : "s" }</span>
<span class ="capture-chip capture-chip-muted" >${state.captureViewMode}</span>
${
state.captureQueryPreset !== "none"
? `<span class ="capture-chip capture-chip-muted" >analysis: ${esc(state.captureQueryPreset)}</span>`
: `<span class ="capture-chip capture-chip-muted" >raw only</span>`
}
<span class ="capture-chip capture-chip-muted" >${activeFilters.length} filter${activeFilters.length === 1 ? "" : "s" }</span>
${
state.captureViewMode === "timeline"
? `<span class ="capture-chip capture-chip-muted" >lanes: ${esc(state.captureTimelineLaneMode)}</span>`
: ""
}
</div>
<div class ="capture-controls-actions" >
${
selectedSessions.length > 0
? `<button class ="btn-sm" type="button" id="capture-summary-toggle" >
${state.captureSummaryExpanded ? "Hide summary" : "Show summary" }
</button>`
: ""
}
${
activeFilters.length > 0
? `<button
id="capture-clear-filters"
class ="secondary-button capture-clear-filters"
type="button"
>Clear filters</button>`
: ""
}
<button class ="btn-sm" type="button" id="capture-controls-toggle" >
${state.captureControlsExpanded ? "Collapse controls" : "Show controls" }
</button>
</div>
</div>
${
state.captureControlsExpanded
? `<div class ="capture-controls-panel" >
<div class ="capture-controls-grid" >
<label class ="capture-session-filter" >Session
<select id="capture-session" multiple size="${Math.min(3, Math.max(2, sessions.length || 2))}" >
${sessions
.map(
(session) =>
`<option value="${esc(session.id)}" ${
sessionIds.includes(session.id) ? " selected" : ""
}>${esc(new Date(session.startedAt).toLocaleString())} · ${esc(session.mode)} · ${session.eventCount} events</option>`,
)
.join("" )}
</select>
</label>
<div class ="capture-inline-actions" >
<label class ="capture-saved-view-filter" >Saved view
<select id="capture-saved-view" >
<option value="" >apply saved view…</option>
${state.captureSavedViews
.map((view) => `<option value="${esc(view.id)}" >${esc(view.name)}</option>`)
.join("" )}
</select>
</label>
<button id="capture-save-view" class ="btn-sm" type="button" >Save view</button>
<button
id="capture-delete-view"
class ="btn-sm"
type="button" ${state.captureSavedViews.length === 0 ? " disabled" : "" }
>Delete view</button>
</div>
${
selectedSessions.length > 0
? `<div class ="capture-selected-sessions-shell" >
<div class ="capture-selected-sessions-summary" >
<span class ="capture-chip capture-chip-muted" >${selectedSessions.length} selected</span>
${
selectedSessions.length > 1
? `<button
id="capture-toggle-selected-sessions"
class ="btn-sm"
type="button"
>${state.captureSelectedSessionsExpanded ? "Hide selected" : "Manage selected" }</button>`
: ""
}
</div>
${
state.captureSelectedSessionsExpanded || selectedSessions.length === 1
? `<div class ="capture-selected-sessions" >
${selectedSessions
.map(
(session) => `<button
type="button"
class ="capture-selected-session-chip"
data-capture-session-remove="${esc(session.id)}"
title="Remove ${esc(new Date(session.startedAt).toLocaleString())}"
>
<span class ="capture-selected-session-chip-label" >${esc(new Date(session.startedAt).toLocaleString())}</span>
<span class ="capture-selected-session-chip-x" >×</span>
</button>`,
)
.join("" )}
</div>`
: ""
}
</div>`
: ""
}
<div class ="capture-inline-actions" >
<button
id="capture-delete-selected-sessions"
class ="btn-sm"
type="button" ${selectedSessions.length === 0 ? " disabled" : "" }
>Delete selected data</button>
<button
id="capture-purge-all"
class ="btn-sm"
type="button" ${sessions.length === 0 ? " disabled" : "" }
>Purge all data</button>
</div>
<label>Analysis
<select id="capture-preset" >
${(
[
"none" ,
"double-sends" ,
"retry-storms" ,
"cache-busting" ,
"ws-duplicate-frames" ,
"missing-ack" ,
"error-bursts" ,
] as CaptureQueryPreset[]
)
.map(
(preset) =>
`<option value="${preset}" ${
preset === state.captureQueryPreset ? " selected" : ""
}>${preset === "none" ? "none (show raw events only)" : preset}</option>`,
)
.join("" )}
</select>
</label>
<label>Kind
<select id="capture-kind-filter" multiple size="${Math.min(6, Math.max(3, availableKinds.length || 3))}" >
${availableKinds
.map(
(kind) =>
`<option value="${esc(kind)}" ${
state.captureKindFilter.includes(kind) ? " selected" : ""
}>${esc(kind)}</option>`,
)
.join("" )}
</select>
</label>
<label>Provider
<select id="capture-provider-filter" multiple size="${Math.min(6, Math.max(3, availableProviders.length || 3))}" >
${availableProviders
.map(
(provider) =>
`<option value="${esc(provider)}" ${
state.captureProviderFilter.includes(provider) ? " selected" : ""
}>${esc(provider)}</option>`,
)
.join("" )}
</select>
</label>
<label>Host
<select id="capture-host-filter" multiple size="${Math.min(6, Math.max(3, availableHosts.length || 3))}" >
${availableHosts
.map(
(host) =>
`<option value="${esc(host)}" ${
state.captureHostFilter.includes(host) ? " selected" : ""
}>${esc(host)}</option>`,
)
.join("" )}
</select>
</label>
<label>View
<select id="capture-view-mode" >
<option value="list" ${state.captureViewMode === "list" ? " selected" : "" }>list</option>
<option value="timeline" ${state.captureViewMode === "timeline" ? " selected" : "" }>timeline</option>
</select>
</label>
${
state.captureViewMode === "timeline"
? `
<label>Timeline Lanes
<select id="capture-timeline-lane-mode" >
<option value="domain" ${state.captureTimelineLaneMode === "domain" ? " selected" : "" }>domain</option>
<option value="provider" ${state.captureTimelineLaneMode === "provider" ? " selected" : "" }>provider</option>
<option value="flow" ${state.captureTimelineLaneMode === "flow" ? " selected" : "" }>flow</option>
</select>
</label>
<label>Lane Sort
<select id="capture-timeline-lane-sort" >
<option value="most-events" ${state.captureTimelineLaneSort === "most-events" ? " selected" : "" }>most events</option>
<option value="most-errors" ${state.captureTimelineLaneSort === "most-errors" ? " selected" : "" }>most errors</option>
<option value="severity" ${state.captureTimelineLaneSort === "severity" ? " selected" : "" }>severity</option>
<option value="alphabetical" ${state.captureTimelineLaneSort === "alphabetical" ? " selected" : "" }>alphabetical</option>
</select>
</label>
<label class ="capture-search-field" >Lane Search
<input
id="capture-timeline-lane-search"
type="search"
value="${esc(state.captureTimelineLaneSearch)}"
placeholder="provider, host, flow..."
spellcheck="false"
/>
</label>
<label>Timeline Zoom
<select id="capture-timeline-zoom" >
<option value="75" ${state.captureTimelineZoom === 75 ? " selected" : "" }>75 %</option>
<option value="100" ${state.captureTimelineZoom === 100 ? " selected" : "" }>100 %</option>
<option value="150" ${state.captureTimelineZoom === 150 ? " selected" : "" }>150 %</option>
<option value="200" ${state.captureTimelineZoom === 200 ? " selected" : "" }>200 %</option>
<option value="300" ${state.captureTimelineZoom === 300 ? " selected" : "" }>300 %</option>
</select>
</label>
<label>Sparkline
<select id="capture-timeline-sparkline-mode" >
<option value="session-relative" ${state.captureTimelineSparklineMode === "session-relative" ? " selected" : "" }>session-relative</option>
<option value="lane-relative" ${state.captureTimelineSparklineMode === "lane-relative" ? " selected" : "" }>lane-relative</option>
</select>
</label>
<button
id="capture-timeline-clear-window"
class ="secondary-button capture-clear-filters"
type="button" ${
activeWindowStartPct == null && draftWindowStartPct == null ? " disabled" : ""
}
>Clear window</button>
<label class ="capture-checkbox" >
<input
id="capture-timeline-focus-flow"
type="checkbox" ${
state.captureTimelineFocusSelectedFlow ? " checked" : ""
}${selectedEvent?.flowId ? "" : " disabled" }
/>
<span>focus selected flow</span>
</label>
<label>Focused Lanes
<select id="capture-timeline-focused-lane-mode" ${state.captureTimelineFocusSelectedFlow && selectedEvent?.flowId ? "" : " disabled" }>
<option value="all" ${state.captureTimelineFocusedLaneMode === "all" ? " selected" : "" }>show all</option>
<option value="only-matching" ${state.captureTimelineFocusedLaneMode === "only-matching" ? " selected" : "" }>only matching</option>
<option value="collapse-background" ${state.captureTimelineFocusedLaneMode === "collapse-background" ? " selected" : "" }>collapse background</option>
</select>
</label>
<label>Focus Threshold
<select id="capture-timeline-focused-lane-threshold" ${state.captureTimelineFocusSelectedFlow && selectedEvent?.flowId ? "" : " disabled" }>
<option value="any" ${state.captureTimelineFocusedLaneThreshold === "any" ? " selected" : "" }>any presence</option>
<option value="events-2" ${state.captureTimelineFocusedLaneThreshold === "events-2" ? " selected" : "" }>2 + events</option>
<option value="percent-10" ${state.captureTimelineFocusedLaneThreshold === "percent-10" ? " selected" : "" }>10 %+ of lane</option>
<option value="percent-25" ${state.captureTimelineFocusedLaneThreshold === "percent-25" ? " selected" : "" }>25 %+ of lane</option>
</select>
</label>`
: `
<label>Group
<select id="capture-group-mode" >
<option value="none" ${state.captureGroupMode === "none" ? " selected" : "" }>flat stream</option>
<option value="burst" ${state.captureGroupMode === "burst" ? " selected" : "" }>burst clusters</option>
<option value="flow" ${state.captureGroupMode === "flow" ? " selected" : "" }>flow id</option>
<option value="host-path" ${state.captureGroupMode === "host-path" ? " selected" : "" }>host + path</option>
</select>
</label>`
}
<label>Detail Pane
<select id="capture-detail-placement" >
<option value="right" ${state.captureDetailPlacement === "right" ? " selected" : "" }>right</option>
<option value="bottom" ${state.captureDetailPlacement === "bottom" ? " selected" : "" }>bottom</option>
</select>
</label>
<label>Headers
<select id="capture-header-mode" >
<option value="key" ${state.captureHeaderMode === "key" ? " selected" : "" }>key only</option>
<option value="all" ${state.captureHeaderMode === "all" ? " selected" : "" }>all</option>
<option value="hidden" ${state.captureHeaderMode === "hidden" ? " selected" : "" }>hidden</option>
</select>
</label>
<label class ="capture-search-field" >Search
<input
id="capture-search-filter"
type="search"
value="${esc(state.captureSearchText)}"
placeholder="host, path, method, status, payload..."
spellcheck="false"
/>
</label>
<label class ="capture-checkbox" >
<input id="capture-errors-only" type="checkbox" ${state.captureErrorsOnly ? " checked" : "" } />
<span>errors only</span>
</label>
</div></div>`
: ""
}
</div>
${
state.captureControlsExpanded && activeFilters.length > 0
? `<div class ="capture-active-filters" >
<span class ="capture-summary-label" style="margin:0" >Active Filters</span>
<div class ="capture-chip-row" >
${activeFilters.map((filter) => `<span class ="capture-chip capture-chip-muted" >${esc(filter)}</span>`).join("" )}
</div>
</div>`
: ""
}
${
selectedSessions.length > 0 && state.captureSummaryExpanded
? `<div class ="capture-summary capture-summary--expanded" >
<div class ="capture-summary-card" >
<div class ="capture-summary-label" >Session</div>
<div class ="capture-summary-value" >${
singleSelectedSession
? esc(new Date(singleSelectedSession.startedAt).toLocaleString())
: `${selectedSessions.length} sessions selected`
}</div>
<div class ="text-dimmed text-sm" >${
singleSelectedSession
? `${esc(singleSelectedSession.mode)} · ${singleSelectedSession.eventCount} stored events`
: `${selectedSessionEventCount} stored events across ${selectedSessions.length} sessions`
}</div>
</div>
<div class ="capture-summary-card" >
<div class ="capture-summary-label" >What You’re Seeing</div>
<div class ="capture-chip-row" >
${summaryChips.map((chip) => `<span class ="capture-chip" >${esc(chip)}</span>`).join("" )}
</div>
${
summaryMeta.length > 0
? `<div class ="capture-summary-meta text-dimmed text-sm" >${summaryMeta.map((part) => esc(part)).join(" · " )}</div>`
: ""
}
${
state.captureQueryPreset !== "none" && sessionIds.length > 1
? '<div class="capture-summary-note text-dimmed text-sm">Analysis presets currently run on a single session only. Raw traffic below is merged across the selected sessions.</div>'
: ""
}
${
state.captureViewMode === "timeline"
? '<div class="capture-summary-note text-dimmed text-sm">Keys: 1-4 views · ←/→ markers · Home/End jump · Esc clears brush · drag sparkline bins · Shift+drag widens.</div>'
: ""
}
</div>
<div class ="capture-summary-card" >
<div class ="capture-summary-label" >Visible Event Kinds</div>
<div class ="capture-chip-row" >
${
topKinds.length > 0
? topKinds
.map(
([kind, count]) =>
`<span class ="capture-chip" >${esc(kind)} · ${count}</span>`,
)
.join("" )
: '<span class="text-dimmed text-sm">No events match the current filters.</span>'
}
</div>
</div>
<div class ="capture-summary-card" >
<div class ="capture-summary-label" >Observed Providers</div>
<div class ="capture-chip-row" >
${
topProviders.length > 0
? topProviders
.map(
(provider) =>
`<span class ="capture-chip" >${esc(provider.value)} · ${provider.count}</span>`,
)
.join("" )
: '<span class="text-dimmed text-sm">No provider metadata captured for this session yet.</span>'
}
</div>
${
topModels.length > 0
? `<div class ="capture-summary-meta text-dimmed text-sm" >Top models: ${topModels
.map((model) => `${esc(model.value)} (${model.count})`)
.join(", " )}</div>`
: ""
}
${
state.captureCoverage
? `<div class ="capture-summary-meta text-dimmed text-sm" >${state.captureCoverage.totalEvents} total events · ${state.captureCoverage.unlabeledEventCount} unlabeled by provider/model/api</div>`
: ""
}
</div>
</div>`
: ""
}
<div class ="results-view" ${analysisEnabled ? ' style="grid-template-columns: minmax(420px, 1.7fr) minmax(280px, 0.9fr);"' : "" }>
<div class ="results-inspector" >
<div
class ="capture-body capture-body--detail-${state.captureDetailPlacement}"
data-capture-detail-split-root
style="--capture-detail-pane-width:${state.captureDetailSplitPct.toFixed(2)}%;"
>
<section class ="capture-main-panel" >
<div class ="inspector-section-title" >${state.captureViewMode === "timeline" ? "Timeline" : "Recent Events" }</div>
<div class ="events-scroll capture-events-scroll" >
${
events.length === 0
? `<div style="padding:20px" >${renderCaptureStartupInstructions(state.captureStartupStatus)}</div>`
: filteredEvents.length === 0
? '<div class="empty-state" style="padding:20px">No events match the current filters or search text.</div>'
: state.captureViewMode === "timeline"
? `<div class ="capture-timeline" style="${timelineWidthStyle}" >
<div class ="capture-timeline-legend" >
<span class ="capture-timeline-legend-item" ><span class ="capture-timeline-legend-dot capture-timeline-legend-dot-request" ></span>request</span>
<span class ="capture-timeline-legend-item" ><span class ="capture-timeline-legend-dot capture-timeline-legend-dot-response" ></span>response</span>
<span class ="capture-timeline-legend-item" ><span class ="capture-timeline-legend-dot capture-timeline-legend-dot-error" ></span>error</span>
<span class ="capture-timeline-legend-item" ><span class ="capture-timeline-legend-dot capture-timeline-legend-dot-ws" ></span>ws</span>
<span class ="capture-timeline-legend-item" ><span class ="capture-timeline-legend-line" ></span>flow trail</span>
${
activeWindowStartPct != null && activeWindowEndPct != null
? '<span class="capture-timeline-legend-item"><span class="capture-timeline-legend-window"></span>active window</span>'
: ""
}
</div>
<div class ="capture-timeline-axis-grid" >
<div class ="capture-timeline-axis-spacer" ></div>
<div class ="capture-timeline-viewport capture-timeline-brush-surface" data-capture-timeline-brush-surface="axis" data-capture-timeline-track-width="${timelineTrackWidthPx}" >
<div class ="capture-timeline-axis" >
${renderTimelineWindow(activeWindowStartPct, activeWindowEndPct, "capture-timeline-window" )}
${renderTimelineWindow(draftWindowStartPct, draftWindowEndPct, "capture-timeline-window capture-timeline-window-draft" )}
${timelineAxisTicks
.map(
(
tick,
) => `<div class ="capture-timeline-axis-tick ${tick.edgeClass}" style="left:${tick.pct.toFixed(2)}%" >
<span class ="capture-timeline-axis-tick-line" ></span>
<span class ="capture-timeline-axis-tick-label" >${esc(tick.label)}</span>
</div>`,
)
.join("" )}
</div>
</div>
</div>
${
visibleTimelineLanes.length === 0
? '<div class="empty-state" style="padding:20px">No timeline lanes match the current lane search.</div>'
: visibleTimelineLanes
.map((lane) => {
const laneErrorCount = lane.events.filter(
(event) =>
Boolean (event.errorText) || (event.status ?? 0 ) >= 400 ,
).length;
const laneRequestCount = lane.events.filter(
(event) => event.kind === "request" ,
).length;
const laneResponseCount = lane.events.filter(
(event) => event.kind === "response" ,
).length;
const collapsed = collapsedLaneIds.has(lane.id);
const pinned = pinnedLaneIds.has(lane.id);
const sortedLaneEvents = [...lane.events].toSorted(
(left, right) => left.ts - right.ts,
);
const markerGapPx = 16 ;
const rowStridePx = 18 ;
const baselineTopPx = 18 ;
const rowRightEdges: number[] = [];
const packedMarkers = sortedLaneEvents.map((event) => {
const key = captureEventKey(event);
const leftPct = ((event.ts - minTs) / totalSpanMs) * 100 ;
const leftPx = (leftPct / 100 ) * timelineTrackWidthPx;
let rowIndex = 0 ;
while (
rowIndex < rowRightEdges.length &&
rowRightEdges[rowIndex] > leftPx - markerGapPx
) {
rowIndex += 1 ;
}
rowRightEdges[rowIndex] = leftPx + markerGapPx;
const topPx = baselineTopPx + rowIndex * rowStridePx;
return { event, key, leftPct, leftPx, rowIndex, topPx };
});
const laneRowCount = Math.max(
1 ,
packedMarkers.reduce(
(max, marker) => Math.max(max, marker.rowIndex + 1 ),
1 ,
),
);
const laneTrackHeightPx = collapsed
? 18
: Math.max(
42 ,
baselineTopPx + (laneRowCount - 1 ) * rowStridePx + 18 ,
);
const selectedLaneEvent =
lane.events.find((event) => {
const key = captureEventKey(event);
return key === selectedEventKey;
}) ?? null ;
const selectedFlowId =
selectedLaneEvent?.flowId || selectedEvent?.flowId || "" ;
const focusSelectedFlow =
state.captureTimelineFocusSelectedFlow &&
selectedFlowId.length > 0 ;
const laneFocusedEventCount = focusSelectedFlow
? lane.events.filter((event) => event.flowId === selectedFlowId)
.length
: 0 ;
const laneBackgroundEventCount = focusSelectedFlow
? lane.events.length - laneFocusedEventCount
: 0 ;
const laneFocusedPercent =
focusSelectedFlow && lane.events.length > 0
? Math.round(
(laneFocusedEventCount / lane.events.length) * 100 ,
)
: 0 ;
const laneSelected = selectedLaneEvent != null ;
const laneSeverity = describeLaneSeverity(lane.events);
const laneMeetsThreshold = focusSelectedFlow
? laneMeetsFocusedThreshold(
laneFocusedEventCount,
lane.events.length,
)
: false ;
const autoCollapsed =
focusSelectedFlow &&
focusedLaneMode === "collapse-background" &&
!laneMeetsThreshold;
const laneCompactMetaParts = [
focusSelectedFlow
? `${laneFocusedPercent}% focus${laneBackgroundEventCount > 0 ? ` · ${laneBackgroundEventCount} bg` : "" }`
: null ,
laneErrorCount > 0 ? `${laneErrorCount} err` : null ,
state.captureTimelineLaneSort === "severity"
? laneSeverity.summary
: null ,
autoCollapsed ? "auto-collapsed" : null ,
].filter((value): value is string => Boolean (value));
const previousIndex = previousLanePosition.get(lane.id);
const currentIndex = timelineLanes.findIndex(
(candidate) => candidate.id === lane.id,
);
const laneMovement =
previousIndex == null ? null : previousIndex - currentIndex;
const laneIsCollapsed = collapsed || autoCollapsed;
const flowLinks = laneIsCollapsed
? ""
: Array.from(
packedMarkers.reduce<Map<string, typeof packedMarkers>>(
(flows, marker) => {
const flowId = marker.event.flowId?.trim();
if (!flowId) {
return flows;
}
const existing = flows.get(flowId) ?? [];
existing.push(marker);
flows.set(flowId, existing);
return flows;
},
new Map(),
),
)
.flatMap(([, markers]) => {
if (markers.length < 2 ) {
return [];
}
return markers.slice(1 ).map((marker, index) => {
const previous = markers[index];
const dx = marker.leftPx - previous.leftPx;
const dy = marker.topPx - previous.topPx;
const length = Math.sqrt(dx * dx + dy * dy);
const angle = (Math.atan2(dy, dx) * 180 ) / Math.PI;
const selected =
selectedFlowId.length > 0 &&
marker.event.flowId === selectedFlowId;
const dimmed =
focusSelectedFlow &&
marker.event.flowId !== selectedFlowId;
const paired =
pairedEventKey != null &&
captureEventKey(marker.event) === pairedEventKey;
return `<div
class ="capture-timeline-flow-link${selected ? " selected" : " "}${dimmed ? " dimmed" : " "}${paired ? " paired" : " "}"
style="left:${previous.leftPct.toFixed(2)}%;top:${previous.topPx}px;width:${length.toFixed(2)}px;transform:translateY(-50%) rotate(${angle.toFixed(2)}deg)"
></div>`;
});
})
.join("" );
const laneGuides = timelineAxisTicks
.slice(1 , -1 )
.map(
(tick) => `<div
class ="capture-timeline-guide"
style="left:${tick.pct.toFixed(2)}%"
aria-hidden="true"
></div>`,
)
.join("" );
const markers = packedMarkers
.map(({ event, key, leftPct, topPx }) => {
const selected =
selectedEventKey != null && key === selectedEventKey;
const kindClass = `capture-timeline-marker-${event.kind.replace(/[^a-z0-9 ]+/gi, "-" ).toLowerCase()}`;
const dimmed =
focusSelectedFlow && event.flowId !== selectedFlowId;
const paired =
pairedEventKey != null && key === pairedEventKey;
const label = [
formatTime(event.ts),
event.provider,
event.model,
event.kind,
event.method,
event.host,
event.path,
event.status ? `status ${event.status}` : "" ,
event.errorText ?? "" ,
]
.filter(Boolean )
.join(" · " );
return `<button
class ="capture-timeline-marker ${kindClass}${selected ? " selected" : " "}${dimmed ? " dimmed" : " "}${paired ? " paired" : " "}"
data-capture-event="${esc(key)}"
type="button"
style="left:${leftPct.toFixed(2)}%;top:${topPx}px"
title="${esc(label)}"
></button>`;
})
.join("" );
const collapsedMarkers = laneIsCollapsed
? packedMarkers
.map(({ event, key, leftPct }) => {
const selected =
selectedEventKey != null && key === selectedEventKey;
const kindClass = `capture-timeline-marker-${event.kind
.replace(/[^a-z0-9 ]+/gi, "-" )
.toLowerCase()}`;
const dimmed =
focusSelectedFlow && event.flowId !== selectedFlowId;
const paired =
pairedEventKey != null && key === pairedEventKey;
return `<button
class ="capture-timeline-marker capture-timeline-marker-mini ${kindClass}${
selected ? " selected" : ""
}${dimmed ? " dimmed" : "" }${paired ? " paired" : "" }"
data-capture-event="${esc(key)}"
type="button"
style="left:${leftPct.toFixed(2)}%;top:${baselineTopPx}px"
title="${esc(
[formatTime(event.ts), event.kind, event.host, event.path]
.filter(Boolean )
.join(" · " ),
)}"
></button>`;
})
.join("" )
: "" ;
const selectedLaneLeft =
selectedLaneEvent == null
? 50
: Math.min(
84 ,
Math.max(
16 ,
((selectedLaneEvent.ts - minTs) / totalSpanMs) * 100 ,
),
);
const quickPreview =
selectedLaneEvent && !laneIsCollapsed
? `<div class ="capture-timeline-quick-preview" style="left:${selectedLaneLeft.toFixed(2)}%" >
<div class ="capture-timeline-quick-preview-row" >
<span class ="capture-chip" >${esc(selectedLaneEvent.kind)}</span>
${
selectedLaneEvent.provider
? `<span class ="capture-chip" >${esc(selectedLaneEvent.provider)}</span>`
: ""
}
${
selectedLaneEvent.status
? `<span class ="capture-chip capture-chip-muted" >status ${selectedLaneEvent.status}</span>`
: ""
}
</div>
<div class ="capture-timeline-quick-preview-title" >${esc(
[
selectedLaneEvent.method,
selectedLaneEvent.host,
selectedLaneEvent.path,
]
.filter(Boolean )
.join(" " ) || selectedLaneEvent.flowId,
)}</div>
<div class ="capture-timeline-quick-preview-meta" >${esc(
[
new Date(selectedLaneEvent.ts).toLocaleTimeString(),
selectedLaneEvent.model,
selectedLaneEvent.api,
]
.filter(Boolean )
.join(" · " ),
)}</div>
${
selectedLaneEvent.errorText
? `<div class ="capture-timeline-quick-preview-error" >${esc(selectedLaneEvent.errorText)}</div>`
: selectedLaneEvent.payloadPreview
? `<div class ="capture-timeline-quick-preview-snippet" >${esc(selectedLaneEvent.payloadPreview)}</div>`
: ""
}
</div>`
: "" ;
return `<div class ="capture-timeline-lane${laneSelected ? " selected" : " "}" >
<div class ="capture-timeline-lane-label${laneSelected ? " selected" : " "}" >
<div class ="capture-timeline-lane-toolbar" >
<button class ="capture-timeline-lane-toggle" data-capture-lane-toggle="${esc(lane.id)}" type="button" >
<span class ="capture-timeline-lane-chevron" >${laneIsCollapsed ? "▸" : "▾" }</span>
<span class ="capture-timeline-lane-title" >${esc(lane.label)}</span>
</button>
<button class ="capture-timeline-lane-pin${pinned ? " pinned" : " "}" data-capture-lane-pin="${esc(lane.id)}" type="button" title="${pinned ? " Unpin lane" : " Pin lane"}" >
${pinned ? "★" : "☆" }
</button>
</div>
<div class ="capture-timeline-lane-meta" >${lane.events.length} event${lane.events.length === 1 ? "" : "s" }${
lane.meta ? ` · ${esc(lane.meta)}` : ""
}</div>
${
focusSelectedFlow && laneSelected
? `<div class ="capture-timeline-lane-focus-meta" >
<span class ="capture-mono" >${esc(selectedFlowId)}</span>
<span>·</span>
<span>${laneFocusedEventCount}/${lane.events.length} events focused</span>
<span>·</span>
<span>${laneFocusedPercent}% of lane</span>
${
laneBackgroundEventCount > 0
? `<span>·</span><span>${laneBackgroundEventCount} background</span>`
: ""
}
${
focusSelectedFlow && !laneMeetsThreshold
? `<span>·</span><span>below threshold</span>`
: ""
}
</div>`
: ""
}
${
autoCollapsed && laneSelected
? '<div class="capture-timeline-lane-meta">Auto-collapsed because the focused flow is not present in this lane.</div>'
: ""
}
${
!laneSelected && laneCompactMetaParts.length > 0
? `<div class ="capture-timeline-lane-compact-meta" >${esc(laneCompactMetaParts.join(" · " ))}</div>`
: ""
}
${renderLaneSparkline(lane.events, lane.id)}
<div class ="capture-timeline-lane-stats" >
<span class ="capture-timeline-stat" title="requests" >
<span class ="capture-timeline-stat-key capture-timeline-stat-key-request" >R</span>
<span class ="capture-timeline-stat-value" >${laneRequestCount}</span>
</span>
<span class ="capture-timeline-stat" title="responses" >
<span class ="capture-timeline-stat-key capture-timeline-stat-key-response" >S</span>
<span class ="capture-timeline-stat-value" >${laneResponseCount}</span>
</span>
${
laneMovement == null || laneMovement === 0
? ""
: `<span class ="capture-chip capture-chip-movement capture-timeline-inline-chip ${
laneMovement > 0 ? "up" : "down"
}">${laneMovement > 0 ? `up ${laneMovement}` : `down ${Math.abs(laneMovement)}`}</span>`
}
${
state.captureTimelineLaneSort === "severity"
? `<span class ="capture-chip capture-chip-severity capture-timeline-inline-chip" >severity ${laneSeverity.score.toFixed(1 )}</span>`
: ""
}
${
focusSelectedFlow
? `<span class ="capture-timeline-stat" title="focused flow events" >
<span class ="capture-timeline-stat-key capture-timeline-stat-key-focus" >F</span>
<span class ="capture-timeline-stat-value" >${laneFocusedEventCount}</span>
</span>`
: ""
}
${
focusSelectedFlow && laneBackgroundEventCount > 0
? `<span class ="capture-timeline-stat" title="background events" >
<span class ="capture-timeline-stat-key capture-timeline-stat-key-background" >B</span>
<span class ="capture-timeline-stat-value" >${laneBackgroundEventCount}</span>
</span>`
: ""
}
${
laneErrorCount > 0
? `<span class ="capture-timeline-stat capture-timeline-stat-danger" title="errors" >
<span class ="capture-timeline-stat-key capture-timeline-stat-key-error" >!</span>
<span class ="capture-timeline-stat-value" >${laneErrorCount}</span>
</span>`
: ""
}
</div>
${
laneSelected &&
(state.captureTimelineLaneSort === "severity" ||
laneMovement != null )
? `<div class ="capture-timeline-lane-severity" >${
laneMovement == null || laneMovement === 0
? ""
: `<span class ="capture-timeline-lane-movement-copy" >${
laneMovement > 0
? `Moved up ${laneMovement} from ${state.captureTimelinePreviousLaneSort}`
: `Moved down ${Math.abs(laneMovement)} from ${state.captureTimelinePreviousLaneSort}`
}</span>${
state.captureTimelineLaneSort === "severity"
? " · "
: ""
}`
}${
state.captureTimelineLaneSort === "severity"
? esc(laneSeverity.summary)
: ""
}</div>`
: ""
}
</div>
<div class ="capture-timeline-viewport" >
<div class ="capture-timeline-lane-track${laneIsCollapsed ? " collapsed" : " "}${laneSelected ? " selected" : " "}" style="height:${laneTrackHeightPx}px" >
${renderTimelineWindow(activeWindowStartPct, activeWindowEndPct, "capture-timeline-window" )}
${renderTimelineWindow(draftWindowStartPct, draftWindowEndPct, "capture-timeline-window capture-timeline-window-draft" )}
${laneGuides}
<div class ="capture-timeline-track-line" style="top:${baselineTopPx}px" ></div>
${flowLinks}
${quickPreview}
${laneIsCollapsed ? collapsedMarkers : markers}
</div>
</div>
</div>`;
})
.join("" )
}
</div>`
: groupedEvents
.map((group) => {
const groupMeta = [
`${group.events.length} event${group.events.length === 1 ? "" : "s" }`,
group.meta,
]
.filter(Boolean )
.join(" · " );
const rows =
state.captureGroupMode === "burst"
? clusterEventBursts(group.events)
.map((cluster) => {
const event = cluster.representative;
const key = cluster.key;
const selected =
selectedEvent != null &&
key === captureEventKey(selectedEvent);
const paired =
pairedEventKey != null && key === pairedEventKey;
const glyph = captureEventGlyph(event);
return `
<button class ="capture-event-card capture-event-card-compact${selected ? " selected" : " "}${paired ? " paired" : " "}" data-capture-event="${esc(key)}" type="button" >
<div class ="capture-event-card-rail" >
<span class ="capture-glyph capture-glyph-${glyph.cls}" >${esc(glyph.label)}</span>
</div>
<div class ="capture-event-card-body" >
<div class ="capture-event-card-header" >
<div class ="capture-event-card-title-row" >
<strong>${esc(event.host || event.provider || event.kind)}</strong>
<span class ="text-dimmed text-sm" >${esc(
[event.method, event.path]
.filter(Boolean )
.join(" " ) || event.kind,
)}</span>
</div>
<div class ="capture-event-card-meta-row" >
<span class ="text-dimmed text-sm" >${cluster.count} events</span>
<span class ="text-dimmed text-sm" >${esc(formatTime(cluster.startTs))} → ${esc(formatTime(cluster.endTs))}</span>
${event.status ? `<span class ="text-dimmed text-sm" >status ${event.status}</span>` : "" }
</div>
</div>
${
event.provider || event.model
? `<div class ="text-dimmed text-sm" >${esc(
[event.provider, event.model]
.filter(Boolean )
.join(" · " ),
)}</div>`
: ""
}
${paired ? '<div class="capture-pair-badge">paired counterpart</div>' : "" }
${
event.payloadPreview
? `<div class ="capture-event-card-preview" >${esc(event.payloadPreview)}</div>`
: ""
}
"
</button>java.lang.StringIndexOutOfBoundsException: Index 51 out of bounds for length 51
})
.join("" )
: group.events
.map((event: CaptureEventView) => {
const key = captureEventKey(event);
const selected =
selectedEvent != null &&
key === captureEventKey(selectedEvent);
const paired =
pairedEventKey != null && key === pairedEventKey;
const glyph = captureEventGlyph(event);
return `
<button class ="capture-event-card capture-event-card-compact${selected ? " selected" : " "}${paired ? " paired" : " "}" data-capture-event="${esc(key)}" type="button" >
<div class ="capture-event-card-rail" >
<span class ="capture-glyph capture-glyph-${glyph.cls}" >${esc(glyph.label)}</span>
</div>
<div class ="capture-event-card-body" >
<div class ="capture-event-card-header" >
<div class ="capture-event-card-title-row" >
<strong>${esc(event.host || event.provider || event.kind)}</strong>
<span class ="text-dimmed text-sm" >${esc(
[event.method, event.path].filter(Boolean ).join(" " ) ||
event.kind,
)}</span>
</div>
<div class ="capture-event-card-meta-row" >
<span class ="text-dimmed text-sm" >${esc(new Date(event.ts).toLocaleTimeString())}</span>
${event.status ? `<span class ="text-dimmed text-sm" >status ${event.status}</span>` : "" }
${event.closeCode ? `<span class ="text-dimmed text-sm" >close ${event.closeCode}</span>` : "" }
<span class ="text-dimmed text-sm" >${esc(event.direction)} · ${esc(event.protocol)}</span>
</div>
</div>
${paired ? '<div class="capture-pair-badge">paired counterpart</div>' : "" }
${
event.provider || event.api || event.captureOrigin
? `<div class ="text-dimmed text-sm" >${esc(
[event.provider, event.api, event.captureOrigin]
.filter(Boolean )
.join(" · " ),
)}</div>`
: ""
}
${
event.payloadPreview
? `<div class ="capture-event-card-preview" >${esc(event.payloadPreview)}</div>`
: ""
}
${event.errorText ? `<div class ="capture-error" style="margin-top:8px" >${esc(event.errorText)}</div>` : "" }
</div>
</button>`;
})
.join("" );
return state.captureGroupMode === "none"
? rows
: `<section class ="capture-group" >
<div class ="capture-group-header" >
<div class ="capture-group-title" >${esc(group.label)}</div>
<div class ="capture-group-meta" >${esc(groupMeta)}</div>
</div>
${rows}
</section>`;
})
.join("" )
}
</div>
</section>
${
state.captureDetailPlacement === "right"
? `<div class ="capture-detail-splitter${state.captureDetailSplitDragging ? " dragging" : " "}" data-capture-detail-splitter role="separator" aria-orientation="vertical" aria-label="Resize detail pane" >
<span class ="capture-detail-splitter-label" >${Math.round(state.captureDetailSplitPct)}%</span>
</div>`
: ""
}
<aside class ="capture-detail-pane" >
<div class ="inspector-section-title" >Selected Event</div>
${
selectedEvent == null
? '<div class="empty-state">Select an event to inspect its details.</div>'
: `
<div class ="capture-detail-card" >
<div class ="capture-detail-view-switch" role="radiogroup" aria-label="Detail view" >
${availableDetailViews
.filter((view) => view.available)
.map(
(view) => `<label class ="capture-detail-view-option" >
<input type="radio" name="capture-detail-view" value="${view.value}" ${
effectiveDetailView === view.value ? " checked" : ""
} />
<span>${view.label}${view.recommended ? ' <em class="capture-detail-view-hint">recommended</em>' : "" }</span>
</label>`,
)
.join("" )}
</div>
<div class ="capture-detail-meta" >
<span class ="capture-chip" >${esc(selectedEvent.kind)}</span>
<span class ="capture-chip" >${esc(selectedEvent.direction)}</span>
<span class ="capture-chip" >${esc(selectedEvent.protocol)}</span>
${selectedEvent.provider ? `<span class ="capture-chip" >${esc(selectedEvent.provider)}</span>` : "" }
${selectedEvent.api ? `<span class ="capture-chip" >${esc(selectedEvent.api)}</span>` : "" }
${selectedEvent.model ? `<span class ="capture-chip" >${esc(selectedEvent.model)}</span>` : "" }
${selectedEvent.status ? `<span class ="capture-chip" >status ${selectedEvent.status}</span>` : "" }
${selectedEvent.closeCode ? `<span class ="capture-chip" >close ${selectedEvent.closeCode}</span>` : "" }
</div>
<div class ="capture-detail-view-body" >
${renderDetailView()}
</div>
</div>`
}
</aside>
</div>
</div>
<div class ="results-sidebar" ${analysisEnabled ? "" : ' style="display:none"' }>
<div class ="inspector-section-title" >Analysis Results</div>
<div class ="text-dimmed text-sm" style="margin-bottom:10px" >
${
analysisEnabled
? `Preset: ${esc(state.captureQueryPreset)}`
: "Analysis disabled. Select a preset to group the raw events."
}
</div>
<div class ="events-scroll" style="max-height: 520px" >
${
rows.length === 0
? '<div class="empty-state" style="padding:20px">This session has raw traffic, but nothing matched the selected analysis preset.</div>'
: rows
.map(
(row) =>
`<pre class ="report-pre" style="margin:0 0 10px 0" >${esc(JSON.stringify(row, null , 2 ))}</pre>`,
)
.join("" )
}
</div>
</div>
</div>
</div>`;
}
/* ===== Render: Active tab switch ===== */
function renderActiveTab(state: UiState): string {
switch (state.activeTab) {
case "chat" :
return renderChatView(state);
case "results" :
return renderResultsView(state);
case "report" :
return renderReportView(state);
case "events" :
return renderEventsView(state);
case "capture" :
return renderCaptureView(state);
default :
return renderChatView(state);
}
}
/* ===== Main render ===== */
export function renderQaLabUi(state: UiState): string {
return `
<div class ="app-shell${state.sidebarCollapsed ? " app-shell--sidebar-collapsed" : " "}" data-theme="${state.theme}" >
${renderHeader(state)}
<div class ="layout" >
${renderSidebar(state)}
<main class ="main-content" >
${renderTabBar(state)}
<div class ="tab-content" >
${renderActiveTab(state)}
</div>
</main>
</div>
</div>`;
}
Messung V0.5 in Prozent C=99 H=98 G=98
¤ Dauer der Verarbeitung: 0.76 Sekunden
(vorverarbeitet am 2026-06-10)
¤
*© Formatika GbR, Deutschland