From 4a4905790939d056905c6704b548d0cb1cc1956d Mon Sep 17 00:00:00 2001 From: Fabro Date: Mon, 4 May 2026 18:44:15 +0000 Subject: [PATCH] fabro(01KQT1VDVXGWN9P6MFK4R5E44D): simplify_gpt (succeeded) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fabro-Run: 01KQT1VDVXGWN9P6MFK4R5E44D Fabro-Completed: 7 Fabro-Checkpoint: 07f2f2fd2dae3f75f3982468f1befa1de2cb1bc1 ⚒️ Generated with [Fabro](https://fabro.sh) --- .../app/components/stage-sidebar.tsx | 2 +- apps/fabro-web/app/lib/run-events.test.tsx | 23 ++++++++++++++++++- apps/fabro-web/app/lib/run-events.ts | 10 ++++---- apps/fabro-web/app/lib/stage-sidebar.test.ts | 6 +++-- apps/fabro-web/app/lib/stage-sidebar.ts | 2 +- apps/fabro-web/app/routes/run-overview.tsx | 7 +----- apps/fabro-web/app/routes/run-stages.test.ts | 2 +- apps/fabro-web/app/routes/run-stages.tsx | 10 ++++---- docs/public/api-reference/fabro-api.yaml | 2 +- .../src/server/handler/billing.rs | 7 +++--- lib/crates/fabro-workflow/src/lib.rs | 10 ++++---- 11 files changed, 48 insertions(+), 33 deletions(-) diff --git a/apps/fabro-web/app/components/stage-sidebar.tsx b/apps/fabro-web/app/components/stage-sidebar.tsx index 46138ac5e..2d3122602 100644 --- a/apps/fabro-web/app/components/stage-sidebar.tsx +++ b/apps/fabro-web/app/components/stage-sidebar.tsx @@ -157,4 +157,4 @@ export function StageSidebar({ stages, runId, selectedStageId, activeLink }: Sta ); -} \ No newline at end of file +} diff --git a/apps/fabro-web/app/lib/run-events.test.tsx b/apps/fabro-web/app/lib/run-events.test.tsx index 61700901c..6e323e751 100644 --- a/apps/fabro-web/app/lib/run-events.test.tsx +++ b/apps/fabro-web/app/lib/run-events.test.tsx @@ -123,6 +123,27 @@ describe("subscribeToRunEvents", () => { cleanup(); }); + test("falls back to node_id when an event has no stage_id", () => { + const source = new FakeEventSource(); + const keys: string[] = []; + const cleanup = subscribeToRunEvents( + "run-stage-node", + (key) => { + keys.push(key); + return Promise.resolve(); + }, + () => source, + { debounceMs: 0 }, + ); + + source.emit({ event: "stage.started", node_id: "verify" }); + + expect(keys).toContain(queryKeys.runs.stageTurns("run-stage-node", "verify")); + expect(keys).toContain(queryKeys.runs.stages("run-stage-node")); + + cleanup(); + }); + test("malformed events are ignored and StrictMode-style cleanup does not underflow", () => { const firstSource = new FakeEventSource(); const secondSource = new FakeEventSource(); @@ -156,4 +177,4 @@ describe("subscribeToRunEvents", () => { expect(firstSource.closed).toBe(true); expect(secondSource.closed).toBe(true); }); -}); \ No newline at end of file +}); diff --git a/apps/fabro-web/app/lib/run-events.ts b/apps/fabro-web/app/lib/run-events.ts index 28115899b..00bd93e4f 100644 --- a/apps/fabro-web/app/lib/run-events.ts +++ b/apps/fabro-web/app/lib/run-events.ts @@ -136,10 +136,10 @@ export function subscribeToRunEvents( } function stageIdFromPayload(payload: RunEventPayload): string | undefined { - // Only return a true `node_id@visit` StageId. A bare `node_id` would not - // match the suffixed `stageTurns(runId, "verify@1")` cache key, so falling - // back to it would silently no-op the invalidation. - return typeof payload.stage_id === "string" ? payload.stage_id : undefined; + if (typeof payload.stage_id === "string") return payload.stage_id; + if (typeof payload.node_id === "string") return payload.node_id; + const nodeId = payload.properties?.node_id; + return typeof nodeId === "string" ? nodeId : undefined; } export function useRunEvents(runId: string | undefined) { @@ -149,4 +149,4 @@ export function useRunEvents(runId: string | undefined) { if (!runId) return; return subscribeToRunEvents(runId, mutate as MutateFn); }, [mutate, runId]); -} \ No newline at end of file +} diff --git a/apps/fabro-web/app/lib/stage-sidebar.test.ts b/apps/fabro-web/app/lib/stage-sidebar.test.ts index c34132e14..9e8891645 100644 --- a/apps/fabro-web/app/lib/stage-sidebar.test.ts +++ b/apps/fabro-web/app/lib/stage-sidebar.test.ts @@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test"; import type { PaginatedRunStageList, StageState } from "@qltysh/fabro-api-client"; import type { Stage } from "../components/stage-sidebar"; -import { aggregateGraphNodeStatus, mapRunStagesToSidebarStages } from "./stage-sidebar"; +import { aggregateGraphNodeStatus, formatStageLabel, mapRunStagesToSidebarStages } from "./stage-sidebar"; function makeStage(nodeId: string, visit: number, status: StageState): Stage { return { @@ -44,10 +44,12 @@ describe("mapRunStagesToSidebarStages", () => { expect(result[0].id).toBe("apply-changes@1"); expect(result[0].nodeId).toBe("apply"); expect(result[0].visit).toBe(1); + expect(formatStageLabel(result[0])).toBe("Apply Changes"); expect(result[1].id).toBe("apply-changes@2"); expect(result[1].nodeId).toBe("apply"); expect(result[1].visit).toBe(2); + expect(formatStageLabel(result[1])).toBe("Apply Changes (2)"); }); test("filters by node_id (suffixed start@1 / exit@1 are still hidden)", () => { @@ -167,4 +169,4 @@ describe("aggregateGraphNodeStatus", () => { latestStageId: "apply@1", }); }); -}); \ No newline at end of file +}); diff --git a/apps/fabro-web/app/lib/stage-sidebar.ts b/apps/fabro-web/app/lib/stage-sidebar.ts index a820f5498..e44e5f4a5 100644 --- a/apps/fabro-web/app/lib/stage-sidebar.ts +++ b/apps/fabro-web/app/lib/stage-sidebar.ts @@ -71,4 +71,4 @@ export function aggregateGraphNodeStatus(stages: readonly Stage[]): Map< result.set(nodeId, { displayStatus: display.status, latestStageId: latestStage.id }); } return result; -} \ No newline at end of file +} diff --git a/apps/fabro-web/app/routes/run-overview.tsx b/apps/fabro-web/app/routes/run-overview.tsx index 179825560..48066eae0 100644 --- a/apps/fabro-web/app/routes/run-overview.tsx +++ b/apps/fabro-web/app/routes/run-overview.tsx @@ -54,9 +54,6 @@ export default function RunOverview() { const inner = innerRef.current; if (!inner || !graphSvg) return; - let cancelled = false; - (async () => { - if (cancelled) return; inner.innerHTML = graphSvg; const svg = inner.querySelector("svg"); if (!svg) return; @@ -151,8 +148,6 @@ export default function RunOverview() { } } } - })(); - return () => { cancelled = true; }; }, [stages, graphSvg, id, navigate, terminalOutcome]); const onPointerDown = useCallback((e: React.PointerEvent) => { @@ -237,4 +232,4 @@ export default function RunOverview() { ); -} \ No newline at end of file +} diff --git a/apps/fabro-web/app/routes/run-stages.test.ts b/apps/fabro-web/app/routes/run-stages.test.ts index 9a7d9bc43..704c5cac8 100644 --- a/apps/fabro-web/app/routes/run-stages.test.ts +++ b/apps/fabro-web/app/routes/run-stages.test.ts @@ -108,4 +108,4 @@ describe("turnsFromEvents", () => { expect(turn.running).toBe(false); } }); -}); \ No newline at end of file +}); diff --git a/apps/fabro-web/app/routes/run-stages.tsx b/apps/fabro-web/app/routes/run-stages.tsx index 90ba4ed0c..1176cb71a 100644 --- a/apps/fabro-web/app/routes/run-stages.tsx +++ b/apps/fabro-web/app/routes/run-stages.tsx @@ -41,7 +41,7 @@ import { EmptyState } from "../components/state"; import { CopyButton } from "../components/ui"; import { formatDurationSecs } from "../lib/format"; import { fetchRunCommandLog, useRunEventsList, useRunStageTurns, useRunStages } from "../lib/queries"; -import { formatStageLabel, mapRunStagesToSidebarStages } from "../lib/stage-sidebar"; +import { ACTIVE_STAGE_STATES, formatStageLabel, mapRunStagesToSidebarStages } from "../lib/stage-sidebar"; import { getNumber, getString, type UnknownRecord } from "../lib/unknown"; import { CommandOutputStream, @@ -614,7 +614,7 @@ export default function RunStages() { () => mapTurns(turnsQuery.data, eventsQuery.data, selectedStage?.id), [eventsQuery.data, selectedStage?.id, turnsQuery.data], ); - const isRunning = selectedStage?.status === "running"; + const isActive = selectedStage ? ACTIVE_STAGE_STATES.has(selectedStage.status) : false; if (!id || !stages.length) { return ( @@ -636,13 +636,13 @@ export default function RunStages() {
- +

{formatStageLabel(selectedStage)}

@@ -663,4 +663,4 @@ export default function RunStages() {
); -} \ No newline at end of file +} diff --git a/docs/public/api-reference/fabro-api.yaml b/docs/public/api-reference/fabro-api.yaml index 9cad7b736..bea733407 100644 --- a/docs/public/api-reference/fabro-api.yaml +++ b/docs/public/api-reference/fabro-api.yaml @@ -8330,4 +8330,4 @@ components: login: type: string description: User's login identifier (e.g. GitHub username). - example: octocat \ No newline at end of file + example: octocat diff --git a/lib/crates/fabro-server/src/server/handler/billing.rs b/lib/crates/fabro-server/src/server/handler/billing.rs index 6bf442c04..36e815e75 100644 --- a/lib/crates/fabro-server/src/server/handler/billing.rs +++ b/lib/crates/fabro-server/src/server/handler/billing.rs @@ -2,7 +2,7 @@ use std::num::NonZeroU32; use std::sync::Arc; use fabro_store::RunProjectionReducer; -use fabro_types::{EventBody, RunProjection, StageId}; +use fabro_types::{EventBody, RunProjection, StageId, StageProjection}; use super::super::{ ApiError, AppState, BilledTokenCounts, BillingByModel, BillingStageRef, EventEnvelope, HashMap, @@ -72,8 +72,7 @@ async fn list_run_stages( let stage_durations = fabro_workflow::extract_stage_durations_by_stage_id(&events); let lifecycle_states = latest_stage_states(&events); - let mut entries: Vec<(&StageId, &fabro_types::StageProjection)> = - projection.iter_stages().collect(); + let mut entries: Vec<(&StageId, &StageProjection)> = projection.iter_stages().collect(); entries.sort_by_key(|(_, stage)| stage.first_event_seq); let mut stages = Vec::with_capacity(entries.len()); @@ -217,4 +216,4 @@ async fn get_run_billing( }; (StatusCode::OK, Json(response)).into_response() -} \ No newline at end of file +} diff --git a/lib/crates/fabro-workflow/src/lib.rs b/lib/crates/fabro-workflow/src/lib.rs index 40a487330..61289ec85 100644 --- a/lib/crates/fabro-workflow/src/lib.rs +++ b/lib/crates/fabro-workflow/src/lib.rs @@ -20,7 +20,7 @@ use std::sync::Arc; use fabro_retro::retro::CompletedStage; use fabro_store::EventEnvelope; -use fabro_types::EventBody; +use fabro_types::{EventBody, StageId}; /// Callback invoked when a workflow node starts executing. pub type OnNodeCallback = Option>; @@ -114,11 +114,9 @@ pub fn extract_stage_durations_from_events(events: &[EventEnvelope]) -> HashMap< /// Extract per-stage (node_id, visit) durations from `stage.completed` / /// `stage.failed` events. Differs from /// [`extract_stage_durations_from_events`] by keying on the full -/// [`fabro_types::StageId`] instead of just `node_id`, so multi-visit +/// [`StageId`] instead of just `node_id`, so multi-visit /// stages (e.g. a looped `verify` node) keep distinct durations. -pub fn extract_stage_durations_by_stage_id( - events: &[EventEnvelope], -) -> HashMap { +pub fn extract_stage_durations_by_stage_id(events: &[EventEnvelope]) -> HashMap { let mut durations = HashMap::new(); for envelope in events { let Some(duration_ms) = stage_completion_duration_ms(&envelope.event.body) else { @@ -179,4 +177,4 @@ mod stage_scope; pub mod test_support; #[doc(hidden)] pub mod transforms; -pub mod workflow_bundle; \ No newline at end of file +pub mod workflow_bundle;