From ca66bff02b42f251c0dfc1c68c62ef80cbc8d83d Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Mon, 1 Jun 2026 21:02:54 +0200 Subject: [PATCH] feat(automations): render live step ticker, defer REST until terminal Step results now render from the synced Zero row so the panel ticks forward as the run progresses. The REST getRun call is gated on the run reaching a terminal status, since output/artifacts/error are only written at terminal mark. --- .../components/run-details-panel.tsx | 108 ++++++++++-------- 1 file changed, 60 insertions(+), 48 deletions(-) diff --git a/surfsense_web/app/dashboard/[search_space_id]/automations/[automation_id]/components/run-details-panel.tsx b/surfsense_web/app/dashboard/[search_space_id]/automations/[automation_id]/components/run-details-panel.tsx index 164f156e5..1a54ac0e5 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/automations/[automation_id]/components/run-details-panel.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/automations/[automation_id]/components/run-details-panel.tsx @@ -15,7 +15,7 @@ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/component import { ScrollArea } from "@/components/ui/scroll-area"; import { Separator } from "@/components/ui/separator"; import { Skeleton } from "@/components/ui/skeleton"; -import type { RunStepResult } from "@/contracts/types/automation.types"; +import type { RunStatus, RunStepResult } from "@/contracts/types/automation.types"; import { useAutomationRun } from "@/hooks/use-automation-runs"; import { cn } from "@/lib/utils"; import { RunStepResultCard } from "./run-step-result-card"; @@ -23,44 +23,46 @@ import { RunStepResultCard } from "./run-step-result-card"; interface RunDetailsPanelProps { automationId: number; runId: number; + /** Live step entries from Zero; rendered while the run is in-flight and + * also kept as the authoritative source once it finishes. */ + liveSteps: RunStepResult[]; + /** Live run status from Zero. Used to hide diagnostic sections that + * only make sense after the run reaches a terminal state. */ + liveStatus: RunStatus; } /** - * Expanded view of a single run. Fetches lazily — the parent only renders - * this once the row is opened, so the list view stays cheap. + * Expanded view of a single run. Steps render immediately from the live + * Zero row so the panel updates as the run progresses; the heavy REST + * payload (output, artifacts, resolved inputs, run-level error) is + * fetched lazily and merged in when it arrives. * - * We surface the run outcome readably: a run-level error first (when - * present), then per-step cards that render the agent's markdown - * ``final_message`` directly, and finally the structural artifacts/inputs. - * The full ``definition_snapshot`` is omitted because it usually mirrors the - * live definition — surfacing it would dominate the panel without informing + * Surfacing order is outcome-first: a run-level error (when present), + * then per-step cards that render the agent's markdown ``final_message`` + * directly, and finally the structural artifacts/inputs. The full + * ``definition_snapshot`` is omitted because it usually mirrors the live + * definition — surfacing it would dominate the panel without informing * what the user is trying to learn ("did this work? what did it do?"). */ -export function RunDetailsPanel({ automationId, runId }: RunDetailsPanelProps) { - const { data: run, isLoading, error } = useAutomationRun(automationId, runId); +export function RunDetailsPanel({ + automationId, + runId, + liveSteps, + liveStatus, +}: RunDetailsPanelProps) { + const isTerminal = liveStatus !== "pending" && liveStatus !== "running"; + // Defer the REST round-trip until the run can actually carry heavy + // fields — output/artifacts/error are only written at terminal mark. + const { data: run, isLoading, error } = useAutomationRun(automationId, runId, { + enabled: isTerminal, + }); - if (isLoading) { - return ( -
- - -
- ); - } - - if (error || !run) { - return ( -
- Couldn't load run details{error?.message ? `: ${error.message}` : "."} -
- ); - } - - const runError = run.error && Object.keys(run.error).length > 0 ? run.error : null; - const hasOutput = run.output && Object.keys(run.output).length > 0; - const hasInputs = Object.keys(run.inputs ?? {}).length > 0; - const steps = run.step_results as RunStepResult[]; - const hasDiagnostics = run.artifacts.length > 0 || hasInputs; + const runError = run?.error && Object.keys(run.error).length > 0 ? run.error : null; + const hasOutput = !!run?.output && Object.keys(run.output).length > 0; + const hasInputs = !!run && Object.keys(run.inputs ?? {}).length > 0; + const hasDiagnostics = !!run && (run.artifacts.length > 0 || hasInputs); + const heavyLoading = isTerminal && isLoading && !run; + const heavyError = isTerminal && !!error; return (
@@ -72,30 +74,40 @@ export function RunDetailsPanel({ automationId, runId }: RunDetailsPanelProps) { ) : null} -
- {steps.length === 0 ? ( -

No steps recorded.

+
+ {liveSteps.length === 0 ? ( +

+ {isTerminal ? "No steps recorded." : "Waiting for first step…"} +

) : (
- {steps.map((step, index) => ( + {liveSteps.map((step, index) => ( ))}
)}
- {hasDiagnostics ? : null} - - {run.artifacts.length > 0 ? ( -
- -
- ) : null} - - {hasInputs ? ( -
- -
+ {heavyLoading ? ( + + ) : heavyError ? ( +

+ Couldn't load run details{error?.message ? `: ${error.message}` : "."} +

+ ) : hasDiagnostics ? ( + <> + + {run && run.artifacts.length > 0 ? ( +
+ +
+ ) : null} + {hasInputs ? ( +
+ +
+ ) : null} + ) : null}
);