Remove decision and review summaries from issue headers (#10891)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Operators use issue pages to read task state and control task work
> - The issue header showed separate summaries for open decisions and
review paths
> - These summaries repeated state that belongs in the Decisions view
> - The extra sections added noise before the issue description and
thread
> - This pull request removes both header summaries and keeps decision
actions in the Decisions view
> - The benefit is a simpler issue header with one place for decision
work

## Linked Issues or Issue Description

**What existing behavior does this improve?**

The issue detail header shows separate pending-decision and review-path
sections.

**Subsystem affected**

`ui/` — React and Vite board UI.

**Current behavior**

An issue header can show a decision strip and a larger review panel
before the issue content.

**Proposed behavior**

The issue header does not show either decision section. Operators
continue to manage decisions and stalled reviews in the Decisions view.

**Reason and benefit**

This removes duplicate decision state from the issue header and reduces
visual noise.

**Breaking changes**

The issue page no longer provides these summaries or shortcuts. Decision
data, review state, and the Decisions view do not change.

## What Changed

- Removed the pending-decision strip and review-path panel from the
issue detail header.
- Deleted the two unused header components and the panel-specific test.
- Kept stalled-review actions and their Storybook examples in the
Decisions queue.
- Added an issue-detail regression test that covers both removed
sections.

## Verification

- `pnpm --filter @paperclipai/ui exec vitest run
src/pages/IssueDetail.test.tsx` (46 tests passed)
- `pnpm --filter @paperclipai/ui typecheck`
- `pnpm check:token-gates`
- `pnpm build-storybook`
- `git diff --check`

## Risks

- Low risk. This change removes two issue-header surfaces. It does not
change decision APIs or data.
- Users must open the Decisions view to find pending decisions and
stalled-review actions.

> This change does not duplicate planned core work in `ROADMAP.md`.
GitHub searches found no related open issue or pull request.

## Model Used

- OpenAI Codex, GPT-5. The exact deployment ID and context window are
not exposed. Tool use and code execution were enabled.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Dotta 2026-08-05 10:23:16 -05:00 committed by GitHub
parent 6ffe9df842
commit 14d755824c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 84 additions and 508 deletions

View File

@ -1,183 +0,0 @@
// @vitest-environment jsdom
import { createRoot } from "react-dom/client";
import { flushSync } from "react-dom";
import type { ReactElement } from "react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import type { IssueReviewAttention } from "@paperclipai/shared";
import { IssueReviewPanel, type ReviewPanelIssue } from "./IssueReviewPanel";
import { ToastProvider } from "../context/ToastContext";
import { ToastViewport } from "./ToastViewport";
const decideStalledReviewMock = vi.hoisted(() => vi.fn(() => Promise.resolve({})));
vi.mock("../api/issues", () => ({
issuesApi: {
decideStalledReview: decideStalledReviewMock,
},
}));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
function act<T>(callback: () => T): T {
let result: T | undefined;
flushSync(() => {
result = callback();
});
return result as T;
}
/** react-query dispatches the mutationFn on a microtask — let it settle. */
async function flushMicrotasks() {
await Promise.resolve();
await Promise.resolve();
}
let root: ReturnType<typeof createRoot> | null = null;
let container: HTMLDivElement | null = null;
afterEach(() => {
if (root) act(() => root?.unmount());
root = null;
container?.remove();
container = null;
vi.clearAllMocks();
});
function render(element: ReactElement) {
container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container);
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
act(() =>
root?.render(
<ToastProvider>
<QueryClientProvider client={client}>
{element}
<ToastViewport />
</QueryClientProvider>
</ToastProvider>,
),
);
return container;
}
function buildIssue(reviewAttention: IssueReviewAttention | undefined, status: ReviewPanelIssue["status"] = "in_review"): ReviewPanelIssue {
return { id: "issue-1", companyId: "c1", status, reviewAttention };
}
const coveredAttention: IssueReviewAttention = {
state: "covered",
reason: "Review has a maintained action path.",
paths: [
{
kind: "interaction",
label: "Pending request confirmation",
responder: "Board",
since: "2026-08-02T00:00:00.000Z",
ref: "interaction-1",
},
{
kind: "human_reviewer",
label: "Human reviewer",
responder: "Dotta",
since: "2026-08-02T00:00:00.000Z",
ref: null,
},
],
};
const stalledAttention: IssueReviewAttention = {
state: "stalled",
reason: "Issue is in review without a maintained action path.",
paths: [],
};
describe("IssueReviewPanel", () => {
it("renders nothing when the issue is not in review", () => {
const el = render(<IssueReviewPanel issue={buildIssue(coveredAttention, "in_progress")} />);
expect(el.querySelector('[data-testid="issue-review-panel"]')).toBeNull();
});
it("renders nothing when reviewAttention is absent (older payloads)", () => {
const el = render(<IssueReviewPanel issue={buildIssue(undefined)} />);
expect(el.querySelector('[data-testid="issue-review-panel"]')).toBeNull();
});
it("covered: names each maintained path with its responder and outcome hint", () => {
const el = render(<IssueReviewPanel issue={buildIssue(coveredAttention)} />);
const panel = el.querySelector('[data-testid="issue-review-panel"]');
expect(panel?.getAttribute("data-review-state")).toBe("covered");
expect(panel?.textContent).toContain("In review");
expect(panel?.textContent).toContain("Pending request confirmation");
expect(panel?.textContent).toContain("Board");
expect(panel?.textContent).toContain("Human reviewer");
expect(panel?.textContent).toContain("Dotta");
// The outcome hint tells the operator what each verb does.
expect(panel?.textContent).toContain("Approving marks this issue done");
// Covered reviews do not expose the escape actions.
expect(panel?.textContent).not.toContain("Send back to work");
});
it("stalled: shows the amber notice and the three review actions", () => {
const el = render(<IssueReviewPanel issue={buildIssue(stalledAttention)} />);
const panel = el.querySelector('[data-testid="issue-review-panel"]');
expect(panel?.getAttribute("data-review-state")).toBe("stalled");
expect(panel?.textContent).toContain("Nobody is reviewing this");
expect(panel?.textContent).toContain("Approve");
expect(panel?.textContent).toContain("Request changes");
expect(panel?.textContent).toContain("Send back to work");
});
it("stalled: request-changes is disabled until a note is entered", () => {
const el = render(<IssueReviewPanel issue={buildIssue(stalledAttention)} />);
const requestChanges = el.querySelector<HTMLButtonElement>(
'[data-testid="stalled-review-request-changes"]',
);
expect(requestChanges?.disabled).toBe(true);
const note = el.querySelector<HTMLTextAreaElement>('[data-testid="stalled-review-note"]');
act(() => {
const setter = Object.getOwnPropertyDescriptor(
window.HTMLTextAreaElement.prototype,
"value",
)!.set!;
setter.call(note, "Please fix the failing test");
note!.dispatchEvent(new Event("input", { bubbles: true }));
});
expect(requestChanges?.disabled).toBe(false);
});
it("stalled: approving posts approve to the decision endpoint", async () => {
const el = render(<IssueReviewPanel issue={buildIssue(stalledAttention)} />);
const approve = el.querySelector<HTMLButtonElement>('[data-testid="stalled-review-approve"]');
act(() => approve!.click());
await flushMicrotasks();
expect(decideStalledReviewMock).toHaveBeenCalledWith("issue-1", {
action: "approve",
note: undefined,
});
});
it("stalled: send-back forwards the typed note", async () => {
const el = render(<IssueReviewPanel issue={buildIssue(stalledAttention)} />);
const note = el.querySelector<HTMLTextAreaElement>('[data-testid="stalled-review-note"]');
act(() => {
const setter = Object.getOwnPropertyDescriptor(
window.HTMLTextAreaElement.prototype,
"value",
)!.set!;
setter.call(note, "Back to you");
note!.dispatchEvent(new Event("input", { bubbles: true }));
});
const sendBack = el.querySelector<HTMLButtonElement>('[data-testid="stalled-review-send-back"]');
act(() => sendBack!.click());
await flushMicrotasks();
expect(decideStalledReviewMock).toHaveBeenCalledWith("issue-1", {
action: "send_back",
note: "Back to you",
});
});
});

