mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-09 03:20:56 +00:00
Remove the model request status line above the stage toolbar
The "Model request · waiting on <model>" 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 <noreply@anthropic.com>
This commit is contained in:
parent
db0b6c9b7c
commit
67ed7af026
5 changed files with 1 additions and 241 deletions
|
|
@ -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> = {},
|
||||
): 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(
|
||||
<StageInferenceIndicator inference={inference} settled={settled} />,
|
||||
);
|
||||
});
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
|
@ -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, string> = {
|
||||
[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 (
|
||||
<p className="pb-2 text-xs text-fg-muted">
|
||||
Model request opened {formatAbsoluteTs(inference.started_at)}, never
|
||||
completed
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<p className="pb-2 text-xs text-fg-muted">
|
||||
<Tooltip
|
||||
label={`Model request opened ${formatAbsoluteTs(inference.started_at)}`}
|
||||
>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<span
|
||||
className="size-1.5 animate-pulse rounded-full bg-teal-500"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span aria-live="polite">{statusParts.join(" · ")}</span>
|
||||
{elapsedSecs !== null && (
|
||||
<span aria-hidden="true">
|
||||
{" · "}
|
||||
{formatDurationSecs(elapsedSecs)}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
|
@ -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")]);
|
||||
|
|
|
|||
|
|
@ -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, {
|
||||
|
|
|
|||
|
|
@ -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<string, PendingTool>();
|
||||
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<EventEnvelope[]>(() => {
|
||||
return (stageEventsQuery.data ?? []).filter(
|
||||
|
|
@ -2259,11 +2241,6 @@ function RunStageActivityStage({
|
|||
</Link>
|
||||
</p>
|
||||
)}
|
||||
<StageInferenceIndicator
|
||||
inference={inference}
|
||||
settled={inferenceSettled}
|
||||
/>
|
||||
|
||||
<EventsToolbar
|
||||
tab={effectiveTab}
|
||||
renderer={renderer}
|
||||
|
|
@ -2361,15 +2338,11 @@ function RunStageActivity({
|
|||
selectedStage,
|
||||
stages,
|
||||
runStart,
|
||||
inference,
|
||||
runSettled,
|
||||
}: {
|
||||
runId: string;
|
||||
selectedStage: Stage;
|
||||
stages: Stage[];
|
||||
runStart: string | undefined;
|
||||
inference: StageInferenceProjection | null | undefined;
|
||||
runSettled: boolean;
|
||||
}) {
|
||||
const [activityState, dispatchActivity] = useReducer(
|
||||
stageActivityReducer,
|
||||
|
|
@ -2385,8 +2358,6 @@ function RunStageActivity({
|
|||
selectedStage={selectedStage}
|
||||
stages={stages}
|
||||
runStart={runStart}
|
||||
inference={inference}
|
||||
runSettled={runSettled}
|
||||
tab={tab}
|
||||
selectedKinds={selectedKinds}
|
||||
selectedDebugCategories={selectedDebugCategories}
|
||||
|
|
@ -2438,8 +2409,6 @@ export default function RunStages() {
|
|||
isAgentStage && selectedStageId
|
||||
? runStateQuery.data?.stages[selectedStageId]
|
||||
: undefined;
|
||||
const runStatusKind = runQuery.data?.lifecycle.status.kind;
|
||||
const runSettled = isTerminalRunStatus(runStatusKind);
|
||||
|
||||
if (!id || !selectedStage) {
|
||||
return (
|
||||
|
|
@ -2491,8 +2460,6 @@ export default function RunStages() {
|
|||
selectedStage={selectedStage}
|
||||
stages={stages}
|
||||
runStart={runStart}
|
||||
inference={stageProjection?.inference}
|
||||
runSettled={runSettled}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue