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:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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} />);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user