diff --git a/ui/src/components/EntityRow.test.tsx b/ui/src/components/EntityRow.test.tsx
index d4f7eb9b27e..5845a9d33e8 100644
--- a/ui/src/components/EntityRow.test.tsx
+++ b/ui/src/components/EntityRow.test.tsx
@@ -59,4 +59,37 @@ describe("EntityRow", () => {
const markup = renderToStaticMarkup();
expect(markup).toContain("min-w-0 flex-1");
});
+
+ it("gives the title a min-width floor and lets meta shrink under titlePriority (PAP-12988)", () => {
+ const markup = renderToStaticMarkup(
+ chips}
+ />,
+ );
+
+ // The name keeps a usable floor instead of collapsing to zero...
+ expect(markup).toContain("min-w-(--sz-7rem)");
+ // ...and the meta cluster is the item that yields (shrinks), not the title.
+ expect(markup).toContain("min-w-0 shrink");
+ expect(markup).not.toContain('class="flex items-center gap-2 shrink-0"');
+ });
+
+ it("stacks a secondaryRow on its own line beneath the main row (PAP-12988)", () => {
+ const markup = renderToStaticMarkup(
+ chips-inline}
+ secondaryRow={chips-stacked}
+ />,
+ );
+
+ // The secondary content renders, and the shell switches from a single flex
+ // row to a stacked block layout so the cluster gets its own full-width line.
+ expect(markup).toContain("secondary-cell");
+ expect(markup).toContain("chips-stacked");
+ expect(markup).not.toMatch(/^
}
>
);
+ // With a secondaryRow, wrap the main line in its own flex row and stack the
+ // secondary content beneath it (indented to align under the title, past the
+ // leading capsule). Without it, `content` is rendered directly (unchanged).
+ const body = secondaryRow ? (
+ <>
+
);
}
diff --git a/ui/src/components/SidebarAgents.tsx b/ui/src/components/SidebarAgents.tsx
index cea2fdaeb80..4def4ef4080 100644
--- a/ui/src/components/SidebarAgents.tsx
+++ b/ui/src/components/SidebarAgents.tsx
@@ -151,17 +151,10 @@ function SidebarAgentItem({
: isPaused && hasInvalidOrgChain
? "Invalid org chain"
: pauseResumeLabel;
- const trailingLabelParts: string[] = [];
- if (builtInStatus) {
- trailingLabelParts.push(
- builtInStatus === "pending_approval"
- ? "Built-in, pending approval"
- : builtInStatus === "needs_setup"
- ? "Built-in, needs setup"
- : "Built-in",
- );
- }
- if (hasInvalidOrgChain) trailingLabelParts.push("Invalid reporting chain");
+ const trailingLabel = [
+ builtInStatus ? `Built-in agent ${builtInStatus.replace(/_/g, " ")}` : null,
+ hasInvalidOrgChain ? "Invalid reporting chain" : null,
+ ].filter(Boolean).join(", ") || undefined;
// C11 (DECISION-SHEET.md): the row itself is a SidebarNavItem, so agent rows
// share the nav-row chrome (type, active state, rail tooltip, live dot).
@@ -180,7 +173,7 @@ function SidebarAgentItem({
)}
trailing={
builtInStatus || hasInvalidOrgChain ? (
-
+
{builtInStatus ? (
<>
@@ -193,7 +186,7 @@ function SidebarAgentItem({
) : undefined
}
- trailingLabel={trailingLabelParts.length > 0 ? trailingLabelParts.join(", ") : undefined}
+ trailingLabel={trailingLabel}
liveAccessory={
agent.pauseReason === "budget" ? : undefined
}
diff --git a/ui/src/pages/Agents.tsx b/ui/src/pages/Agents.tsx
index 8b6b18c7cee..f166f0807ef 100644
--- a/ui/src/pages/Agents.tsx
+++ b/ui/src/pages/Agents.tsx
@@ -314,6 +314,32 @@ export function Agents() {
const agentJoinLeavePending = agentPending && membershipMutation.variables?.starred === undefined;
const agentStarred = isStarred(membershipsQuery.data, "agent", agent.id);
const builtInState = builtInByAgentId.get(agent.id);
+ // Provenance badge + lifecycle chip + inline `Set up`. Rendered inline in
+ // `meta` at xl (where there's room and the meta columns align) and on a
+ // dedicated full-width line beneath the name below xl, so the chips never
+ // starve the name — the row's primary identifier — at narrow widths.
+ const builtInCluster = builtInState ? (
+ <>
+
+
+ {builtInState.status === "needs_setup" && (
+ {
+ e.preventDefault();
+ e.stopPropagation();
+ }}
+ >
+
+
+ )}
+ >
+ ) : null;
return (
)}
+ secondaryRow={
+ builtInCluster ? (
+
+ {builtInCluster}
+
+ ) : undefined
+ }
meta={
- {builtInState && (
-
- {/* Provenance sits immediately after the name, then the
- lifecycle chip; the real agent status stays in `trailing`. */}
-
-
- {builtInState.status === "needs_setup" && (
- {
- e.preventDefault();
- e.stopPropagation();
- }}
- >
-
-
- )}
+ {builtInCluster && (
+
+ {builtInCluster}
)}
@@ -624,10 +638,11 @@ function OrgTreeNode({
) : (
)}
- {/* min-w-0 + truncate so deep indentation on narrow screens shortens
- the name with an ellipsis instead of overflowing the row. */}
-
-
+
+ {/* Name floor + `truncate` keeps the primary identifier readable; the
+ cluster wraps to a second line under pressure instead of starving
+ the name at narrow widths. */}
+
{node.name}
{roleLabels[node.role] ?? node.role}
diff --git a/ui/storybook/stories/built-in-agents.stories.tsx b/ui/storybook/stories/built-in-agents.stories.tsx
index b623bde2c14..04a52ce5d27 100644
--- a/ui/storybook/stories/built-in-agents.stories.tsx
+++ b/ui/storybook/stories/built-in-agents.stories.tsx
@@ -66,6 +66,42 @@ const notProvisionedState: BuiltInAgentState = {
pauseReason: null,
};
+/**
+ * Mirrors Agents.tsx renderAgentRow: the built-in cluster sits inline in `meta`
+ * at xl and drops to a full-width `secondaryRow` beneath the name below xl, with
+ * `titlePriority` giving the name a floor so it never collapses (PAP-12988).
+ */
+function RosterRow({
+ name,
+ lifecycle,
+ status,
+}: {
+ name: string;
+ lifecycle?: "needs_setup" | "pending_approval";
+ status: string;
+}) {
+ const cluster = (
+ <>
+
+ {lifecycle && }
+ {lifecycle === "needs_setup" && (
+
+ )}
+ >
+ );
+ return (
+ {cluster}
}
+ meta={
{cluster}
}
+ trailing={}
+ />
+ );
+}
+
function SectionLabel({ children }: { children: React.ReactNode }) {
return (
@@ -89,40 +125,15 @@ export const SurfaceGallery: Story = {
Board 1 — Roster rows
-
-
-
-
-
- }
- trailing={}
- />
-
-
-
- }
- trailing={}
- />
-
-
-
-
- }
- trailing={}
- />
+
+
+
+
+ Resize below the xl breakpoint to see the badge/action
+ cluster drop to a second line so the agent name never collapses
+ (PAP-12988).
+