FN-6936: show agent model details on list cards

Agent list cards now surface model/runtime configuration alongside existing agent details.

- Add list-card model/runtime labeling with provider/model, legacy model, runtime hint, and Auto fallback handling.
- Style the new value badge for truncation and responsive wrapping.
- Cover configured, legacy, runtime, and automatic agent labels in AgentsView tests.
- Document the model/runtime field in the dashboard guide.

Files changed:
 docs/dashboard-guide.md                            |  1 +
 packages/dashboard/app/components/AgentsView.css   | 19 +++++++-
 packages/dashboard/app/components/AgentsView.tsx   | 38 ++++++++++++++++
 .../app/components/__tests__/AgentsView.test.tsx   | 50 ++++++++++++++++++++++
 4 files changed, 107 insertions(+), 1 deletion(-)

Fusion-Task-Id: FN-6936

Fusion-Task-Lineage: 7ce3591e-eae0-4968-8923-d9acab0f92c3
This commit is contained in:
gsxdsm
2026-06-23 00:15:38 -07:00
parent deaa64218b
commit 826a7eea31
4 changed files with 107 additions and 1 deletions

View File

@@ -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

View File

@@ -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);

View File

@@ -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 (
<div
key={agent.id}
@@ -1833,6 +1865,12 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
</div>
<div className="agent-card-body">
<div className="agent-model-runtime">
<span className="text-secondary">{modelLabel.isRuntime ? t("agents.runtime", "Runtime") : t("agents.model", "Model")}:</span>
<span className="badge agent-model-runtime__value" title={modelLabel.label ?? t("agents.auto", "Auto")}>
{modelLabel.label ?? t("agents.auto", "Auto")}
</span>
</div>
{agent.state === "error" && agent.lastError ? (
<AgentErrorIndicator
errorText={agent.lastError}

View File

@@ -265,6 +265,56 @@ describe("AgentsView", () => {
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(<AgentsView addToast={mockAddToast} />);
await waitFor(() => {
expect(screen.getByText("Provider Model Agent")).toBeInTheDocument();
});
const getCardModelRow = (agentId: string) => {
const card = Array.from(container.querySelectorAll<HTMLElement>(".agent-card")).find((element) => element.textContent?.includes(agentId));
expect(card).toBeTruthy();
const row = card?.querySelector<HTMLElement>(".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(<AgentsView addToast={mockAddToast} />);