diff --git a/ui/src/components/IssueDispositionBadge.test.tsx b/ui/src/components/IssueDispositionBadge.test.tsx deleted file mode 100644 index 374bb7b5158..00000000000 --- a/ui/src/components/IssueDispositionBadge.test.tsx +++ /dev/null @@ -1,170 +0,0 @@ -// @vitest-environment jsdom - -import { act } from "react"; -import { createRoot } from "react-dom/client"; -import type { IssueExecutionDisposition } from "@paperclipai/shared"; -import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { - IssueDispositionBadge, - dispositionCategory, - shouldShowDispositionBadge, -} from "./IssueDispositionBadge"; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true; - -describe("IssueDispositionBadge", () => { - let container: HTMLDivElement; - - beforeEach(() => { - container = document.createElement("div"); - document.body.appendChild(container); - }); - - afterEach(() => { - document.body.removeChild(container); - }); - - function render(disposition: IssueExecutionDisposition | null) { - const root = createRoot(container); - act(() => { - root.render(); - }); - return root; - } - - it("renders nothing for null disposition", () => { - const root = render(null); - expect(container.querySelector("[data-execution-disposition-kind]")).toBeNull(); - act(() => root.unmount()); - }); - - it("renders nothing for terminal and resting kinds by default", () => { - let root = render({ kind: "terminal" }); - expect(container.querySelector("[data-execution-disposition-kind]")).toBeNull(); - act(() => root.unmount()); - - root = render({ kind: "resting" }); - expect(container.querySelector("[data-execution-disposition-kind]")).toBeNull(); - act(() => root.unmount()); - }); - - it("renders nothing for dispatchable (transient bookkeeping signal)", () => { - const root = render({ kind: "dispatchable", wakeTarget: "agent-1" }); - expect(container.querySelector("[data-execution-disposition-kind]")).toBeNull(); - act(() => root.unmount()); - }); - - it("renders Live category for live disposition", () => { - const root = render({ kind: "live", path: "active_run" }); - const badge = container.querySelector("[data-execution-disposition-kind]"); - expect(badge?.getAttribute("data-execution-disposition-kind")).toBe("live"); - expect(badge?.getAttribute("data-execution-disposition-category")).toBe("live"); - expect(badge?.textContent).toContain("Live"); - expect(badge?.getAttribute("title")).toContain("Active run"); - expect(badge?.getAttribute("aria-label")).toContain("Active run"); - act(() => root.unmount()); - }); - - it("distinguishes blocked_chain from generic waiting", () => { - const root = render({ kind: "waiting", path: "blocker_chain" }); - const badge = container.querySelector("[data-execution-disposition-kind]"); - expect(badge?.getAttribute("data-execution-disposition-category")).toBe("blocked_chain"); - expect(badge?.textContent).toContain("Blocked"); - act(() => root.unmount()); - }); - - it("renders agent_continuable as Resuming with attempt counter on the visible label", () => { - const root = render({ kind: "agent_continuable", continuationAttempt: 1, maxAttempts: 2 }); - const badge = container.querySelector("[data-execution-disposition-kind]"); - expect(badge?.getAttribute("data-execution-disposition-category")).toBe("resuming"); - expect(badge?.textContent).toContain("Resuming · 1/2"); - // muted indigo treatment, not rose - expect(badge?.className).toContain("indigo"); - expect(badge?.className).not.toContain("rose"); - act(() => root.unmount()); - }); - - it("differentiates human_escalation_required by owner in the visible label", () => { - const owners = [ - { owner: "board", label: "Needs board" }, - { owner: "manager", label: "Needs manager" }, - { owner: "recovery_owner", label: "Needs owner" }, - { owner: "external", label: "Needs external" }, - ] as const; - for (const { owner, label } of owners) { - const root = render({ kind: "human_escalation_required", owner }); - const badge = container.querySelector("[data-execution-disposition-kind]"); - expect(badge?.getAttribute("data-execution-disposition-category")).toBe("needs_attention"); - expect(badge?.textContent).toContain(label); - // shared rose family for the escalation category - expect(badge?.className).toContain("rose"); - act(() => root.unmount()); - } - }); - - it("renders invalid (Stalled) with solid rose-600 fill so it stands out", () => { - const root = render({ - kind: "invalid", - reason: "in_review_without_action_path", - suggestedCorrection: "fix it", - }); - const badge = container.querySelector("[data-execution-disposition-kind]"); - expect(badge?.getAttribute("data-execution-disposition-category")).toBe("invalid"); - expect(badge?.textContent).toContain("Stalled"); - expect(badge?.getAttribute("title")).toContain("Review without action path"); - expect(badge?.className).toContain("bg-rose-600"); - expect(badge?.className).toContain("text-white"); - act(() => root.unmount()); - }); - - it("dispositionCategory routes recovery and continuable to distinct categories", () => { - expect(dispositionCategory({ kind: "recoverable_by_control_plane", recovery: "dispatch" })).toBe( - "recovery", - ); - expect( - dispositionCategory({ kind: "agent_continuable", continuationAttempt: 1, maxAttempts: 2 }), - ).toBe("resuming"); - }); - - it("dispositionCategory returns null for dispatchable", () => { - expect(dispositionCategory({ kind: "dispatchable", wakeTarget: "agent-1" })).toBeNull(); - }); - - describe("shouldShowDispositionBadge", () => { - it("returns false for null, terminal, resting, and dispatchable", () => { - expect(shouldShowDispositionBadge(null)).toBe(false); - expect(shouldShowDispositionBadge({ kind: "terminal" })).toBe(false); - expect(shouldShowDispositionBadge({ kind: "resting" })).toBe(false); - expect(shouldShowDispositionBadge({ kind: "dispatchable", wakeTarget: "a" })).toBe(false); - }); - - it("suppresses generic waiting when the explicit waiting pill is shown", () => { - const waiting: IssueExecutionDisposition = { kind: "waiting", path: "interaction" }; - expect(shouldShowDispositionBadge(waiting, { isExplicitWaiting: true })).toBe(false); - expect(shouldShowDispositionBadge(waiting, { isExplicitWaiting: false })).toBe(true); - }); - - it("suppresses recovery when the IssueBlockedNotice already shows recovery_needed copy", () => { - const recovery: IssueExecutionDisposition = { - kind: "recoverable_by_control_plane", - recovery: "continuation", - }; - expect( - shouldShowDispositionBadge(recovery, { blockerAttentionState: "recovery_needed" }), - ).toBe(false); - expect(shouldShowDispositionBadge(recovery, { blockerAttentionState: "covered" })).toBe(true); - }); - - it("does not suppress resuming when blockerAttention is recovery_needed", () => { - const resuming: IssueExecutionDisposition = { - kind: "agent_continuable", - continuationAttempt: 1, - maxAttempts: 2, - }; - expect( - shouldShowDispositionBadge(resuming, { blockerAttentionState: "recovery_needed" }), - ).toBe(true); - }); - }); -}); diff --git a/ui/src/components/IssueDispositionBadge.tsx b/ui/src/components/IssueDispositionBadge.tsx deleted file mode 100644 index 674a77424f8..00000000000 --- a/ui/src/components/IssueDispositionBadge.tsx +++ /dev/null @@ -1,256 +0,0 @@ -import type { - IssueBlockerAttention, - IssueExecutionDisposition, - IssueExecutionHumanEscalationOwner, - IssueExecutionLivePath, - IssueExecutionRecoveryKind, - IssueExecutionWaitingPath, -} from "@paperclipai/shared"; -import { - AlertOctagon, - AlertTriangle, - CircleDashed, - Hourglass, - Lock, - Play, - RotateCw, - Wrench, -} from "lucide-react"; -import { cn } from "../lib/utils"; - -export type IssueDispositionCategory = - | "live" - | "waiting" - | "blocked_chain" - | "resuming" - | "recovery" - | "needs_attention" - | "invalid" - | "terminal" - | "resting"; - -const CATEGORY_LABEL: Record = { - live: "Live", - waiting: "Waiting", - blocked_chain: "Blocked", - resuming: "Resuming", - recovery: "Recovery", - needs_attention: "Needs attention", - invalid: "Stalled", - terminal: "Done", - resting: "Resting", -}; - -const LIVE_PATH_LABEL: Record = { - active_run: "Active run", - queued_wake: "Queued wake", - scheduled_retry: "Scheduled retry", - deferred_execution: "Deferred execution", -}; - -const WAITING_PATH_LABEL: Record = { - participant: "Review participant", - interaction: "Interaction response", - approval: "Board approval", - human_owner: "Human owner", - blocker_chain: "Blocker chain", - pause_hold: "Tree paused", - review_artifact: "Recovery work", - external_owner_action: "External owner", -}; - -const RECOVERY_LABEL: Record = { - dispatch: "Awaiting dispatch repair", - continuation: "Awaiting continuation", - repair_wait: "Repairing wait state", -}; - -const ESCALATION_VISIBLE_LABEL: Record = { - board: "Needs board", - manager: "Needs manager", - recovery_owner: "Needs owner", - external: "Needs external", -}; - -const ESCALATION_DETAIL_LABEL: Record = { - board: "Board must act", - manager: "Manager must act", - recovery_owner: "Recovery owner must act", - external: "External owner must act", -}; - -export function dispositionCategory( - disposition: IssueExecutionDisposition | null | undefined, -): IssueDispositionCategory | null { - if (!disposition) return null; - switch (disposition.kind) { - case "terminal": - return "terminal"; - case "resting": - return "resting"; - case "live": - return "live"; - case "dispatchable": - // Dispatchable is the system's "ready to wake" state — it's a transient bookkeeping signal, - // not something the board needs to scan for. We surface no badge for it. - return null; - case "waiting": - return disposition.path === "blocker_chain" ? "blocked_chain" : "waiting"; - case "agent_continuable": - return "resuming"; - case "recoverable_by_control_plane": - return "recovery"; - case "human_escalation_required": - return "needs_attention"; - case "invalid": - return "invalid"; - default: - return null; - } -} - -export function dispositionDetailLabel( - disposition: IssueExecutionDisposition | null | undefined, -): string | null { - if (!disposition) return null; - switch (disposition.kind) { - case "live": - return LIVE_PATH_LABEL[disposition.path]; - case "waiting": - return WAITING_PATH_LABEL[disposition.path]; - case "recoverable_by_control_plane": - return RECOVERY_LABEL[disposition.recovery]; - case "agent_continuable": - return `Attempt ${disposition.continuationAttempt} of ${disposition.maxAttempts}`; - case "human_escalation_required": - return ESCALATION_DETAIL_LABEL[disposition.owner]; - case "invalid": - return invalidReasonLabel(disposition.reason); - default: - return null; - } -} - -export function dispositionVisibleLabel( - disposition: IssueExecutionDisposition | null | undefined, - category: IssueDispositionCategory, -): string { - if (disposition?.kind === "agent_continuable") { - return `Resuming · ${disposition.continuationAttempt}/${disposition.maxAttempts}`; - } - if (disposition?.kind === "human_escalation_required") { - return ESCALATION_VISIBLE_LABEL[disposition.owner]; - } - return CATEGORY_LABEL[category]; -} - -function invalidReasonLabel(reason: string): string { - switch (reason) { - case "in_review_without_action_path": - return "Review without action path"; - case "invalid_review_participant": - return "Invalid review participant"; - case "blocked_by_invalid_issue": - return "Blocked by invalid issue"; - case "blocked_by_cancelled_issue": - return "Blocked by cancelled issue"; - case "blocked_by_unassigned_issue": - return "Blocked by unassigned issue"; - case "blocked_by_resting_issue": - return "Blocked by resting issue"; - case "blocked_without_action_path": - return "Blocked without action path"; - case "dual_assignee": - return "Dual assignee"; - default: - return reason.replace(/_/g, " "); - } -} - -const CATEGORY_PILL: Record = { - live: "border-emerald-300/70 bg-emerald-50 text-emerald-800 dark:border-emerald-500/40 dark:bg-emerald-500/10 dark:text-emerald-200", - waiting: "border-sky-300/70 bg-sky-50 text-sky-800 dark:border-sky-500/40 dark:bg-sky-500/10 dark:text-sky-200", - blocked_chain: "border-amber-300/70 bg-amber-50 text-amber-800 dark:border-amber-500/40 dark:bg-amber-500/10 dark:text-amber-200", - // Resuming is benign self-recovery — keep it muted (indigo) so it does not compete with sky/Waiting - // or land in the rose alarm family alongside Recovery / Needs attention / Stalled. - resuming: "border-indigo-300/70 bg-indigo-50 text-indigo-800 dark:border-indigo-500/40 dark:bg-indigo-500/10 dark:text-indigo-200", - recovery: "border-rose-300/70 bg-rose-50 text-rose-800 dark:border-rose-500/40 dark:bg-rose-500/10 dark:text-rose-200", - needs_attention: "border-rose-300/70 bg-rose-50 text-rose-800 dark:border-rose-500/40 dark:bg-rose-500/10 dark:text-rose-200", - // Stalled is the canary. Solid fill makes it the visual outlier in the rose family so a board - // scanning the inbox spots it without reading the label. - invalid: "border-rose-600 bg-rose-600 text-white dark:border-rose-500 dark:bg-rose-600 dark:text-white", - terminal: "border-border bg-muted text-muted-foreground", - resting: "border-border bg-muted text-muted-foreground", -}; - -const CATEGORY_ICON: Record> = { - live: Play, - waiting: Hourglass, - blocked_chain: Lock, - resuming: RotateCw, - recovery: Wrench, - needs_attention: AlertTriangle, - invalid: AlertOctagon, - terminal: CircleDashed, - resting: CircleDashed, -}; - -export interface ShouldShowDispositionBadgeOptions { - isExplicitWaiting?: boolean; - blockerAttentionState?: IssueBlockerAttention["state"] | null; -} - -export function shouldShowDispositionBadge( - disposition: IssueExecutionDisposition | null | undefined, - options: ShouldShowDispositionBadgeOptions = {}, -): boolean { - const category = dispositionCategory(disposition); - if (!category) return false; - if (category === "terminal" || category === "resting") return false; - if (category === "waiting" && options.isExplicitWaiting) return false; - if (category === "recovery" && options.blockerAttentionState === "recovery_needed") return false; - return true; -} - -export interface IssueDispositionBadgeProps { - disposition: IssueExecutionDisposition | null | undefined; - className?: string; - hideLabel?: boolean; - hideForResting?: boolean; - hideForTerminal?: boolean; -} - -export function IssueDispositionBadge({ - disposition, - className, - hideLabel = false, - hideForResting = true, - hideForTerminal = true, -}: IssueDispositionBadgeProps) { - const category = dispositionCategory(disposition); - if (!category) return null; - if (hideForResting && category === "resting") return null; - if (hideForTerminal && category === "terminal") return null; - - const detail = dispositionDetailLabel(disposition); - const label = dispositionVisibleLabel(disposition, category); - const Icon = CATEGORY_ICON[category]; - const tooltip = detail ? `${label} · ${detail}` : label; - - return ( - - - {hideLabel ? null : {label}} - - ); -} diff --git a/ui/src/components/IssueProperties.tsx b/ui/src/components/IssueProperties.tsx index 3d81328893a..c11631a84cb 100644 --- a/ui/src/components/IssueProperties.tsx +++ b/ui/src/components/IssueProperties.tsx @@ -27,7 +27,6 @@ import { formatAssigneeUserLabel } from "../lib/assignees"; import { buildExecutionPolicy, stageParticipantValues } from "../lib/issue-execution-policy"; import { StatusIcon } from "./StatusIcon"; import { PriorityIcon } from "./PriorityIcon"; -import { IssueDispositionBadge, dispositionDetailLabel, shouldShowDispositionBadge } from "./IssueDispositionBadge"; import { Identity } from "./Identity"; import { IssueReferencePill } from "./IssueReferencePill"; import { formatDate, cn, projectUrl } from "../lib/utils"; @@ -1049,31 +1048,6 @@ export function IssueProperties({ onChange={(status) => onUpdate({ status })} showLabel /> - {(() => { - // Mirror IssueRow's suppression: if the chat-thread IssueBlockedNotice already - // conveys the same signal (explicit waiting or recovery_needed), skip the badge so - // the detail page doesn't double-render the same state. - const isExplicitWaiting = - issue.blockerAttention?.state === "covered" - && issue.blockerAttention?.reason === "explicit_waiting"; - if ( - !shouldShowDispositionBadge(issue.executionDisposition, { - isExplicitWaiting, - blockerAttentionState: issue.blockerAttention?.state ?? null, - }) - ) { - return null; - } - const detail = dispositionDetailLabel(issue.executionDisposition); - return ( - - - {detail ? ( - {detail} - ) : null} - - ); - })()} diff --git a/ui/src/components/IssueRow.test.tsx b/ui/src/components/IssueRow.test.tsx index 5469ddfd70a..4250065584d 100644 --- a/ui/src/components/IssueRow.test.tsx +++ b/ui/src/components/IssueRow.test.tsx @@ -261,87 +261,6 @@ describe("IssueRow", () => { }); }); - it("renders the disposition badge for live and invalid dispositions", () => { - const root = createRoot(container); - const liveIssue = createIssue({ - executionDisposition: { kind: "live", path: "active_run" }, - }); - act(() => { - root.render(); - }); - let badge = container.querySelector("[data-execution-disposition-kind]"); - expect(badge?.getAttribute("data-execution-disposition-category")).toBe("live"); - act(() => root.unmount()); - - const root2 = createRoot(container); - const stalledIssue = createIssue({ - status: "in_review", - executionDisposition: { - kind: "invalid", - reason: "in_review_without_action_path", - suggestedCorrection: "fix", - }, - }); - act(() => { - root2.render(); - }); - badge = container.querySelector("[data-execution-disposition-kind]"); - expect(badge?.getAttribute("data-execution-disposition-category")).toBe("invalid"); - expect(badge?.textContent).toContain("Stalled"); - act(() => root2.unmount()); - }); - - it("does not render a disposition badge for the dispatchable bookkeeping signal", () => { - const root = createRoot(container); - const issue = createIssue({ - executionDisposition: { kind: "dispatchable", wakeTarget: "agent-1" }, - }); - act(() => { - root.render(); - }); - expect(container.querySelector("[data-execution-disposition-kind]")).toBeNull(); - act(() => root.unmount()); - }); - - it("renders the resuming badge with the attempt counter on the visible label", () => { - const root = createRoot(container); - const issue = createIssue({ - executionDisposition: { kind: "agent_continuable", continuationAttempt: 1, maxAttempts: 2 }, - }); - act(() => { - root.render(); - }); - const badge = container.querySelector("[data-execution-disposition-kind]"); - expect(badge?.getAttribute("data-execution-disposition-category")).toBe("resuming"); - expect(badge?.textContent).toContain("Resuming · 1/2"); - act(() => root.unmount()); - }); - - it("hides the disposition badge when the explicit waiting pill already conveys the same signal", () => { - const root = createRoot(container); - const issue = createIssue({ - blockerAttention: { - state: "covered", - reason: "explicit_waiting", - unresolvedBlockerCount: 0, - coveredBlockerCount: 0, - stalledBlockerCount: 0, - attentionBlockerCount: 0, - sampleBlockerIdentifier: null, - sampleStalledBlockerIdentifier: null, - nextActionOwner: { type: "user", agentId: null, userId: null }, - nextActionHint: "needs_human_review", - }, - executionDisposition: { kind: "waiting", path: "interaction" }, - }); - act(() => { - root.render(); - }); - const badge = container.querySelector("[data-execution-disposition-kind]"); - expect(badge).toBeNull(); - act(() => root.unmount()); - }); - it("renders without error when titleSuffix is omitted", () => { const root = createRoot(container); diff --git a/ui/src/components/IssueRow.tsx b/ui/src/components/IssueRow.tsx index 5668f3f4428..42dbef5c36c 100644 --- a/ui/src/components/IssueRow.tsx +++ b/ui/src/components/IssueRow.tsx @@ -11,7 +11,6 @@ import { formatAssigneeUserLabel } from "../lib/assignees"; import { cn } from "../lib/utils"; import { StatusIcon } from "./StatusIcon"; import { productivityReviewTriggerLabel } from "./ProductivityReviewBadge"; -import { IssueDispositionBadge, shouldShowDispositionBadge } from "./IssueDispositionBadge"; type UnreadState = "hidden" | "visible" | "fading"; @@ -131,16 +130,6 @@ export function IssueRow({ Waiting ) : null; - // Skip the disposition badge when the existing waiting/recovery affordance already conveys the - // same signal (explicit waiting pill or blockerAttention recovery_needed copy), so we don't - // double up. - const showDispositionBadge = shouldShowDispositionBadge(issue.executionDisposition, { - isExplicitWaiting, - blockerAttentionState: issue.blockerAttention?.state ?? null, - }); - const dispositionBadge = showDispositionBadge ? ( - - ) : null; const productivityReviewIndicator = productivityReview ? ( - {(desktopTrailing || trailingMeta || desktopWaitingPill || dispositionBadge) ? ( + {(desktopTrailing || trailingMeta || desktopWaitingPill) ? ( {desktopWaitingPill} - {dispositionBadge} {desktopTrailing} {trailingMeta ? ( {trailingMeta} ) : null} ) : null} - {mobileWaitingPill || dispositionBadge ? ( + {mobileWaitingPill ? ( {mobileWaitingPill} - {dispositionBadge} ) : null} {showUnreadSlot ? ( diff --git a/ui/storybook/stories/status-language.stories.tsx b/ui/storybook/stories/status-language.stories.tsx index c3a8a8d0e5a..c25a66b8cfa 100644 --- a/ui/storybook/stories/status-language.stories.tsx +++ b/ui/storybook/stories/status-language.stories.tsx @@ -15,7 +15,6 @@ import { IssueRow } from "@/components/IssueRow"; import { MetricCard } from "@/components/MetricCard"; import { PriorityIcon } from "@/components/PriorityIcon"; import { ProductivityReviewBadge } from "@/components/ProductivityReviewBadge"; -import { IssueDispositionBadge } from "@/components/IssueDispositionBadge"; import { QuotaBar } from "@/components/QuotaBar"; import { StatusBadge } from "@/components/StatusBadge"; import { StatusIcon } from "@/components/StatusIcon"; @@ -291,106 +290,6 @@ const coveredBlockedIssue = createIssue({ updatedAt: new Date("2026-04-24T13:40:00.000Z"), }); -const dispositionRowFixtures = [ - { - caption: "Live · active run — emerald", - issue: createIssue({ - id: "issue-live-row", - identifier: "PAP-2820", - issueNumber: 2820, - title: "Phase 4 wiring · active execution", - status: "in_progress", - priority: "medium", - executionDisposition: { kind: "live", path: "active_run" }, - lastActivityAt: new Date("2026-04-29T13:40:00.000Z"), - updatedAt: new Date("2026-04-29T13:40:00.000Z"), - }), - }, - { - caption: "Resuming · 1/2 — muted indigo, attempt counter inline", - issue: createIssue({ - id: "issue-resuming-row", - identifier: "PAP-2821", - issueNumber: 2821, - title: "Continuation queued after transient failure", - status: "in_progress", - priority: "medium", - executionDisposition: { kind: "agent_continuable", continuationAttempt: 1, maxAttempts: 2 }, - lastActivityAt: new Date("2026-04-29T13:42:00.000Z"), - updatedAt: new Date("2026-04-29T13:42:00.000Z"), - }), - }, - { - caption: "Needs board — rose, owner-specific label", - issue: createIssue({ - id: "issue-needs-board-row", - identifier: "PAP-2823", - issueNumber: 2823, - title: "Spend approval blocked on board response", - status: "in_review", - priority: "high", - executionDisposition: { kind: "human_escalation_required", owner: "board" }, - lastActivityAt: new Date("2026-04-29T13:44:00.000Z"), - updatedAt: new Date("2026-04-29T13:44:00.000Z"), - }), - }, - { - caption: "Needs manager — rose, owner-specific label", - issue: createIssue({ - id: "issue-needs-manager-row", - identifier: "PAP-2824", - issueNumber: 2824, - title: "Specialty handoff blocked — manager must act", - status: "in_review", - priority: "high", - executionDisposition: { kind: "human_escalation_required", owner: "manager" }, - lastActivityAt: new Date("2026-04-29T13:45:00.000Z"), - updatedAt: new Date("2026-04-29T13:45:00.000Z"), - }), - }, - { - caption: "Stalled — solid rose-600 fill, the canary outlier", - issue: createIssue({ - id: "issue-stalled-row", - identifier: "PAP-2825", - issueNumber: 2825, - title: "Review without action path — operator must intervene", - status: "in_review", - priority: "high", - executionDisposition: { - kind: "invalid", - reason: "in_review_without_action_path", - suggestedCorrection: "Add participant or move out of review", - }, - lastActivityAt: new Date("2026-04-29T13:50:00.000Z"), - updatedAt: new Date("2026-04-29T13:50:00.000Z"), - }), - }, -]; - -function DispositionRowSurface({ mode, size }: { mode: "light" | "dark"; size: "desktop" | "mobile" }) { - const isDark = mode === "dark"; - const isMobile = size === "mobile"; - return ( -
-
-
- {size} · {mode} -
-
- {dispositionRowFixtures.map((fixture) => ( - } - /> - ))} -
-
-
- ); -} - function summaryBlocker( partial: Partial & Pick, ): IssueRelationIssueSummary { @@ -958,76 +857,6 @@ function StatusLanguage() {

-
-

- The badge derives every category from the API's executionDisposition field — - the same classifier the recovery and transition guard services use. The board reads this - instead of inferring health from status alone. -

-
- {[ - { label: "Live · active run", disposition: { kind: "live", path: "active_run" } as const }, - { label: "Live · queued wake", disposition: { kind: "live", path: "queued_wake" } as const }, - { label: "Waiting · interaction", disposition: { kind: "waiting", path: "interaction" } as const }, - { label: "Waiting · approval", disposition: { kind: "waiting", path: "approval" } as const }, - { label: "Waiting · participant", disposition: { kind: "waiting", path: "participant" } as const }, - { label: "Blocked · chain", disposition: { kind: "waiting", path: "blocker_chain" } as const }, - { label: "Resuming · 1/2", disposition: { kind: "agent_continuable", continuationAttempt: 1, maxAttempts: 2 } as const }, - { label: "Resuming · 2/2", disposition: { kind: "agent_continuable", continuationAttempt: 2, maxAttempts: 2 } as const }, - { label: "Recovery · continuation", disposition: { kind: "recoverable_by_control_plane", recovery: "continuation" } as const }, - { label: "Needs board", disposition: { kind: "human_escalation_required", owner: "board" } as const }, - { label: "Needs manager", disposition: { kind: "human_escalation_required", owner: "manager" } as const }, - { label: "Needs owner", disposition: { kind: "human_escalation_required", owner: "recovery_owner" } as const }, - { label: "Needs external", disposition: { kind: "human_escalation_required", owner: "external" } as const }, - { - label: "Stalled review", - disposition: { - kind: "invalid", - reason: "in_review_without_action_path", - suggestedCorrection: "Add participant or move out of review", - } as const, - }, - { - label: "Stalled chain", - disposition: { - kind: "invalid", - reason: "blocked_without_action_path", - suggestedCorrection: "Resolve or remove the orphan blocker", - } as const, - }, - ].map((item) => ( -
- {item.label} - -
- ))} -
-

- Stalled uses a solid rose-600 fill so it stands out against the rose family at scan distance. - Resuming sits in muted indigo (clearly off the rose alarm family) and surfaces the - continuation counter on the visible badge. Each escalation owner gets its own visible - label so the board can see who is blocking work without hovering. Dispatchable is - intentionally invisible — it's a transient bookkeeping signal, not something to scan for. -

-
- -
-
- - - - -
-

- Verifies the badge in production layout. Stalled is visually distinct from any rose-family - row at full-page scan, Resuming reads as benign muted indigo with the attempt counter, - and Needs board / Needs manager are independently legible without hover. -

-
-