From 74039cd1903f75a0688c3bf4fc060ca0084d4776 Mon Sep 17 00:00:00 2001 From: Matt Pua Date: Fri, 17 Jul 2026 17:02:02 -0400 Subject: [PATCH 01/10] feat: improve autoresearch live observability Generated-By: PostHog Code Task-Id: 0de359df-d297-4750-b6a2-1aa301a85429 --- .../src/autoresearch/autoresearch.test.ts | 41 ++ .../core/src/autoresearch/autoresearch.ts | 33 ++ .../src/autoresearch/autoresearchStore.ts | 15 + packages/core/src/autoresearch/schemas.ts | 16 +- packages/core/src/autoresearch/stats.test.ts | 39 +- packages/core/src/autoresearch/stats.ts | 20 + .../AutoresearchObservability.test.tsx | 311 ++++++++++++ .../AutoresearchObservability.tsx | 457 +++++++++++++++--- .../autoresearch/AutoresearchPanel.test.tsx | 20 +- .../autoresearch/AutoresearchPanel.tsx | 17 +- .../AutoresearchRuntimeStats.test.tsx | 36 +- .../autoresearch/AutoresearchRuntimeStats.tsx | 20 +- .../autoresearch/IterationsTable.test.tsx | 46 ++ .../features/autoresearch/IterationsTable.tsx | 30 +- .../autoresearch/PreBaselineState.tsx | 104 ++-- .../autoresearch/autoresearchActivity.test.ts | 266 ++++++++++ .../autoresearch/autoresearchActivity.ts | 152 +++++- 17 files changed, 1482 insertions(+), 141 deletions(-) create mode 100644 packages/ui/src/features/autoresearch/AutoresearchObservability.test.tsx create mode 100644 packages/ui/src/features/autoresearch/IterationsTable.test.tsx diff --git a/packages/core/src/autoresearch/autoresearch.test.ts b/packages/core/src/autoresearch/autoresearch.test.ts index d071303ea9..c5d52bc8f3 100644 --- a/packages/core/src/autoresearch/autoresearch.test.ts +++ b/packages/core/src/autoresearch/autoresearch.test.ts @@ -270,6 +270,7 @@ function makeRun( researchFindings: [], iterations: [], startedAt: 1_000, + pauseIntervals: [], endedAt: null, endReason: null, interruptedReason: null, @@ -941,6 +942,46 @@ describe("AutoresearchService", () => { }); describe("pause and resume", () => { + it("records and settles paused duration", () => { + vi.setSystemTime(1_000); + const run = service.startRun(baseConfig); + + vi.setSystemTime(11_000); + service.pauseRun(run.id); + expect(activeRun().pausedAt).toBe(11_000); + expect(activeRun().pausedDurationMs).toBe(0); + + vi.setSystemTime(31_000); + service.resumeRun(run.id); + expect(activeRun().pausedAt).toBeNull(); + expect(activeRun().pausedDurationMs).toBe(20_000); + expect(activeRun().pauseIntervals).toEqual([ + { startedAt: 11_000, endedAt: 31_000 }, + ]); + }); + + it("excludes interruption downtime and records its interval", async () => { + vi.setSystemTime(1_000); + service.startRun(baseConfig); + await flush(); + + vi.setSystemTime(11_000); + sessionStoreSetters.updateSession(TASK_RUN_ID, { status: "error" }); + expect(activeRun().status).toBe("interrupted"); + expect(activeRun().pausedAt).toBe(11_000); + + vi.setSystemTime(31_000); + sessionStoreSetters.updateSession(TASK_RUN_ID, { status: "connected" }); + await flush(); + + expect(activeRun().status).toBe("running"); + expect(activeRun().pausedAt).toBeNull(); + expect(activeRun().pausedDurationMs).toBe(20_000); + expect(activeRun().pauseIntervals).toEqual([ + { startedAt: 11_000, endedAt: 31_000 }, + ]); + }); + it("records iterations while paused but does not continue the loop", () => { const run = service.startRun(baseConfig); service.pauseRun(run.id); diff --git a/packages/core/src/autoresearch/autoresearch.ts b/packages/core/src/autoresearch/autoresearch.ts index 6b0f64bbab..7b399e8921 100644 --- a/packages/core/src/autoresearch/autoresearch.ts +++ b/packages/core/src/autoresearch/autoresearch.ts @@ -162,6 +162,9 @@ export class AutoresearchService { researchFindings: [], iterations: [], startedAt: Date.now(), + pausedAt: null, + pausedDurationMs: 0, + pauseIntervals: [], endedAt: null, endReason: null, interruptedReason: null, @@ -213,6 +216,7 @@ export class AutoresearchService { this.clearPendingReaction(runId); this.clearRecoveryTimer(runId); this.remindersSent.delete(runId); + this.pauseClock(runId); autoresearchStoreActions.setRunStatus(runId, "paused"); this.persist(runId); this.restoreOriginalStage(run); @@ -231,6 +235,7 @@ export class AutoresearchService { return; } + this.settlePausedClock(runId); this.clearRecoveryTimer(runId); const session = getSessionForTask( sessionStore.getState(), @@ -257,6 +262,7 @@ export class AutoresearchService { autoresearchStoreActions.setRunStatus(runId, "interrupted", { interruptedReason: run.interruptedReason ?? "session-error", }); + this.pauseClock(runId); this.persist(runId); void this.attemptRecovery(runId); return; @@ -765,6 +771,7 @@ export class AutoresearchService { if (run?.status === "running") { this.clearPendingReaction(runId); this.remindersSent.delete(runId); + this.pauseClock(runId); autoresearchStoreActions.setRunStatus(runId, "paused"); this.persist(runId); this.restoreOriginalStage(run); @@ -806,6 +813,7 @@ export class AutoresearchService { this.clearPendingReaction(runId); // A fresh interruption gets its own reminder budget on resume. this.remindersSent.delete(runId); + this.pauseClock(runId); autoresearchStoreActions.setRunStatus(runId, "interrupted", { interruptedReason: reason, lastError, @@ -885,6 +893,7 @@ export class AutoresearchService { return; } + this.settlePausedClock(runId); this.clearRecoveryTimer(runId); const session = getSessionForTask( sessionStore.getState(), @@ -915,6 +924,7 @@ export class AutoresearchService { status: "completed" | "stopped" | "failed", options: { endReason: AutoresearchEndReason; lastError?: string }, ): void { + this.settlePausedClock(runId); const run = autoresearchStore.getState().runs[runId]; autoresearchStoreActions.setRunStatus(runId, status, options); this.persist(runId); @@ -928,6 +938,29 @@ export class AutoresearchService { if (run) this.restoreOriginalStage(run); } + private pauseClock(runId: string): void { + const run = autoresearchStore.getState().runs[runId]; + if (!run || run.pausedAt != null) return; + autoresearchStoreActions.setPauseTiming( + runId, + Date.now(), + run.pausedDurationMs ?? 0, + run.pauseIntervals ?? [], + ); + } + + private settlePausedClock(runId: string): void { + const run = autoresearchStore.getState().runs[runId]; + if (!run || run.pausedAt == null) return; + const endedAt = Date.now(); + autoresearchStoreActions.setPauseTiming( + runId, + null, + (run.pausedDurationMs ?? 0) + Math.max(0, endedAt - run.pausedAt), + [...(run.pauseIntervals ?? []), { startedAt: run.pausedAt, endedAt }], + ); + } + private clearPendingReaction(runId: string): void { const timer = this.pendingReactions.get(runId); if (timer) clearTimeout(timer); diff --git a/packages/core/src/autoresearch/autoresearchStore.ts b/packages/core/src/autoresearch/autoresearchStore.ts index dcf82fc0d3..7a00f53a6e 100644 --- a/packages/core/src/autoresearch/autoresearchStore.ts +++ b/packages/core/src/autoresearch/autoresearchStore.ts @@ -3,6 +3,7 @@ import { type AutoresearchEndReason, type AutoresearchInterruptionReason, type AutoresearchIteration, + type AutoresearchPauseInterval, type AutoresearchPhase, type AutoresearchResearchFinding, type AutoresearchRun, @@ -75,6 +76,20 @@ export const autoresearchStoreActions = { updateRun(runId, (run) => ({ ...run, phase })); }, + setPauseTiming( + runId: string, + pausedAt: number | null, + pausedDurationMs: number, + pauseIntervals: AutoresearchPauseInterval[], + ): void { + updateRun(runId, (run) => ({ + ...run, + pausedAt, + pausedDurationMs, + pauseIntervals, + })); + }, + setRunStatus( runId: string, status: AutoresearchRunStatus, diff --git a/packages/core/src/autoresearch/schemas.ts b/packages/core/src/autoresearch/schemas.ts index 16d775ab7a..2278257dba 100644 --- a/packages/core/src/autoresearch/schemas.ts +++ b/packages/core/src/autoresearch/schemas.ts @@ -124,6 +124,14 @@ export function isTerminalRunStatus(status: AutoresearchRunStatus): boolean { export const autoresearchPhaseSchema = z.enum(["implement", "measure"]); export type AutoresearchPhase = z.infer; +export const autoresearchPauseIntervalSchema = z.object({ + startedAt: z.number(), + endedAt: z.number(), +}); +export type AutoresearchPauseInterval = z.infer< + typeof autoresearchPauseIntervalSchema +>; + export const autoresearchRunSchema = z.object({ id: z.string().min(1), config: autoresearchConfigSchema, @@ -150,6 +158,9 @@ export const autoresearchRunSchema = z.object({ researchFindings: z.array(autoresearchResearchFindingSchema).default([]), iterations: z.array(autoresearchIterationSchema), startedAt: z.number(), + pausedAt: z.number().nullable().optional(), + pausedDurationMs: z.number().min(0).optional(), + pauseIntervals: z.array(autoresearchPauseIntervalSchema).optional(), endedAt: z.number().nullable(), endReason: autoresearchEndReasonSchema.nullable(), interruptedReason: autoresearchInterruptionReasonSchema @@ -176,7 +187,10 @@ export function parseStoredAutoresearchRun( } const parsed = autoresearchRunSchema.safeParse(raw); if (!parsed.success) return null; - const run = parsed.data; + let run = parsed.data; + if (run.status === "paused" && run.pausedAt === undefined) { + run = { ...run, pausedAt: Date.now(), pausedDurationMs: 0 }; + } if (run.status !== "running") return run; return { ...run, diff --git a/packages/core/src/autoresearch/stats.test.ts b/packages/core/src/autoresearch/stats.test.ts index 47489a6a54..9e1efd0813 100644 --- a/packages/core/src/autoresearch/stats.test.ts +++ b/packages/core/src/autoresearch/stats.test.ts @@ -3,6 +3,7 @@ import type { AutoresearchIteration, AutoresearchRun } from "./schemas"; import { computeBest, evaluateContinuation, + getAutoresearchElapsedMs, isImprovement, meetsTarget, summarizeRun, @@ -32,6 +33,11 @@ function makeRun(overrides: { direction?: "maximize" | "minimize"; targetValue?: number | null; maxIterations?: number; + startedAt?: number; + pausedAt?: number | null; + pausedDurationMs?: number; + pauseIntervals?: AutoresearchRun["pauseIntervals"]; + endedAt?: number | null; }): AutoresearchRun { return { id: "ar-1", @@ -54,14 +60,43 @@ function makeRun(overrides: { originalEffort: null, researchFindings: [], iterations: overrides.iterations ?? [], - startedAt: 0, - endedAt: null, + startedAt: overrides.startedAt ?? 0, + pausedAt: overrides.pausedAt, + pausedDurationMs: overrides.pausedDurationMs, + pauseIntervals: overrides.pauseIntervals ?? [], + endedAt: overrides.endedAt ?? null, endReason: null, interruptedReason: null, lastError: null, }; } +describe("getAutoresearchElapsedMs", () => { + it("freezes while paused and excludes completed pause intervals", () => { + expect( + getAutoresearchElapsedMs( + makeRun({ startedAt: 1_000, pausedAt: 11_000 }), + 31_000, + ), + ).toBe(10_000); + expect( + getAutoresearchElapsedMs( + makeRun({ startedAt: 1_000, pausedDurationMs: 20_000 }), + 36_000, + ), + ).toBe(15_000); + expect( + getAutoresearchElapsedMs( + makeRun({ + startedAt: 1_000, + pauseIntervals: [{ startedAt: 11_000, endedAt: 31_000 }], + }), + 36_000, + ), + ).toBe(15_000); + }); +}); + describe("isImprovement", () => { it.each([ ["maximize", 5, 3, true], diff --git a/packages/core/src/autoresearch/stats.ts b/packages/core/src/autoresearch/stats.ts index 5c0165d766..d9c7413aec 100644 --- a/packages/core/src/autoresearch/stats.ts +++ b/packages/core/src/autoresearch/stats.ts @@ -9,6 +9,26 @@ import type { AutoresearchRun, } from "./schemas"; +export function getAutoresearchElapsedMs( + run: AutoresearchRun, + now: number, +): number { + const effectiveEnd = run.endedAt ?? run.pausedAt ?? now; + const trackedPausedDurationMs = (run.pauseIntervals ?? []).reduce( + (total, interval) => { + const overlapStart = Math.max(run.startedAt, interval.startedAt); + const overlapEnd = Math.min(effectiveEnd, interval.endedAt); + return total + Math.max(0, overlapEnd - overlapStart); + }, + 0, + ); + const pausedDurationMs = Math.max( + trackedPausedDurationMs, + run.pausedDurationMs ?? 0, + ); + return Math.max(0, effectiveEnd - run.startedAt - pausedDurationMs); +} + export function isImprovement( candidate: number, reference: number | null, diff --git a/packages/ui/src/features/autoresearch/AutoresearchObservability.test.tsx b/packages/ui/src/features/autoresearch/AutoresearchObservability.test.tsx new file mode 100644 index 0000000000..8d7dbad7b9 --- /dev/null +++ b/packages/ui/src/features/autoresearch/AutoresearchObservability.test.tsx @@ -0,0 +1,311 @@ +import type { AutoresearchRun } from "@posthog/core/autoresearch/schemas"; +import type { AcpMessage } from "@posthog/shared"; +import { Theme } from "@radix-ui/themes"; +import { render, screen, within } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { describe, expect, it } from "vitest"; +import { AutoresearchObservability } from "./AutoresearchObservability"; + +function updateEvent(ts: number, update: Record): AcpMessage { + return { + type: "acp_message", + ts, + message: { + jsonrpc: "2.0", + method: "session/update", + params: { update }, + }, + } as AcpMessage; +} + +function makeRun(overrides: Partial = {}): AutoresearchRun { + return { + id: "run-1", + config: { + taskId: "task-1", + direction: "minimize", + targetValue: null, + maxIterations: 10, + implementModel: null, + measureModel: null, + implementEffort: null, + measureEffort: null, + instructions: "Reduce memory usage.", + }, + status: "running", + metricName: "memory", + metricUnit: "MB", + phase: null, + originalModel: null, + originalEffort: null, + researchFindings: [ + { + index: 1, + summary: "Mapped the hot path", + finding: "Serialization dominates the current measurement.", + nextStep: "Inspect the serializer", + area: "workspace server", + at: 1_500, + }, + ], + iterations: [], + startedAt: 1_000, + endedAt: null, + endReason: null, + interruptedReason: null, + lastError: null, + ...overrides, + }; +} + +describe("AutoresearchObservability", () => { + it("renders timeline-shaped skeletons before the first activity", () => { + render( + + + , + ); + + const loading = screen.getByRole("status", { + name: "Loading live timeline", + }); + expect(within(loading).getAllByRole("listitem")).toHaveLength(3); + expect(loading.querySelectorAll('[data-slot="skeleton"]')).toHaveLength(10); + }); + + it("renders current findings and a reconciled newest-first timeline", () => { + const events = [ + updateEvent(2_000, { + sessionUpdate: "tool_call", + toolCallId: "search-1", + title: "Search serializers", + kind: "search", + status: "in_progress", + }), + updateEvent(3_000, { + sessionUpdate: "tool_call_update", + toolCallId: "search-1", + status: "completed", + }), + updateEvent(5_000, { + sessionUpdate: "tool_call", + toolCallId: "edit-1", + title: "Edit serializer", + kind: "edit", + status: "in_progress", + }), + ]; + + render( + + + , + ); + + expect(screen.getByText("Current findings")).toBeVisible(); + expect(screen.getByText("Mapped the hot path")).toBeVisible(); + const timeline = screen.getByText("Edit serializer").closest("ol"); + const collapsible = timeline?.closest('[data-slot="collapsible"]'); + expect(collapsible).toHaveClass( + "bg-transparent", + "hover:bg-transparent", + "data-open:bg-transparent", + ); + expect(screen.getByRole("button", { name: "Live timeline" })).toHaveClass( + "aria-expanded:bg-transparent", + ); + expect(timeline?.parentElement).toHaveClass("pt-3"); + const items = within(timeline as HTMLElement).getAllByRole("listitem"); + expect(items[0]).toHaveTextContent("Edit serializer"); + expect(items[0]).toHaveTextContent("Now"); + expect(items[0].querySelector("time")).toHaveAttribute( + "datetime", + new Date(5_000).toISOString(), + ); + expect(items[1]).toHaveTextContent("Search serializers"); + expect(items[1]).not.toHaveTextContent("Now"); + }); + + it("toggles the live timeline", async () => { + const user = userEvent.setup(); + const events = [ + updateEvent(2_000, { + sessionUpdate: "tool_call", + title: "Search serializers", + kind: "search", + status: "completed", + }), + ]; + + render( + + + , + ); + + const trigger = screen.getByRole("button", { name: "Live timeline" }); + expect(screen.getByText("Search serializers")).toBeVisible(); + + await user.click(trigger); + expect(screen.queryByText("Search serializers")).not.toBeInTheDocument(); + + await user.click(trigger); + expect(screen.getByText("Search serializers")).toBeVisible(); + }); + + it("labels only the newest live command as now", () => { + const events = [ + updateEvent(2_000, { + sessionUpdate: "tool_call", + title: "Start dev server", + kind: "execute", + status: "in_progress", + }), + updateEvent(4_000, { + sessionUpdate: "tool_call", + title: "Run benchmark", + kind: "execute", + status: "in_progress", + }), + ]; + + render( + + + , + ); + + const timeline = screen.getByText("Run benchmark").closest("ol"); + const items = within(timeline as HTMLElement).getAllByRole("listitem"); + expect(items[0]).toHaveTextContent("Run benchmark"); + expect(items[0]).toHaveTextContent("Now"); + expect(items[0].querySelector("[data-timeline-details]")).toHaveTextContent( + "Now", + ); + expect(within(items[0]).getByText("Now").parentElement).toHaveClass( + "ml-auto", + ); + expect(items[0]).not.toHaveClass("bg-blue-2/50"); + expect(within(items[0]).getByText("Now")).not.toHaveClass( + "motion-safe:animate-pulse", + ); + expect(items[1]).toHaveTextContent("Start dev server"); + expect(items[1]).toHaveTextContent("Background"); + expect(within(items[1]).getByText("Background")).toHaveClass( + "text-gray-10", + ); + expect(screen.getAllByText("Now")).toHaveLength(1); + }); + + it("renders commands as single-line code tooltip triggers", () => { + const command = + "/bin/zsh -lc 'pnpm bench:memory -- --label autoresearch-baseline'"; + const events = [ + updateEvent(2_000, { + sessionUpdate: "tool_call", + title: command, + kind: "execute", + status: "in_progress", + }), + ]; + + render( + + + , + ); + + const trigger = screen.getByText(command); + expect(trigger.tagName).toBe("CODE"); + expect(trigger.parentElement).toHaveClass( + "w-full", + "max-w-full", + "truncate", + "font-mono", + "bg-gray-3", + ); + expect(trigger.parentElement).toHaveAttribute("type", "button"); + expect( + trigger.closest("li")?.querySelector("[data-activity-icon]"), + ).toHaveClass("h-5", "w-3.5", "items-center", "justify-center"); + const commandColumn = trigger + .closest("li") + ?.querySelector("[data-timeline-command-column]"); + expect(commandColumn).toContainElement(trigger); + expect(commandColumn?.querySelector(".text-\\[11px\\]")).toBeTruthy(); + const timelineItem = trigger.closest("li"); + expect(timelineItem).toHaveClass( + "grid", + "w-full", + "min-w-0", + "grid-cols-[auto_minmax(0,1fr)]", + ); + expect(timelineItem?.parentElement).not.toHaveClass("overflow-hidden"); + }); + + it("sorts observed time from longest to shortest", () => { + const events = [ + updateEvent(2_000, { + sessionUpdate: "tool_call", + title: "Search serializers", + kind: "search", + status: "completed", + }), + updateEvent(7_000, { + sessionUpdate: "tool_call", + title: "Edit serializer", + kind: "edit", + status: "completed", + }), + ]; + + render( + + + , + ); + + const observedTime = screen.getByText("Observed time").closest("section"); + expect(observedTime).not.toBeNull(); + const rows = observedTime?.querySelectorAll("[data-observed-kind]"); + expect( + Array.from(rows ?? []).map((row) => + row.getAttribute("data-observed-kind"), + ), + ).toEqual([ + "research", + "implementation", + "reasoning", + "measurement", + "execution", + ]); + }); + + it("freezes observed time at the pause timestamp", () => { + const events = [ + updateEvent(2_000, { + sessionUpdate: "tool_call", + title: "Run benchmark", + kind: "execute", + status: "in_progress", + }), + ]; + + const { container } = render( + + + , + ); + + expect( + container.querySelector('[data-observed-kind="measurement"]'), + ).toHaveTextContent("9s"); + expect(screen.queryByText("Now")).not.toBeInTheDocument(); + }); +}); diff --git a/packages/ui/src/features/autoresearch/AutoresearchObservability.tsx b/packages/ui/src/features/autoresearch/AutoresearchObservability.tsx index f047768fff..68a49604cd 100644 --- a/packages/ui/src/features/autoresearch/AutoresearchObservability.tsx +++ b/packages/ui/src/features/autoresearch/AutoresearchObservability.tsx @@ -1,13 +1,27 @@ import { Binoculars, + Check, Code, Compass, Lightbulb, ListChecks, MagnifyingGlass, + Terminal, TestTube, } from "@phosphor-icons/react"; import type { AutoresearchRun } from "@posthog/core/autoresearch/schemas"; +import { getAutoresearchElapsedMs } from "@posthog/core/autoresearch/stats"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, + Badge as QuillBadge, + Skeleton, + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@posthog/quill"; import type { AcpMessage } from "@posthog/shared"; import { Badge, Progress, Text } from "@radix-ui/themes"; import { useEffect, useMemo, useState } from "react"; @@ -24,86 +38,288 @@ export function AutoresearchObservability({ run: AutoresearchRun; events: AcpMessage[]; }) { - const now = useLiveNow(run.endedAt === null); + const live = run.endedAt === null && run.status === "running"; + const now = useLiveNow(live); + const observationEnd = run.endedAt ?? run.pausedAt ?? now; const activity = useMemo( - () => analyzeAutoresearchActivity(events, run.startedAt, run.endedAt, now), - [events, now, run.endedAt, run.startedAt], + () => + analyzeAutoresearchActivity( + events, + run.startedAt, + run.endedAt, + observationEnd, + { + live, + pauseIntervals: run.pauseIntervals, + pausedDurationMs: run.pausedDurationMs, + }, + ), + [ + events, + live, + observationEnd, + run.endedAt, + run.pauseIntervals, + run.pausedDurationMs, + run.startedAt, + ], ); const lastIteration = run.iterations.at(-1); const hypothesis = activity.currentPlan?.hypothesis ?? lastIteration?.hypothesis; const plan = activity.currentPlan?.plan ?? lastIteration?.plan; const approach = activity.currentPlan?.approach ?? lastIteration?.approach; + const observedTime = ( + Object.keys(activity.timeByKind) as AutoresearchActivityKind[] + ).sort( + (left, right) => activity.timeByKind[right] - activity.timeByKind[left], + ); return ( -
-
- } - title="Current experiment" - /> - - - {approach && ( -
- - {approach} - -
- )} -
- -
- } title="Observed time" /> -
- {(Object.keys(activity.timeByKind) as AutoresearchActivityKind[]).map( - (kind) => ( +
+
+
+ } + title="Current experiment" + /> + + + {approach && ( +
+ + {approach} + +
+ )} +
+ +
+ } title="Observed time" /> +
+ {observedTime.map((kind) => ( - ), - )} -
-
- -
- } title="Live activity" /> - {activity.items.length === 0 ? ( - - Waiting for the first observable tool action. - - ) : ( -
    - {activity.items.map((item) => ( -
  1. - - - {item.label} - - {item.active && Active} -
  2. ))} -
- )} -
+
+
+
+ + + + + + + } + title="Live timeline" + /> + + + {activity.items.length === 0 ? ( + + ) : ( +
    + {activity.items.map((item) => ( +
  1. + + + + +
    + + + {activityLabelContent(item.command, item.label)} + + + {item.label} + + +
    + + {TIME_LABEL[item.kind]} ·{" "} + {" "} + · {formatTimelineTime(item.at - run.startedAt)} + + +
    +
    +
  2. + ))} +
+ )} +
+
+
); } +const TIMELINE_SKELETON_ROWS = [ + { id: "current", commandWidth: "w-3/5", detailWidth: "w-2/5" }, + { id: "recent", commandWidth: "w-4/5", detailWidth: "w-1/3" }, + { id: "earlier", commandWidth: "w-1/2", detailWidth: "w-2/5" }, +]; + +function TimelineLoadingState() { + return ( + +
    + {TIMELINE_SKELETON_ROWS.map((row, index) => ( +
  1. + + + + +
    + +
    + + {index === 0 && ( + + )} +
    +
    +
  2. + ))} +
+
+ ); +} + +function EmptyTimelineState({ live }: { live: boolean }) { + if (live) return ; + return ( + + No timeline activity was recorded. + + ); +} + +function CurrentFindings({ run }: { run: AutoresearchRun }) { + const findings = run.researchFindings.slice(-3).reverse(); + return ( +
+ } + title="Current findings" + /> + {findings.length === 0 ? ( + + Findings appear here as the agent records research checkpoints. + + ) : ( +
    + {findings.map((finding) => ( +
  1. +
    + + {finding.summary} + + {finding.area && } +
    + + {finding.finding} + + {finding.nextStep && ( + + Next: {finding.nextStep} + + )} +
  2. + ))} +
+ )} +
+ ); +} + +function FindingAreaBadge({ area }: { area: string }) { + return ( + + + + } + > + + {area} + + + {area} + + + ); +} + +function activityLabelTrigger(command: boolean) { + const typography = command ? "font-mono text-[11px]" : "text-xs"; + const background = command ? "bg-gray-3 px-1.5 py-0.5" : "bg-transparent"; + return ( +