mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-11 22:53:00 +00:00
feat(web): show LLM model name in stage view toolbar
Extracts the model from `stage.prompt`, `agent.session.activated`, and `agent.cli.started` events that the stage already loads, and renders it on the right side of the events toolbar with a CpuChipIcon. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9e54643a7e
commit
089f6befe8
2 changed files with 99 additions and 1 deletions
|
|
@ -1,7 +1,7 @@
|
|||
import { describe, expect, test } from "bun:test";
|
||||
import type { EventEnvelope } from "@qltysh/fabro-api-client";
|
||||
|
||||
import { eventsToActivity } from "./run-stages";
|
||||
import { eventsToActivity, extractStageModel } from "./run-stages";
|
||||
|
||||
function envelope(seq: number, partial: Partial<EventEnvelope>): EventEnvelope {
|
||||
return {
|
||||
|
|
@ -163,6 +163,63 @@ describe("eventsToActivity", () => {
|
|||
}
|
||||
});
|
||||
|
||||
test("extractStageModel pulls model from agent.session.activated, ignoring other stages", () => {
|
||||
const events: EventEnvelope[] = [
|
||||
envelope(1, {
|
||||
event: "agent.session.activated",
|
||||
stage_id: "simplify@1",
|
||||
node_id: "simplify",
|
||||
properties: { provider: "anthropic", model: "claude-sonnet-4-5" },
|
||||
}),
|
||||
envelope(2, {
|
||||
event: "agent.session.activated",
|
||||
stage_id: "verify@1",
|
||||
node_id: "verify",
|
||||
properties: { provider: "openai", model: "gpt-5" },
|
||||
}),
|
||||
];
|
||||
|
||||
expect(extractStageModel(events, "simplify@1")).toBe("claude-sonnet-4-5");
|
||||
expect(extractStageModel(events, "verify@1")).toBe("gpt-5");
|
||||
expect(extractStageModel(events, "fmt@1")).toBe(null);
|
||||
});
|
||||
|
||||
test("extractStageModel uses latest stage event with a model", () => {
|
||||
const events: EventEnvelope[] = [
|
||||
envelope(1, {
|
||||
event: "stage.prompt",
|
||||
stage_id: "agent@1",
|
||||
node_id: "agent",
|
||||
properties: { model: "claude-opus-4-5" },
|
||||
}),
|
||||
envelope(2, {
|
||||
event: "agent.cli.started",
|
||||
stage_id: "agent@1",
|
||||
node_id: "agent",
|
||||
properties: {
|
||||
provider: "anthropic",
|
||||
model: "claude-sonnet-4-6",
|
||||
command: "claude",
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
expect(extractStageModel(events, "agent@1")).toBe("claude-sonnet-4-6");
|
||||
});
|
||||
|
||||
test("extractStageModel ignores model from unrelated event types", () => {
|
||||
const events: EventEnvelope[] = [
|
||||
envelope(1, {
|
||||
event: "agent.message",
|
||||
stage_id: "agent@1",
|
||||
node_id: "agent",
|
||||
properties: { text: "hi", model: "should-be-ignored" },
|
||||
}),
|
||||
];
|
||||
|
||||
expect(extractStageModel(events, "agent@1")).toBe(null);
|
||||
});
|
||||
|
||||
test("ignores unknown event types and events for other stages", () => {
|
||||
const events: EventEnvelope[] = [
|
||||
envelope(1, {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { XMarkIcon } from "@heroicons/react/24/outline";
|
|||
import {
|
||||
CheckIcon,
|
||||
ChevronUpDownIcon,
|
||||
CpuChipIcon,
|
||||
FunnelIcon,
|
||||
MagnifyingGlassIcon,
|
||||
} from "@heroicons/react/16/solid";
|
||||
|
|
@ -195,6 +196,26 @@ export function eventsToActivity(events: EventEnvelope[], stageId: string): Turn
|
|||
return turns;
|
||||
}
|
||||
|
||||
const STAGE_MODEL_EVENT_NAMES = new Set([
|
||||
"stage.prompt",
|
||||
"agent.session.activated",
|
||||
"agent.cli.started",
|
||||
]);
|
||||
|
||||
export function extractStageModel(
|
||||
events: EventEnvelope[],
|
||||
stageId: string,
|
||||
): string | null {
|
||||
let model: string | null = null;
|
||||
for (const e of events) {
|
||||
if (activityEventStageId(e) !== stageId) continue;
|
||||
if (!e.event || !STAGE_MODEL_EVENT_NAMES.has(e.event)) continue;
|
||||
const candidate = getString(e.properties ?? {}, "model");
|
||||
if (candidate) model = candidate;
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
function turnLabel(turn: TurnType): string {
|
||||
switch (turn.kind) {
|
||||
case "system":
|
||||
|
|
@ -816,6 +837,7 @@ function EventsToolbar({
|
|||
onSearchChange,
|
||||
filteredCount,
|
||||
totalCount,
|
||||
model,
|
||||
}: {
|
||||
tab: EventsTab;
|
||||
onTabChange: (tab: EventsTab) => void;
|
||||
|
|
@ -828,6 +850,7 @@ function EventsToolbar({
|
|||
onSearchChange: (value: string) => void;
|
||||
filteredCount: number;
|
||||
totalCount: number;
|
||||
model: string | null;
|
||||
}) {
|
||||
const transcriptAllSelected = selectedKinds.length === EVENT_KINDS.length;
|
||||
const debugAllSelected =
|
||||
|
|
@ -879,6 +902,15 @@ function EventsToolbar({
|
|||
{filteredCount.toLocaleString()} of {totalCount.toLocaleString()} events
|
||||
</span>
|
||||
)}
|
||||
{model && (
|
||||
<span
|
||||
className="inline-flex items-center gap-1.5 text-xs text-fg-muted"
|
||||
title="LLM model used for this stage"
|
||||
>
|
||||
<CpuChipIcon className="size-3.5" aria-hidden="true" />
|
||||
<span className="font-mono">{model}</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -949,6 +981,14 @@ export default function RunStages() {
|
|||
}
|
||||
return Array.from(set).sort();
|
||||
}, [debugEvents]);
|
||||
const stageModel = useMemo(
|
||||
() =>
|
||||
selectedStageId
|
||||
? extractStageModel(stageEventsQuery.data ?? [], selectedStageId)
|
||||
: null,
|
||||
[stageEventsQuery.data, selectedStageId],
|
||||
);
|
||||
|
||||
const filteredDebugEvents = useMemo<EventEnvelope[]>(() => {
|
||||
const useCategoryFilter = selectedDebugCategories.length > 0;
|
||||
const cats = new Set(selectedDebugCategories);
|
||||
|
|
@ -1008,6 +1048,7 @@ export default function RunStages() {
|
|||
onSearchChange={setSearch}
|
||||
filteredCount={tab === "transcript" ? filteredTurns.length : filteredDebugEvents.length}
|
||||
totalCount={tab === "transcript" ? turns.length : debugEvents.length}
|
||||
model={stageModel}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue