Skip to content

Commit 3224a5b

Browse files
V48 Gate 3 (implementation-only): one-row explained telemetry title-lines + 'While <Pipeline>' processing sentence
Live-QA UX refinements to the synthesis telemetry log + processing indicator (client-side display only — the streamed contract and F19's two formal row kinds are unchanged): - Processing sentence: 'While Depositing, during Discovery, Depository Search Agent is Trying, by Reasoning over Context · 6s since last update'. * 'While <Depositing|Reading>, ' prefix from the pipeline mode — explicit page prop preferred, falling back to the mode latched from the stream's 'synthesize-asset-packs'/'mode' store (stamped onto rows by the activity builder), no prefix when unknown. * prepare_concise_context reads 'Context' in the SENTENCE (FAILSAFE_SENTENCE_NAMES); the PILL keeps 'Prepare Concise Context'. * Agent names trim their pipeline prefix client-side (shared trimPipelineAgentName): CamelCase 'Deposit'/'Read', kebab 'deposit-', and 'setup:'-style namespace prefixes — used by BOTH the title-line agent pill and the sentence. - Title-line pills: ONE inline wrapping row of all five pills (phase, agent, step, failsafe, generation, + tool) replacing the top/bottom split, in compact, desktop, and narrow layouts; corner icon + expand-arrow line kept; failsafe repair badges ('… · stitch ×N'/'chunk N'/'sum') preserved. - Rich tooltips on EVERY title-line element (corner icon + each pill) via TelemetryExplainerTrigger reusing BitcodeInlineExplainer's placement/portal machinery with the pill itself as the hover/focus trigger; two sections per tooltip (generic per-type copy + specific per-value copy) in the new telemetry-pill-explainers module with graceful fallbacks. - Each expanded row's Details/Raw Data JSON gets a copy button (pretty-printed exact payload) reusing the copyTextToClipboard insecure-context fallback. - Activity builder additionally exposes the latched mode + the CURRENT call-chain (latestContext) for page-level header trackers. - Display formatting extracted to execution-telemetry-format.ts; RunClock (m:ss / h:mm:ss) added for the page header's total-run-time clock. - Tests: stale sentence pins updated to the trimmed/prefixed phrasing; new pipelineExecutionLogTelemetryUx suite (registered in jest.config.cjs) covers the trim helper, sentence prefix + composition, failsafe naming, clock format, mode latch, and the pill row. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 1e2f89e commit 3224a5b

12 files changed

Lines changed: 1213 additions & 321 deletions

uapi/app/terminal/terminal-run-activity.ts

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,24 @@ export interface TerminalRunActivitySnapshot {
2222
error: string | null;
2323
latestWorkUpdate: any | null;
2424
iterationUpdates: any[];
25+
/**
26+
* The synthesis pipeline mode latched from the stream (the
27+
* 'synthesize-asset-packs' namespace 'mode' store carries 'deposit'|'read').
28+
* Null until the store event arrives.
29+
*/
30+
mode: 'deposit' | 'read' | null;
31+
/**
32+
* The CURRENT active call-chain: the rolling Phase→Agent→Step→Failsafe→
33+
* Generation context after the last streamed event — drives live header
34+
* trackers without re-parsing the log.
35+
*/
36+
latestContext: {
37+
phase: string | null;
38+
agent: string | null;
39+
step: string | null;
40+
failsafe: string | null;
41+
generation: string | null;
42+
} | null;
2543
}
2644

2745
export type MockRunActivitySnapshot = {
@@ -182,6 +200,10 @@ export function buildTerminalRunActivityFromEvents(
182200
const rollingContext: ExecContext = {};
183201
const toolByNode = new Map<string, { name?: string; input?: unknown }>();
184202
let rowSeq = 0;
203+
// Pipeline mode latched from the 'synthesize-asset-packs'/'mode' store —
204+
// stamped onto subsequent rows (the processing indicator's 'While
205+
// Depositing, …' prefix fallback) and surfaced on the snapshot.
206+
let pipelineMode: 'deposit' | 'read' | null = null;
185207

186208
const pushRow = (displayText: string, enriched: any) => {
187209
const text = toSafeSingleLine(displayText);
@@ -194,14 +216,21 @@ export function buildTerminalRunActivityFromEvents(
194216
outputDetails[rowKey] = enriched;
195217
};
196218

197-
const stampExecutionState = (state: ExecContext, payload: any, extra?: Record<string, unknown>) => ({
198-
...payload,
199-
executionState: { ...state, ...(extra || {}) },
200-
status: {
201-
...(payload?.status && typeof payload.status === 'object' ? payload.status : {}),
202-
executionState: { ...state, ...(extra || {}) },
203-
},
204-
});
219+
const stampExecutionState = (state: ExecContext, payload: any, extra?: Record<string, unknown>) => {
220+
const stamped = {
221+
...state,
222+
...(pipelineMode ? { pipelineMode } : {}),
223+
...(extra || {}),
224+
};
225+
return {
226+
...payload,
227+
executionState: stamped,
228+
status: {
229+
...(payload?.status && typeof payload.status === 'object' ? payload.status : {}),
230+
executionState: stamped,
231+
},
232+
};
233+
};
205234

206235
// Failsafe-repair markers derived from the execution path: a stitch-repair
207236
// generation runs under a 'stitch-<N>-gen-*' segment, a chunk task
@@ -244,6 +273,12 @@ export function buildTerminalRunActivityFromEvents(
244273
const ns = String(payload?.namespace || '');
245274
const key = String(payload?.key || '');
246275
const nodeId = String(payload?.executionNodeId || '');
276+
277+
// Latch the synthesis pipeline mode ('deposit' | 'read') from its store.
278+
if (ns === 'synthesize-asset-packs' && key === 'mode' && typeof payload?.data === 'string') {
279+
const candidate = payload.data.trim().toLowerCase();
280+
if (candidate === 'deposit' || candidate === 'read') pipelineMode = candidate;
281+
}
247282
if ((ns === 'tool' || ns === 'tools') && nodeId) {
248283
if (key === 'name' && typeof payload?.data === 'string') {
249284
const acc = toolByNode.get(nodeId) || {};
@@ -299,6 +334,10 @@ export function buildTerminalRunActivityFromEvents(
299334

300335
const latestStatusEvent = statusEvents[statusEvents.length - 1];
301336

337+
const hasLatestContext = Boolean(
338+
rollingContext.phase || rollingContext.agent || rollingContext.step,
339+
);
340+
302341
return {
303342
output: outputLines.join('\n'),
304343
outputDetails,
@@ -310,6 +349,16 @@ export function buildTerminalRunActivityFromEvents(
310349
error: streamError || errorEvent?.event?.message || errorEvent?.event?.error || null,
311350
latestWorkUpdate,
312351
iterationUpdates: Array.from(normalizedIterationUpdates.values()),
352+
mode: pipelineMode,
353+
latestContext: hasLatestContext
354+
? {
355+
phase: rollingContext.phase ?? null,
356+
agent: rollingContext.agent ?? null,
357+
step: rollingContext.step ?? null,
358+
failsafe: rollingContext.failsafe ?? null,
359+
generation: rollingContext.generation ?? null,
360+
}
361+
: null,
313362
};
314363
}
315364

@@ -339,5 +388,7 @@ export function buildTerminalRunActivityFromMock(
339388
error: snapshot.error ?? null,
340389
latestWorkUpdate: snapshot.latestWorkUpdate ?? null,
341390
iterationUpdates: snapshot.iterationUpdates || [],
391+
mode: null,
392+
latestContext: null,
342393
};
343394
}

uapi/components/base/bitcode/execution/BitcodeInlineExplainer.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import { createPortal } from 'react-dom';
66
import { cn } from '@bitcode/styling';
77
import type { BitcodeExplainer } from './bitcode-transaction-types';
88

9-
type TooltipSide = 'top' | 'bottom';
9+
export type TooltipSide = 'top' | 'bottom';
1010

11-
interface TooltipPlacement {
11+
export interface TooltipPlacement {
1212
side: TooltipSide;
1313
left: number;
1414
width: number;
@@ -46,7 +46,7 @@ function clamp(value: number, min: number, max: number) {
4646
return Math.min(Math.max(value, min), max);
4747
}
4848

49-
function resolveExplainerPlacement(trigger: HTMLElement, preferredSide: TooltipSide): TooltipPlacement {
49+
export function resolveExplainerPlacement(trigger: HTMLElement, preferredSide: TooltipSide): TooltipPlacement {
5050
if (typeof window === 'undefined') {
5151
return {
5252
side: preferredSide,
@@ -110,7 +110,7 @@ function resolveExplainerPlacement(trigger: HTMLElement, preferredSide: TooltipS
110110
};
111111
}
112112

113-
function tooltipPositionStyle(placement: TooltipPlacement): React.CSSProperties {
113+
export function tooltipPositionStyle(placement: TooltipPlacement): React.CSSProperties {
114114
return {
115115
left: placement.left,
116116
width: placement.width,
@@ -119,7 +119,7 @@ function tooltipPositionStyle(placement: TooltipPlacement): React.CSSProperties
119119
};
120120
}
121121

122-
function tooltipArrowClassName({ side }: TooltipPlacement) {
122+
export function tooltipArrowClassName({ side }: TooltipPlacement) {
123123
const sideClassName =
124124
side === 'bottom'
125125
? '-top-[7px] border-x-[7px] border-b-[7px] border-x-transparent border-b-[rgba(4,8,18,0.98)]'
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
'use client';
2+
3+
import React from 'react';
4+
5+
import { PathPill, type PillType } from './PathPill';
6+
import { TelemetryExplainerTrigger } from './TelemetryExplainerTrigger';
7+
import { getTelemetryPillExplainer } from './telemetry-pill-explainers';
8+
import {
9+
formatFailsafeName,
10+
formatGenerationName,
11+
normalizePhaseName,
12+
normalizeStepName,
13+
trimPipelineAgentName,
14+
type SynthesisPipelineMode,
15+
} from './execution-telemetry-format';
16+
17+
export interface ExecutionContextPillRowProps {
18+
phase?: string | null;
19+
agent?: string | null;
20+
step?: string | null;
21+
failsafe?: string | null;
22+
generation?: string | null;
23+
tool?: string | null;
24+
/** Failsafe-repair markers rendered as pill badges ('… · stitch ×N'). */
25+
stitchIteration?: number;
26+
chunkIndex?: number;
27+
chunkSum?: boolean;
28+
/** Pipeline mode, when known — sharpens the phase tooltip copy. */
29+
mode?: SynthesisPipelineMode | string | null;
30+
className?: string;
31+
}
32+
33+
/**
34+
* Badge real failsafe-handling work on the pill label: 'stitch ×N' marks the
35+
* Nth stitch repair; 'chunk N' a chunk task generation; 'sum' the chunk
36+
* summing generation. Absent markers = the non-triggering path.
37+
*/
38+
export function buildFailsafePillLabel(args: {
39+
failsafe: string;
40+
stitchIteration?: number;
41+
chunkIndex?: number;
42+
chunkSum?: boolean;
43+
}): string {
44+
let label = formatFailsafeName(args.failsafe);
45+
if (typeof args.stitchIteration === 'number' && args.stitchIteration > 0) {
46+
label = `${label} · stitch ×${args.stitchIteration}`;
47+
} else if (args.chunkSum) {
48+
label = `${label} · sum`;
49+
} else if (typeof args.chunkIndex === 'number') {
50+
label = `${label} · chunk ${args.chunkIndex}`;
51+
}
52+
return label;
53+
}
54+
55+
/**
56+
* ONE inline, wrapping row of call-chain pills in canonical order — phase,
57+
* agent, step, failsafe, generation (+ tool) — each wrapped as a rich-tooltip
58+
* trigger. Used by the log title-lines and by /deposit's live header tracker
59+
* so the call chain always reads identically.
60+
*/
61+
export function ExecutionContextPillRow({
62+
phase,
63+
agent,
64+
step,
65+
failsafe,
66+
generation,
67+
tool,
68+
stitchIteration,
69+
chunkIndex,
70+
chunkSum,
71+
mode,
72+
className = '',
73+
}: ExecutionContextPillRowProps) {
74+
const pills: { type: PillType; label: string; raw: string }[] = [];
75+
if (phase) pills.push({ type: 'phase', label: normalizePhaseName(phase) || phase, raw: phase });
76+
if (agent) pills.push({ type: 'agent', label: trimPipelineAgentName(agent), raw: agent });
77+
if (step) pills.push({ type: 'step', label: normalizeStepName(step), raw: step });
78+
if (failsafe) {
79+
pills.push({
80+
type: 'failsafe',
81+
label: buildFailsafePillLabel({ failsafe, stitchIteration, chunkIndex, chunkSum }),
82+
raw: failsafe,
83+
});
84+
}
85+
if (generation) pills.push({ type: 'generation', label: formatGenerationName(generation), raw: generation });
86+
if (tool) pills.push({ type: 'tool', label: tool, raw: tool });
87+
88+
if (pills.length === 0) return null;
89+
90+
return (
91+
<div className={`flex min-w-0 flex-wrap items-center gap-1 ${className}`}>
92+
{pills.map((pill) => (
93+
<TelemetryExplainerTrigger
94+
key={pill.type}
95+
explainer={getTelemetryPillExplainer(pill.type, pill.raw, mode)}
96+
>
97+
<PathPill type={pill.type} label={pill.label} />
98+
</TelemetryExplainerTrigger>
99+
))}
100+
</div>
101+
);
102+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
'use client';
2+
3+
import React, { useEffect, useState } from 'react';
4+
5+
/**
6+
* Format an elapsed duration as a run clock: 'm:ss' under an hour
7+
* ('0:07', '4:53'), 'h:mm:ss' from one hour up ('1:04:09'). Negative and
8+
* non-finite inputs clamp to '0:00'. Pure + exported for unit testing.
9+
*/
10+
export function formatRunClock(elapsedMs: number): string {
11+
const totalSeconds = Number.isFinite(elapsedMs) ? Math.max(0, Math.floor(elapsedMs / 1000)) : 0;
12+
const hours = Math.floor(totalSeconds / 3600);
13+
const minutes = Math.floor((totalSeconds % 3600) / 60);
14+
const seconds = totalSeconds % 60;
15+
const two = (n: number) => String(n).padStart(2, '0');
16+
return hours > 0 ? `${hours}:${two(minutes)}:${two(seconds)}` : `${minutes}:${two(seconds)}`;
17+
}
18+
19+
interface RunClockProps {
20+
/** Run start (ms since epoch) — typically the first event's created_at. */
21+
startedAtMs: number | null;
22+
/** Ticks once a second while true; freezes when it flips false. */
23+
running: boolean;
24+
/** Optional authoritative end (e.g. the completion event's created_at). */
25+
endedAtMs?: number | null;
26+
className?: string;
27+
}
28+
29+
/**
30+
* TOTAL RUN TIME clock: ticks once a second while the run is processing and
31+
* freezes on completion/error (at `endedAtMs` when known, else at the last
32+
* tick).
33+
*/
34+
export function RunClock({ startedAtMs, running, endedAtMs, className = '' }: RunClockProps) {
35+
const [nowMs, setNowMs] = useState(() => Date.now());
36+
37+
useEffect(() => {
38+
if (!running) return;
39+
setNowMs(Date.now());
40+
const id = setInterval(() => setNowMs(Date.now()), 1000);
41+
return () => clearInterval(id);
42+
}, [running]);
43+
44+
if (startedAtMs === null || !Number.isFinite(startedAtMs)) return null;
45+
46+
const endMs = running ? nowMs : endedAtMs ?? nowMs;
47+
return (
48+
<span className={`tabular-nums ${className}`} data-testid="telemetry-run-clock" title="Total run time">
49+
{formatRunClock(endMs - startedAtMs)}
50+
</span>
51+
);
52+
}

0 commit comments

Comments
 (0)