diff --git a/apps/web/src/components/GenerationPreviewStage.module.css b/apps/web/src/components/GenerationPreviewStage.module.css
index 258d24549..33213a3f1 100644
--- a/apps/web/src/components/GenerationPreviewStage.module.css
+++ b/apps/web/src/components/GenerationPreviewStage.module.css
@@ -153,6 +153,20 @@
background: var(--bg-panel);
color: var(--text-muted);
font-size: 12px;
+ /* Steps are revealed one at a time as the agent reaches them, so each
+ pill slides + fades in on mount to make the progression visible. */
+ animation: stepReveal 280ms cubic-bezier(0.23, 1, 0.32, 1);
+}
+
+@keyframes stepReveal {
+ from {
+ opacity: 0;
+ transform: translateY(4px) scale(0.96);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0) scale(1);
+ }
}
.step[data-status='running'] {
@@ -207,7 +221,8 @@
.mark[data-active='true'],
.progress[data-active='true']::after,
.stepDot[data-running='true'],
- .lead[data-live='true'] {
+ .lead[data-live='true'],
+ .step {
animation: none;
}
}
diff --git a/apps/web/src/components/GenerationPreviewStage.tsx b/apps/web/src/components/GenerationPreviewStage.tsx
index 3aa6136e2..8649917c6 100644
--- a/apps/web/src/components/GenerationPreviewStage.tsx
+++ b/apps/web/src/components/GenerationPreviewStage.tsx
@@ -79,7 +79,9 @@ export function GenerationPreviewStage({ model, onRetry }: Props) {
- {model.steps.map((step) => (
+ {model.steps
+ .filter((step) => step.status !== 'pending')
+ .map((step) => (
-
{step.status === 'succeeded' ? (
diff --git a/apps/web/src/runtime/generation-preview.ts b/apps/web/src/runtime/generation-preview.ts
index a55a5a01f..21b3fad03 100644
--- a/apps/web/src/runtime/generation-preview.ts
+++ b/apps/web/src/runtime/generation-preview.ts
@@ -172,7 +172,12 @@ export function derivePrototypeGenerationSteps(input: {
let understand: GenerationStepStatus = 'running';
if (input.failed && !hasText && !hasToolUse) {
understand = 'failed';
- } else if (hasText || hasStatus(['thinking', 'streaming', 'requesting', 'starting']) || hasToolUse) {
+ } else if (hasText || hasStatus(['thinking', 'streaming']) || hasToolUse) {
+ // `requesting`/`starting` only mean the request left the client — the
+ // model hasn't produced anything yet, so we keep "understand" in
+ // progress until real thinking/output/tool activity arrives. This lets
+ // the UI reveal the steps one at a time instead of jumping straight to
+ // a fully populated row.
understand = 'succeeded';
}
@@ -241,9 +246,9 @@ function latestActivityLabel(events: AgentEvent[]): string | null {
if (event.kind === 'text' && event.text.trim() && !QUESTION_FORM_RE.test(event.text)) {
return truncateActivity(event.text);
}
- if (event.kind === 'status' && event.detail?.trim()) {
- return truncateActivity(event.detail);
- }
+ // Intentionally skip `status` details: their payload is often an
+ // internal identifier (e.g. the model slug from a `requesting` event)
+ // rather than human-readable progress, so surfacing it reads as noise.
}
return null;
}
diff --git a/apps/web/tests/runtime/generation-preview.test.ts b/apps/web/tests/runtime/generation-preview.test.ts
index 53fc9c700..a8c047247 100644
--- a/apps/web/tests/runtime/generation-preview.test.ts
+++ b/apps/web/tests/runtime/generation-preview.test.ts
@@ -46,6 +46,24 @@ describe('generation preview helpers', () => {
]);
});
+ it('keeps the understand step in progress while the request is still pending', () => {
+ // `requesting` only means the request left the client; nothing should
+ // advance past the first step until real model activity arrives, so the
+ // UI can reveal steps one at a time.
+ expect(
+ derivePrototypeGenerationSteps({
+ events: [{ kind: 'status', label: 'requesting', detail: 'claude-opus-4-7' }],
+ hasArtifactHtml: false,
+ hasPreviewSurface: false,
+ failed: false,
+ }),
+ ).toEqual([
+ { id: 'understand', status: 'running' },
+ { id: 'generate', status: 'pending' },
+ { id: 'prepare', status: 'pending' },
+ ]);
+ });
+
it('builds preview state for an active assistant run without an open preview tab', () => {
const assistant: ChatMessage = {
id: 'a1',