From 67ed7af02685f11b478cc30f51d1dc2ac173dff4 Mon Sep 17 00:00:00 2001
From: Bryan Helmkamp
Date: Sat, 25 Jul 2026 08:38:46 -0400
Subject: [PATCH] Remove the model request status line above the stage toolbar
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The "Model request · waiting on " readout sat directly above the
Chat/Thread/Debug toolbar and appeared and disappeared as requests opened
and closed, shifting the toolbar underneath it.
Drops the StageInferenceIndicator component and everything that existed
only to feed it: the inference/runSettled prop threading through
RunStages, and StageActivity's watchdogTimedOut field. The watchdog.timeout
event now falls through to the same ignore path it always would have, since
it was never in STAGE_ACTIVITY_EVENT_TYPES.
The run-events invalidations for watchdog.timeout and agent.llm.* stay:
they still refresh stage events for the Debug tab and run state for the
insights sidebar.
Co-Authored-By: Claude Fable 5
---
.../stage-inference-indicator.test.tsx | 107 ------------------
.../components/stage-inference-indicator.tsx | 87 --------------
apps/fabro-web/app/lib/run-events.test.tsx | 2 +-
apps/fabro-web/app/routes/run-stages.test.ts | 13 ---
apps/fabro-web/app/routes/run-stages.tsx | 33 ------
5 files changed, 1 insertion(+), 241 deletions(-)
delete mode 100644 apps/fabro-web/app/components/stage-inference-indicator.test.tsx
delete mode 100644 apps/fabro-web/app/components/stage-inference-indicator.tsx
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 (
-
-
-
-
- {statusParts.join(" · ")}
- {elapsedSecs !== null && (
-
- {" · "}
- {formatDurationSecs(elapsedSecs)}
-
- )}
-
-
-
- );
-}
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({
)}
-
-
);