View File

@ -1,160 +0,0 @@
import type { IssueReviewAttention, IssueReviewAttentionPath, IssueStatus } from "@paperclipai/shared";
import {
Activity,
AlertTriangle,
Bell,
Clock,
HelpCircle,
LifeBuoy,
ShieldCheck,
UserCheck,
Users,
type LucideIcon,
} from "lucide-react";
import { cn, relativeTime } from "../lib/utils";
import { StatusGlyph } from "./StatusGlyph";
import { StalledReviewActions } from "./StalledReviewActions";
/** Minimal shape the panel needs — the full `Issue` satisfies it. */
export interface ReviewPanelIssue {
id: string;
companyId: string;
status: IssueStatus;
reviewAttention?: IssueReviewAttention;
}
const PATH_ICON: Record<IssueReviewAttentionPath["kind"], LucideIcon> = {
execution_participant: Users,
interaction: HelpCircle,
approval: ShieldCheck,
monitor: Clock,
human_reviewer: UserCheck,
active_run: Activity,
queued_wake: Bell,
recovery: LifeBuoy,
};
/**
* Persistent review panel pinned above the thread whenever an issue is
* `in_review` (PAP-16080 §4.4). Driven by `issue.reviewAttention` (P2):
*
* - **covered** names WHAT is being reviewed (each maintained path), WHO
* decides it, and since when, plus what each outcome does. Keeps a stalled
* review from ever being the *only* thing an operator sees, and surfaces the
* responder so a covered review reads as "someone has this".
* - **stalled** the amber "nobody is reviewing this" notice with the three
* escape actions (approve / request changes / send back), so an agent-owned
* review can never become an invisible zombie (the PAP-14994 failure).
*
* Renders nothing when the issue is not in review, or when `reviewAttention` is
* absent (older payloads) the thread simply shows as it does today.
*/
export function IssueReviewPanel({ issue }: { issue: ReviewPanelIssue }) {
const reviewAttention = issue.reviewAttention;
if (issue.status !== "in_review" || !reviewAttention) return null;
if (reviewAttention.state === "stalled") {
return <StalledReviewPanel issue={issue} reason={reviewAttention.reason} />;
}
if (reviewAttention.state === "covered") {
return <CoveredReviewPanel reviewAttention={reviewAttention} />;
}
return null;
}
function CoveredReviewPanel({ reviewAttention }: { reviewAttention: IssueReviewAttention }) {
return (
<div
className="flex flex-col gap-3 rounded-xl border border-violet-500/30 bg-violet-500/5 px-4 py-3"
data-testid="issue-review-panel"
data-review-state="covered"
>
<div className="flex items-start gap-2">
<StatusGlyph status="in_review" size="md" />
<div className="min-w-0">
<p className="text-sm font-semibold text-violet-950 dark:text-violet-100">In review</p>
<p className="text-xs text-muted-foreground">
{reviewAttention.reason ?? "This issue has a maintained review path."}
</p>
</div>
</div>
<ul className="flex flex-col gap-1.5" data-testid="issue-review-paths">
{reviewAttention.paths.map((path, index) => (
<ReviewPathRow key={`${path.kind}-${path.ref ?? index}`} path={path} />
))}
</ul>
<p className="text-(length:--text-nano) text-muted-foreground">
Approving marks this issue done. Requesting changes or sending it back returns it to the
assignee.
</p>
</div>
);
}
function ReviewPathRow({ path }: { path: IssueReviewAttentionPath }) {
const Icon = PATH_ICON[path.kind] ?? Activity;
return (
<li className="flex items-center gap-2 text-xs" data-review-path-kind={path.kind}>
<Icon className="h-3.5 w-3.5 shrink-0 text-violet-600 dark:text-violet-400" aria-hidden />
<span className="font-medium text-foreground">{path.label}</span>
{path.responder && (
<>
<PathDot />
<span className="text-muted-foreground">{path.responder}</span>
</>
)}
{path.since && (
<>
<PathDot />
<span className="text-muted-foreground" title={new Date(path.since).toLocaleString()}>
{relativeTime(path.since)}
</span>
</>
)}
</li>
);
}
function PathDot() {
return (
<span className="text-muted-foreground/60" aria-hidden>
·
</span>
);
}
function StalledReviewPanel({
issue,
reason,
}: {
issue: ReviewPanelIssue;
reason: string | null;
}) {
return (
<div
className={cn(
"flex flex-col gap-3 rounded-xl border border-amber-400/60 bg-amber-50/70 px-4 py-3",
"dark:border-amber-500/40 dark:bg-amber-500/10",
)}
data-testid="issue-review-panel"
data-review-state="stalled"
>
<div className="flex items-start gap-2">
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-amber-600 dark:text-amber-400" aria-hidden />
<div className="min-w-0">
<p className="text-sm font-semibold text-amber-950 dark:text-amber-100">
Nobody is reviewing this
</p>
<p className="text-xs text-amber-900/80 dark:text-amber-100/80">
{reason
?? "No reviewer, interaction, approval, or monitor exists — the review has no owner."}
</p>
</div>
</div>
<StalledReviewActions issueId={issue.id} companyId={issue.companyId} />
</div>
);
}

