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