diff --git a/apps/fabro-web/app/components/stage-inference-indicator.test.tsx b/apps/fabro-web/app/components/stage-inference-indicator.test.tsx deleted file mode 100644 index 45da56ae4..000000000 --- a/apps/fabro-web/app/components/stage-inference-indicator.test.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import { afterEach, beforeEach, describe, expect, test } from "bun:test"; -import TestRenderer, { act } from "react-test-renderer"; - -import { LlmOutputKind } from "@qltysh/fabro-api-client"; -import type { StageInferenceProjection } from "@qltysh/fabro-api-client"; - -import { StageInferenceIndicator } from "./stage-inference-indicator"; - -const OPENED_AT = new Date(Date.now() - 12_000).toISOString(); - -function makeInference( - overrides: Partial = {}, -): StageInferenceProjection { - return { - session_id: "ses_root", - started_at: OPENED_AT, - requested_model: { - provider: "anthropic", - model_id: "claude-fable-5", - }, - retries: 0, - ...overrides, - }; -} - -function render( - inference: StageInferenceProjection | null | undefined, - settled = false, -): string { - let renderer!: TestRenderer.ReactTestRenderer; - act(() => { - renderer = TestRenderer.create( - , - ); - }); - const output = JSON.stringify(renderer.toJSON()); - act(() => renderer.unmount()); - return output; -} - -describe("StageInferenceIndicator", () => { - const actGlobal = globalThis as { - IS_REACT_ACT_ENVIRONMENT?: boolean; - }; - const previousActEnvironment = actGlobal.IS_REACT_ACT_ENVIRONMENT; - - beforeEach(() => { - actGlobal.IS_REACT_ACT_ENVIRONMENT = true; - }); - - afterEach(() => { - if (previousActEnvironment === undefined) { - delete actGlobal.IS_REACT_ACT_ENVIRONMENT; - } else { - actGlobal.IS_REACT_ACT_ENVIRONMENT = previousActEnvironment; - } - }); - - test("renders nothing without an open bracket", () => { - expect(render(undefined)).toBe("null"); - expect(render(null)).toBe("null"); - }); - - test("names the requested model while nothing has come back", () => { - const output = render(makeInference()); - expect(output).toContain("Model request"); - expect(output).toContain("waiting on claude-fable-5"); - expect(output).toContain('"aria-live":"polite"'); - expect(output).toContain('"aria-hidden":"true"'); - // No completion estimate exists, so none may be shown. - expect(output).not.toContain("%"); - }); - - test("reports the observed first-output kind", () => { - expect( - render(makeInference({ first_output_kind: LlmOutputKind.REASONING })), - ).toContain("reasoning"); - expect( - render(makeInference({ first_output_kind: LlmOutputKind.TEXT })), - ).toContain("writing"); - expect( - render(makeInference({ first_output_kind: LlmOutputKind.TOOL_CALL })), - ).toContain("calling tools"); - }); - - test("never says thinking for non-reasoning output", () => { - for (const kind of [LlmOutputKind.TEXT, LlmOutputKind.TOOL_CALL]) { - expect(render(makeInference({ first_output_kind: kind }))).not.toContain( - "thinking", - ); - } - }); - - test("counts retries without presenting them as failure", () => { - const output = render(makeInference({ retries: 2 })); - expect(output).toContain("retry 2"); - expect(output).not.toContain("failed"); - }); - - test("goes static once the run can no longer advance the bracket", () => { - const output = render(makeInference(), true); - // An open bracket on a settled run means we never learned how the request - // ended — animating it would claim work that may not be happening. - expect(output).toContain("never completed"); - expect(output).not.toContain("animate-pulse"); - }); -}); diff --git a/apps/fabro-web/app/components/stage-inference-indicator.tsx b/apps/fabro-web/app/components/stage-inference-indicator.tsx deleted file mode 100644 index a329f3fa0..000000000 --- a/apps/fabro-web/app/components/stage-inference-indicator.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { LlmOutputKind } from "@qltysh/fabro-api-client"; -import type { StageInferenceProjection } from "@qltysh/fabro-api-client"; - -import { Tooltip } from "./ui"; -import { formatAbsoluteTs, formatDurationSecs } from "../lib/format"; -import { elapsedSecsSince, useTickingNow } from "../lib/time"; - -export interface StageInferenceIndicatorProps { - /** Open inference bracket from the stage projection, if there is one. */ - inference: StageInferenceProjection | null | undefined; - /** - * The run can no longer make progress on this bracket: it reached a terminal - * status, or the stall watchdog fired. An open bracket then means *we never - * learned how the request ended*, not *it is still working*, so the readout - * goes static. - */ - settled: boolean; -} - -const ACTIVITY_LABEL: Record = { - [LlmOutputKind.REASONING]: "reasoning", - [LlmOutputKind.TEXT]: "writing", - [LlmOutputKind.TOOL_CALL]: "calling tools", -}; - -/** - * Live readout for an open model request. - * - * Says only what the event log proves. There is no progress bar, percentage, - * or ETA, because no completion estimate exists; the elapsed clock counts - * since the request opened rather than claiming the model is still working; - * and "reasoning" appears only when the provider actually sent reasoning - * output, never as a guess filling a gap in the log. - */ -export function StageInferenceIndicator({ - inference, - settled, -}: StageInferenceIndicatorProps) { - // Ticking is what distinguishes "we are still hearing from this request" - // from "this is a record of one that never closed", so it stops the moment - // the bracket can no longer advance. - const now = useTickingNow(Boolean(inference) && !settled); - - if (!inference) return null; - - if (settled) { - return ( -

- Model request opened {formatAbsoluteTs(inference.started_at)}, never - completed -

- ); - } - - const elapsedSecs = elapsedSecsSince(inference.started_at, now); - const activity = inference.first_output_kind - ? ACTIVITY_LABEL[inference.first_output_kind] - : `waiting on ${inference.requested_model.model_id}`; - - const statusParts = ["Model request", activity]; - // A retry that later succeeds is normal, so this is a count, not a failure. - if (inference.retries > 0) { - statusParts.push(`retry ${inference.retries}`); - } - - return ( -

- - - - -

- ); -} diff --git a/apps/fabro-web/app/lib/run-events.test.tsx b/apps/fabro-web/app/lib/run-events.test.tsx index 11544b14d..e984be74d 100644 --- a/apps/fabro-web/app/lib/run-events.test.tsx +++ b/apps/fabro-web/app/lib/run-events.test.tsx @@ -150,7 +150,7 @@ describe("queryKeysForRunEvent", () => { ]); }); - test("watchdog timeout refreshes the stage events that settle inference", () => { + test("watchdog timeout refreshes the stage events for that stage", () => { expect( queryKeysForRunEvent("run-1", "watchdog.timeout", "code@1"), ).toEqual([queryKeys.runs.stageEvents("run-1", "code@1")]); diff --git a/apps/fabro-web/app/routes/run-stages.test.ts b/apps/fabro-web/app/routes/run-stages.test.ts index 2238aaf74..51684a895 100644 --- a/apps/fabro-web/app/routes/run-stages.test.ts +++ b/apps/fabro-web/app/routes/run-stages.test.ts @@ -884,19 +884,6 @@ describe("buildChatItems", () => { }); describe("buildStageActivity pending tools", () => { - test("records watchdog settlement only for the selected stage", () => { - const events: EventEnvelope[] = [ - envelope(1, { - event: "watchdog.timeout", - stage_id: "plan@1", - node_id: "plan", - }), - ]; - - expect(buildStageActivity(events, "plan@1").watchdogTimedOut).toBe(true); - expect(buildStageActivity(events, "code@1").watchdogTimedOut).toBe(false); - }); - test("returns started-but-not-completed calls for the stage", () => { const events: EventEnvelope[] = [ envelope(1, { diff --git a/apps/fabro-web/app/routes/run-stages.tsx b/apps/fabro-web/app/routes/run-stages.tsx index 48111ce23..ba2432151 100644 --- a/apps/fabro-web/app/routes/run-stages.tsx +++ b/apps/fabro-web/app/routes/run-stages.tsx @@ -31,7 +31,6 @@ import type { ThreadDnaSelection, } from "../components/event-debug"; import { StageContext } from "../components/stage-context"; -import { StageInferenceIndicator } from "../components/stage-inference-indicator"; import { StageInsightsSidebar } from "../components/stage-insights-sidebar"; import { StageSidebar } from "../components/stage-sidebar"; import type { Stage } from "../components/stage-sidebar"; @@ -73,7 +72,6 @@ import { useRunStages, useRunState, } from "../lib/queries"; -import { isTerminalRunStatus } from "../lib/run-actions"; import { STAGE_ACTIVITY_EVENT_TYPES, type StageActivityEventType, @@ -86,7 +84,6 @@ import { getNumber, getString, type UnknownRecord } from "../lib/unknown"; import type { EventEnvelope, StageHandler, - StageInferenceProjection, StageModelUsage, } from "@qltysh/fabro-api-client"; @@ -271,7 +268,6 @@ export interface PendingToolCall { interface StageActivity { turns: TurnType[]; pendingTools: PendingToolCall[]; - watchdogTimedOut: boolean; } interface PendingCommand { @@ -287,17 +283,12 @@ export function buildStageActivity( const pendingTools = new Map(); let pendingCommand: PendingCommand | undefined; let sawAssistantMessage = false; - let watchdogTimedOut = false; for (const e of events) { const eventName = e.event; if (activityEventStageId(e) !== stageId) { continue; } - if (eventName === "watchdog.timeout") { - watchdogTimedOut = true; - continue; - } if ( !eventName || !STAGE_ACTIVITY_EVENT_SET.has(eventName) @@ -452,7 +443,6 @@ export function buildStageActivity( return { turns, - watchdogTimedOut, pendingTools: Array.from(pendingTools, ([toolCallId, tool]) => ({ toolCallId, toolName: tool.toolName, @@ -2075,8 +2065,6 @@ function RunStageActivityStage({ selectedStage, stages, runStart, - inference, - runSettled, tab, selectedKinds, selectedDebugCategories, @@ -2090,8 +2078,6 @@ function RunStageActivityStage({ selectedStage: Stage; stages: Stage[]; runStart: string | undefined; - inference: StageInferenceProjection | null | undefined; - runSettled: boolean; tab: EventsTab; selectedKinds: EventKind[]; selectedDebugCategories: DebugCategory[]; @@ -2103,15 +2089,11 @@ function RunStageActivityStage({ }) { const selectedStageId = selectedStage.id; const stageEventsQuery = useRunStageEvents(runId, selectedStageId); - // An open bracket on a run that can no longer advance means we never learned - // how the request ended, not that it is still working. The watchdog stays - // the authority on "actually stuck", so its timeout settles the readout too. const activity = useMemo( () => buildStageActivity(stageEventsQuery.data ?? [], selectedStageId), [stageEventsQuery.data, selectedStageId], ); const { turns } = activity; - const inferenceSettled = runSettled || activity.watchdogTimedOut; const renderer: StageRenderer = selectStageRenderer(selectedStage.handler); const debugEvents = useMemo(() => { return (stageEventsQuery.data ?? []).filter( @@ -2259,11 +2241,6 @@ function RunStageActivityStage({

)} - - );