View File

@ -1,38 +0,0 @@
import { useQuery } from "@tanstack/react-query";
import { Scale } from "lucide-react";
import { Link } from "@/lib/router";
import { decisionsApi } from "../api/decisions";
import { queryKeys } from "../lib/queryKeys";
/**
* Information-scent breadcrumb on a target issue: when a decision proposed
* *elsewhere* targets this issue (via `decision_target_issues`), surface it so
* the pending decision isn't lost in another thread. It never actuates inline
* decisions are always decided from the one Decisions inbox (PAP-14966 §3).
*/
export function PendingDecisionStrip({ companyId, issueId }: { companyId: string; issueId: string }) {
const { data } = useQuery({
queryKey: queryKeys.decisions.forTargetIssue(companyId, issueId),
queryFn: () => decisionsApi.list(companyId, { targetIssueId: issueId, status: "open" }),
enabled: !!companyId && !!issueId,
});
const count = data?.length ?? 0;
if (count === 0) return null;
// Deep-link to the single decision when there's just one; otherwise the inbox.
const to = count === 1 ? `/decisions?decisionId=${data![0]!.id}` : "/decisions";
return (
<Link
to={to}
className="flex items-center gap-2 rounded-lg border-l-2 border-violet-500/60 bg-violet-500/5 px-3 py-2 text-sm text-violet-900 transition-colors hover:bg-violet-500/10 dark:text-violet-100"
>
<Scale className="h-4 w-4 shrink-0 text-violet-600 dark:text-violet-400" aria-hidden />
<span className="font-medium">
{count === 1 ? "1 pending decision affects this issue" : `${count} pending decisions affect this issue`}
</span>
<span className="text-xs text-muted-foreground">Review in Decisions </span>
</Link>
);
}

