diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index b4128781f8..7e295ddc08 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -686,6 +686,7 @@ Navigation: Features: - Switch between **List**, **Board**, and **Org chart** layouts - Filter by role/state, include/exclude system agents, and inspect health/status +- Agent list cards show the configured **Model** or plugin **Runtime** for each agent, falling back to **Auto** when no override is set - First-run setup asks whether to create an optional project agent after project registration. The default template is **CEO**; users can choose another preset, use the AI interview when `experimentalFeatures.agentOnboarding` is enabled, or skip it. Fusion can still build tasks without an agent by starting temporary agents to plan, code, review, and merge task work. - Start, pause, stop, and trigger agent runs from the view and from detail panels - In **Agent detail**, use the kebab **Bulk agent actions** button in the header utility cluster (next to **Refresh** and **Close**) to run project-wide lifecycle transitions for non-ephemeral agents in the current project — **Pause All Agents** targets agents in the `active` or `running` state, while **Resume All Agents** targets agents in the `paused` state only diff --git a/packages/dashboard/app/components/AgentsView.css b/packages/dashboard/app/components/AgentsView.css index eb879bfa11..597969d478 100644 --- a/packages/dashboard/app/components/AgentsView.css +++ b/packages/dashboard/app/components/AgentsView.css @@ -787,11 +787,24 @@ FN-6774 removes the saturated left-edge status stripe from split-sidebar agent c } .agent-task, -.agent-heartbeat { +.agent-heartbeat, +.agent-model-runtime { display: flex; gap: var(--space-sm); } +.agent-model-runtime { + align-items: center; + min-width: 0; +} + +.agent-model-runtime__value { + min-width: 0; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; +} + .agent-heartbeat-control { display: flex; align-items: center; @@ -1394,6 +1407,10 @@ AgentsView uses the measured SVG overlay as the single connector system so paren font-size: calc(var(--space-sm) + var(--space-xs) * 0.625); } + .agent-model-runtime { + flex-wrap: wrap; + } + .agent-card-error { width: 100%; padding: var(--space-xs) var(--space-sm); diff --git a/packages/dashboard/app/components/AgentsView.tsx b/packages/dashboard/app/components/AgentsView.tsx index 8dc4590123..b19d6dcb86 100644 --- a/packages/dashboard/app/components/AgentsView.tsx +++ b/packages/dashboard/app/components/AgentsView.tsx @@ -121,6 +121,37 @@ function getStateCardClass( } } +interface AgentModelLabel { + label: string | null; + isRuntime: boolean; +} + +/* +FNXC:AgentsView 2026-06-23-04:00: +Agent list cards must expose the configured model or plugin runtime without requiring a detail-view open. +Use the same runtimeHint/modelProvider+modelId/legacy model fallback order as the detail view and leave no-override agents as Auto at render time. +*/ +function getAgentModelLabel(agent: Agent): AgentModelLabel { + const runtimeConfig = agent.runtimeConfig ?? {}; + const runtimeHint = typeof runtimeConfig.runtimeHint === "string" ? runtimeConfig.runtimeHint : ""; + if (runtimeHint) { + return { label: runtimeHint, isRuntime: true }; + } + + const modelProvider = typeof runtimeConfig.modelProvider === "string" ? runtimeConfig.modelProvider : ""; + const modelId = typeof runtimeConfig.modelId === "string" ? runtimeConfig.modelId : ""; + if (modelProvider && modelId) { + return { label: `${modelProvider}/${modelId}`, isRuntime: false }; + } + + const legacyModel = typeof runtimeConfig.model === "string" ? runtimeConfig.model : ""; + if (legacyModel.includes("/")) { + const slashIdx = legacyModel.indexOf("/"); + return { label: legacyModel.slice(slashIdx + 1), isRuntime: false }; + } + + return { label: null, isRuntime: false }; +} function getOrgChartLeafCount(node: OrgTreeNode): number { if (node.children.length === 0) { @@ -1720,6 +1751,7 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin const configuredIntervalMs = resolveHeartbeatIntervalMs(agent.runtimeConfig?.heartbeatIntervalMs); const heartbeatOptions = getHeartbeatIntervalOptions(configuredIntervalMs); const isUpdatingHeartbeat = updatingHeartbeatAgentId === agent.id; + const modelLabel = getAgentModelLabel(agent); return (
+
+ {modelLabel.isRuntime ? t("agents.runtime", "Runtime") : t("agents.model", "Model")}: + + {modelLabel.label ?? t("agents.auto", "Auto")} + +
{agent.state === "error" && agent.lastError ? ( { expect(screen.getByText("review")).toHaveAttribute("title", "auto::skills/../../.agents/skills/review/SKILL.md"); }); + it("renders model and runtime labels on list-view agent cards", async () => { + const modelAgents: Agent[] = [ + { + ...mockAgents[0], + id: "agent-provider-model", + name: "Provider Model Agent", + runtimeConfig: { modelProvider: "openai", modelId: "gpt-4.1" }, + }, + { + ...mockAgents[0], + id: "agent-legacy-model", + name: "Legacy Model Agent", + runtimeConfig: { model: "anthropic/claude-sonnet" }, + }, + { + ...mockAgents[0], + id: "agent-runtime", + name: "Plugin Runtime Agent", + runtimeConfig: { runtimeHint: "hermes-local" }, + }, + { + ...mockAgents[0], + id: "agent-auto", + name: "Auto Model Agent", + runtimeConfig: undefined, + }, + ]; + mockFetchAgents.mockResolvedValueOnce(modelAgents); + mockFetchAgentStats.mockResolvedValueOnce({ total: 4, byState: {}, byRole: {} }); + + const { container } = render(); + + await waitFor(() => { + expect(screen.getByText("Provider Model Agent")).toBeInTheDocument(); + }); + + const getCardModelRow = (agentId: string) => { + const card = Array.from(container.querySelectorAll(".agent-card")).find((element) => element.textContent?.includes(agentId)); + expect(card).toBeTruthy(); + const row = card?.querySelector(".agent-model-runtime"); + expect(row).toBeTruthy(); + return row; + }; + + expect(getCardModelRow("agent-provider-model").textContent).toMatch(/Model:\s*openai\/gpt-4\.1/); + expect(getCardModelRow("agent-legacy-model").textContent).toMatch(/Model:\s*claude-sonnet/); + expect(getCardModelRow("agent-runtime").textContent).toMatch(/Runtime:\s*hermes-local/); + expect(getCardModelRow("agent-auto").textContent).toMatch(/Model:\s*Auto/); + }); + it("renders cross-pane overview above split layout", async () => { const { container } = render();