fix(FN-6950): show effective model icons in task chat

This commit is contained in:
gsxdsm
2026-06-23 00:38:14 -07:00
parent a47222b3e5
commit 5e55d9c46c
4 changed files with 55 additions and 4 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Show provider icons in task detail chat for default-backed executor, reviewer, planner, and merger models.

View File

@@ -25,6 +25,7 @@ interface TaskChatTabProps {
onTaskUpdated?: (task: Task) => void;
expanded?: boolean;
onToggleExpanded?: () => void;
effectiveModels?: Partial<Record<"triage" | "executor" | "reviewer" | "merger", TaskChatModelInfo | null>>;
}
type AgentLogRole = AgentRole | undefined;
@@ -122,12 +123,28 @@ function getRuntimeModelForRole(entries: readonly AgentLogEntry[], role: AgentLo
return null;
}
function getModelForRole(task: Task | TaskDetail, role: AgentLogRole, entries: readonly AgentLogEntry[]): TaskChatModelInfo | null {
function getEffectiveModelForRole(
effectiveModels: TaskChatTabProps["effectiveModels"] | undefined,
role: AgentLogRole,
): TaskChatModelInfo | null {
if (!role) return null;
return effectiveModels?.[role] ?? null;
}
function getModelForRole(
task: Task | TaskDetail,
role: AgentLogRole,
entries: readonly AgentLogEntry[],
effectiveModels?: TaskChatTabProps["effectiveModels"],
): TaskChatModelInfo | null {
/*
FNXC:TaskDetailChat 2026-06-23-21:18:
Task-detail chat agent headers should identify the AI provider actually backing each role, not a generic/role avatar. Prefer explicit task model overrides because they are stable before logs stream, then fall back to the runtime "using model" log marker emitted by active planner/executor/reviewer sessions. Merger output uses the validator/reviewer lane provider because merge-fix/review flows share that model family in the UI.
FNXC:TaskDetailChat 2026-06-23-00:54:
Default executor models such as OpenAI Codex GPT-5.5 can resolve through settings rather than task overrides or log markers. Task chat receives the same effective model resolution used by the task-detail model header so role icons match Chat and Agent Log instead of falling back to CPU for default-backed agents.
*/
return getExplicitModelForRole(task, role) ?? getRuntimeModelForRole(entries, role);
return getExplicitModelForRole(task, role) ?? getRuntimeModelForRole(entries, role) ?? getEffectiveModelForRole(effectiveModels, role);
}
function TaskChatAgentIcon({ label, modelInfo, role }: { label: string; modelInfo: TaskChatModelInfo | null; role: AgentLogRole }) {
@@ -532,7 +549,7 @@ function TaskChatUserMessage({ message }: { message: UserChatMessage }) {
);
}
export function TaskChatTab({ task, projectId, active, addToast, sessionLive, onTaskUpdated, expanded = false, onToggleExpanded }: TaskChatTabProps) {
export function TaskChatTab({ task, projectId, active, addToast, sessionLive, onTaskUpdated, expanded = false, onToggleExpanded, effectiveModels }: TaskChatTabProps) {
const { t } = useTranslation("app");
const { entries, loading, loadMore, hasMore, loadingMore } = useAgentLogs(task.id, active, projectId);
const [draft, setDraft] = useState("");
@@ -868,7 +885,7 @@ export function TaskChatTab({ task, projectId, active, addToast, sessionLive, on
const segments = segmentGroupEntries(item.entries);
const latestEntryTimestamp = item.entries[item.entries.length - 1]?.timestamp ?? "";
const relativeTime = formatRelativeTimeAgo(latestEntryTimestamp);
const modelInfo = getModelForRole(task, item.role, item.entries);
const modelInfo = getModelForRole(task, item.role, item.entries, effectiveModels);
return (
<section className="task-chat-group" key={`${item.role ?? "agent"}-${itemIndex}`} aria-label={t("taskChat.agentMessages", "{{label}} messages", { label: item.label })}>
<header className="task-chat-group-header">

View File

@@ -263,6 +263,11 @@ function resolveEffectivePlanning(
return resolveTaskPlanningModel(task, settings);
}
function toTaskChatModelInfo(model: ModelSelection): { provider: string; modelId?: string } | null {
if (!model.provider) return null;
return model.modelId ? { provider: model.provider, modelId: model.modelId } : { provider: model.provider };
}
function getStepStatusColor(status: string): string {
switch (status) {
case "done":
@@ -3304,6 +3309,12 @@ export function TaskDetailContent({
onTaskUpdated={handleChatTaskUpdated}
expanded={chatExpanded}
onToggleExpanded={() => setChatExpanded((value) => !value)}
effectiveModels={{
triage: toTaskChatModelInfo(resolveEffectivePlanning(workingTask, agentLogEntries, settings)),
executor: toTaskChatModelInfo(resolveEffectiveExecutor(workingTask, agentLogEntries, assignedAgent, settings)),
reviewer: toTaskChatModelInfo(resolveEffectiveValidator(workingTask, agentLogEntries, assignedAgent, settings)),
merger: toTaskChatModelInfo(resolveEffectiveValidator(workingTask, agentLogEntries, assignedAgent, settings)),
}}
/>
</div>
) : activeTab === "logs" ? (

View File

@@ -468,6 +468,24 @@ describe("TaskChatTab", () => {
expect(document.querySelector(".task-chat-provider-icon [data-provider='anthropic']")).toBeTruthy();
});
it("renders provider icons for task chat roles from effective default models", () => {
mockLogs([
makeEntry({ agent: "executor", text: "executor output without model marker" }),
]);
render(
<TaskChatTab
task={makeTask()}
active
addToast={vi.fn()}
effectiveModels={{ executor: { provider: "openai-codex", modelId: "gpt-5.5" } }}
/>,
);
expect(document.querySelector(".task-chat-provider-icon [data-provider='openai-codex']")).toBeTruthy();
expect(screen.queryByLabelText("Executor: model provider unknown")).not.toBeInTheDocument();
});
it("groups consecutive entries by agent role", () => {
mockLogs([
makeEntry({ agent: "executor", text: "first" }),