View File

@ -69,6 +69,10 @@ const mockProjectsApi = vi.hoisted(() => ({
list: vi.fn(),
}));
const mockDecisionsApi = vi.hoisted(() => ({
list: vi.fn(),
}));
const mockInstanceSettingsApi = vi.hoisted(() => ({
getGeneral: vi.fn(),
getExperimental: vi.fn(),
@ -134,6 +138,10 @@ vi.mock("../api/projects", () => ({
projectsApi: mockProjectsApi,
}));
vi.mock("../api/decisions", () => ({
decisionsApi: mockDecisionsApi,
}));
vi.mock("../api/instanceSettings", () => ({
instanceSettingsApi: mockInstanceSettingsApi,
}));
@ -1010,6 +1018,7 @@ describe("IssueDetail", () => {
mockAccessApi.listUserDirectory.mockResolvedValue({ users: [] });
mockAuthApi.getSession.mockResolvedValue({ session: null, user: null });
mockProjectsApi.list.mockResolvedValue([]);
mockDecisionsApi.list.mockResolvedValue([]);
mockInstanceSettingsApi.getGeneral.mockResolvedValue({
keyboardShortcuts: false,
feedbackDataSharingPreference: "prompt",
@ -1066,6 +1075,39 @@ describe("IssueDetail", () => {
).toBe(false);
});
it("does not load or render decision sections in the issue header", async () => {
mockIssuesApi.get.mockResolvedValue(createIssue({
status: "in_review",
reviewAttention: {
state: "covered",
reason: "Review has a maintained action path.",
paths: [
{
kind: "interaction",
label: "Pending request confirmation",
responder: "Board",
since: "2026-04-21T00:00:00.000Z",
ref: "interaction-1",
},
],
},
}));
await act(async () => {
root.render(
<QueryClientProvider client={queryClient}>
<IssueDetail />
</QueryClientProvider>,
);
});
await flushReact();
await flushReact();
expect(container.textContent).toContain("Issue detail smoke");
expect(container.querySelector('[data-testid="issue-review-panel"]')).toBeNull();
expect(mockDecisionsApi.list).not.toHaveBeenCalled();
});
it("updates status and priority from the task header controls", async () => {
const issue = createIssue({ status: "todo", priority: "medium" });
mockIssuesApi.get.mockResolvedValue(issue);

View File

@ -122,8 +122,6 @@ import {
} from "../components/IssueMonitorBanner";
import { IssueScheduledRetryCard } from "../components/IssueScheduledRetryCard";
import { IssueProperties } from "../components/IssueProperties";
import { PendingDecisionStrip } from "../components/PendingDecisionStrip";
import { IssueReviewPanel } from "../components/IssueReviewPanel";
import { PauseAffectsSummaryView } from "../components/interrupt-handoff/InterruptHandoffViews";
import { computePauseAffectsSummary } from "../lib/interrupt-handoff";
import { useIssueExternalObjects } from "../hooks/useIssueExternalObjects";
@ -4527,10 +4525,6 @@ export function IssueDetail() {
checkingNow={checkIssueMonitorNow.isPending}
/>
<PendingDecisionStrip companyId={issue.companyId} issueId={issue.id} />
<IssueReviewPanel issue={issue} />
{taskChatShellEnabled ? null : (
<InlineEditor
value={issue.description ?? ""}

View File

@ -1,26 +1,10 @@
import { useState } from "react";
import type { Meta, StoryObj } from "@storybook/react-vite";
import type { AttentionItem, IssueReviewAttention, IssueReviewAttentionPath } from "@paperclipai/shared";
import { IssueReviewPanel, type ReviewPanelIssue } from "@/components/IssueReviewPanel";
import type { AttentionItem } from "@paperclipai/shared";
import { AttentionQueueRow } from "@/components/AttentionQueueRow";
// PAP-16080 §4.4 — the review panel that pins above an `in_review` thread and
// the same three verbs inline on the /decisions card. These stories are the
// design/QA reference for the covered, stalled, and post-action states in both
// light and dark themes.
function path(overrides: Partial<IssueReviewAttentionPath> & Pick<IssueReviewAttentionPath, "kind" | "label">): IssueReviewAttentionPath {
return {
responder: null,
since: "2026-08-02T00:30:00.000Z",
ref: null,
...overrides,
};
}
function issue(reviewAttention: IssueReviewAttention): ReviewPanelIssue {
return { id: "issue-1", companyId: "company-1", status: "in_review", reviewAttention };
}
// Stalled review actions remain available where decisions are handled. The
// issue header no longer duplicates decision or review-path summaries.
function Frame({
label,
@ -29,7 +13,7 @@ function Frame({
}: {
label: string;
children: React.ReactNode;
/** Content width in px — narrow (390) exercises the stalled action row's base stacked layout. */
/** Content width in px — narrow (390) exercises the action row's stacked layout. */
width?: number;
}) {
return (
@ -40,101 +24,6 @@ function Frame({
);
}
const meta = {
title: "Product/Issue/Review panel",
component: IssueReviewPanel,
args: { issue: issue({ state: "none", paths: [], reason: null }) },
parameters: {
docs: {
description: {
component:
"Pinned above the thread whenever an issue is `in_review`. Covered names each maintained review path (what/who/since) and what each outcome does; stalled shows the amber \"nobody is reviewing this\" notice with approve / request-changes / send-back. The same three verbs actuate inline on the /decisions card when a review goes stalled.",
},
},
},
} satisfies Meta<typeof IssueReviewPanel>;
export default meta;
type Story = StoryObj<typeof meta>;
export const CoveredSinglePath: Story = {
name: "Covered · one maintained path",
render: () => (
<Frame label="In review · a pending confirmation is waiting on the board">
<IssueReviewPanel
issue={issue({
state: "covered",
reason: "Review has a maintained action path.",
paths: [
path({
kind: "interaction",
label: "Pending request confirmation",
responder: "Board",
ref: "interaction-1",
}),
],
})}
/>
</Frame>
),
};
export const CoveredMultiplePaths: Story = {
name: "Covered · reviewer + monitor",
render: () => (
<Frame label="In review · a named human reviewer plus a scheduled monitor">
<IssueReviewPanel
issue={issue({
state: "covered",
reason: "Review has 2 maintained action paths.",
paths: [
path({ kind: "human_reviewer", label: "Human reviewer", responder: "Dotta" }),
path({
kind: "monitor",
label: "Scheduled review monitor",
responder: null,
since: "2026-08-02T00:10:00.000Z",
}),
],
})}
/>
</Frame>
),
};
export const Stalled: Story = {
name: "Stalled · nobody is reviewing this",
render: () => (
<Frame label="In review · no reviewer, interaction, approval, or monitor exists">
<IssueReviewPanel
issue={issue({
state: "stalled",
paths: [],
reason:
"Issue is in review without a maintained reviewer, interaction, approval, monitor, run, wake, or recovery path.",
})}
/>
</Frame>
),
};
export const StalledNarrow: Story = {
name: "Stalled · 390px phone (action row stacks)",
render: () => (
<Frame width={390} label="In review · phone width — the three verbs stack, never overlap">
<IssueReviewPanel
issue={issue({
state: "stalled",
paths: [],
reason:
"Issue is in review without a maintained reviewer, interaction, approval, monitor, run, wake, or recovery path.",
})}
/>
</Frame>
),
};
const stalledReviewRow: AttentionItem = {
id: "review:issue-1",
companyId: "company-1",
@ -152,8 +41,16 @@ const stalledReviewRow: AttentionItem = {
whyNow:
"Issue is in review without a maintained reviewer, interaction, approval, monitor, run, wake, or recovery path.",
decisionVerbs: [
{ id: "choose_review_path", label: "Choose review path", description: "Add a reviewer or waiting path, return the issue to work, or accept it." },
{ id: "request_changes", label: "Request changes", description: "Return the issue to the assignee with changes requested." },
{
id: "choose_review_path",
label: "Choose review path",
description: "Add a reviewer or waiting path, return the issue to work, or accept it.",
},
{
id: "request_changes",
label: "Request changes",
description: "Return the issue to the assignee with changes requested.",
},
],
inlineResolvable: true,
entryRule: "",
@ -185,12 +82,36 @@ const stalledReviewRow: AttentionItem = {
trainingExampleId: null,
};
const meta = {
title: "Product/Decisions/Stalled review actions",
component: AttentionQueueRow,
args: {
item: stalledReviewRow,
companyId: "company-1",
expanded: true,
onToggleExpand: () => {},
onDismiss: () => {},
},
parameters: {
docs: {
description: {
component:
"Stalled reviews remain actionable in the Decisions queue after removing decision summaries from the issue header.",
},
},
},
} satisfies Meta<typeof AttentionQueueRow>;
export default meta;
type Story = StoryObj<typeof meta>;
export const DecisionsCardInline: Story = {
name: "Decisions card · stalled review resolves in-row",
name: "Stalled review resolves in-row",
render: () => {
const [expanded, setExpanded] = useState(true);
return (
<Frame label="/decisions · a stalled review actuates the three verbs inline">
<Frame label="Decisions · a stalled review actuates the three verbs inline">
<AttentionQueueRow
item={stalledReviewRow}
companyId="company-1"
@ -204,11 +125,11 @@ export const DecisionsCardInline: Story = {
};
export const DecisionsCardInlineNarrow: Story = {
name: "Decisions card · 390px phone (verbs stack in-row)",
name: "390px phone (verbs stack in-row)",
render: () => {
const [expanded, setExpanded] = useState(true);
return (
<Frame width={390} label="/decisions · phone width — the inline verbs stack, never overlap">
<Frame width={390} label="Decisions · phone width — the inline verbs stack, never overlap">
<AttentionQueueRow
item={stalledReviewRow}
companyId="company-1"