feat(FN-814): add awaiting-approval board highlight with distinct visuals

- Add amber/orange highlight styling for tasks with awaiting-approval status on the board
- Update TaskCard component to render awaiting-approval status badge and border styling
- Add comprehensive unit tests for awaiting-approval card rendering scenarios
- Document awaiting-approval board highlight behavior in README
This commit is contained in:
gsxdsm
2026-04-03 23:21:06 -07:00
parent 98bcb5df5c
commit 2c861e3f75
4 changed files with 178 additions and 8 deletions

View File

@@ -315,8 +315,9 @@ function TaskCardComponent({
const isFailed = task.status === "failed";
const isPaused = task.paused === true;
const isAwaitingApproval = task.column === "triage" && task.status === "awaiting-approval";
const isArchived = task.column === "archived";
const isAgentActive = !globalPaused && !queued && !isFailed && !isPaused && (task.column === "in-progress" || ACTIVE_STATUSES.has(task.status as string));
const isAgentActive = !globalPaused && !queued && !isFailed && !isPaused && !isAwaitingApproval && (task.column === "in-progress" || ACTIVE_STATUSES.has(task.status as string));
const isDraggable = !queued && !isPaused && !isEditing && !isArchived; // Disable drag during edit or if archived
// Check if this card can be edited inline
@@ -502,7 +503,7 @@ function TaskCardComponent({
setShowSteps((current) => !current);
}, []);
const cardClass = `card${dragging ? " dragging" : ""}${queued ? " queued" : ""}${isAgentActive ? " agent-active" : ""}${isFailed ? " failed" : ""}${isPaused ? " paused" : ""}${fileDragOver ? " file-drop-target" : ""}${isEditing ? " card-editing" : ""}${isSaving ? " card-saving" : ""}`;
const cardClass = `card${dragging ? " dragging" : ""}${queued ? " queued" : ""}${isAgentActive ? " agent-active" : ""}${isFailed ? " failed" : ""}${isPaused ? " paused" : ""}${isAwaitingApproval ? " awaiting-approval" : ""}${fileDragOver ? " file-drop-target" : ""}${isEditing ? " card-editing" : ""}${isSaving ? " card-saving" : ""}`;
if (isEditing) {
return (
@@ -564,13 +565,15 @@ function TaskCardComponent({
)}
{!isPaused && task.status && task.status !== "queued" && (
<span
className={`card-status-badge${ACTIVE_STATUSES.has(task.status) ? " pulsing" : ""}${isFailed ? " failed" : ""}`}
style={isFailed
? { background: "rgba(218,54,51,0.15)", color: "#da3633" }
: { background: COLUMN_COLOR_MAP[task.column], color: COLUMN_TEXT_COLOR_MAP[task.column] }
className={`card-status-badge${isAwaitingApproval ? " awaiting-approval" : ""}${ACTIVE_STATUSES.has(task.status) ? " pulsing" : ""}${isFailed ? " failed" : ""}`}
style={isAwaitingApproval
? { background: "rgba(210,153,34,0.2)", color: "var(--triage)" }
: isFailed
? { background: "rgba(218,54,51,0.15)", color: "#da3633" }
: { background: COLUMN_COLOR_MAP[task.column], color: COLUMN_TEXT_COLOR_MAP[task.column] }
}
>
{task.status}
{isAwaitingApproval ? "Awaiting Approval" : task.status}
</span>
)}
{hasGitHubBadge && (

View File

@@ -2907,3 +2907,152 @@ describe("TaskCard title display", () => {
expect(cardTitle).toBeDefined();
});
});
/**
* Tests for awaiting-approval visual state in TaskCard.
* Tasks in triage with status "awaiting-approval" receive a distinct
* highlight and approval-specific badge text on the board.
*/
describe("TaskCard awaiting-approval state", () => {
const noopToast = vi.fn();
beforeEach(() => {
vi.clearAllMocks();
});
it("applies awaiting-approval class when task is in triage with awaiting-approval status", () => {
const task = makeTask({ column: "triage", status: "awaiting-approval" });
render(
<TaskCard
task={task}
onOpenDetail={vi.fn()}
addToast={noopToast}
/>
);
const card = document.querySelector('[data-id="FN-099"]');
expect(card?.classList.contains("awaiting-approval")).toBe(true);
});
it("does NOT apply awaiting-approval class for triage tasks with other statuses", () => {
const task = makeTask({ column: "triage", status: "specifying" });
render(
<TaskCard
task={task}
onOpenDetail={vi.fn()}
addToast={noopToast}
/>
);
const card = document.querySelector('[data-id="FN-099"]');
expect(card?.classList.contains("awaiting-approval")).toBe(false);
});
it("does NOT apply awaiting-approval class for non-triage columns", () => {
const columns: Column[] = ["todo", "in-progress", "in-review", "done"];
for (const column of columns) {
const task = makeTask({ column, status: "awaiting-approval" });
const { unmount } = render(
<TaskCard
task={task}
onOpenDetail={vi.fn()}
addToast={noopToast}
/>
);
const card = document.querySelector('[data-id="FN-099"]');
expect(card?.classList.contains("awaiting-approval")).toBe(false);
unmount();
}
});
it("shows 'Awaiting Approval' badge text for awaiting-approval tasks", () => {
const task = makeTask({ column: "triage", status: "awaiting-approval" });
render(
<TaskCard
task={task}
onOpenDetail={vi.fn()}
addToast={noopToast}
/>
);
const badge = screen.getByText("Awaiting Approval");
expect(badge).toBeDefined();
expect(badge.classList.contains("card-status-badge")).toBe(true);
expect(badge.classList.contains("awaiting-approval")).toBe(true);
});
it("shows raw status text for other triage statuses", () => {
const task = makeTask({ column: "triage", status: "specifying" });
render(
<TaskCard
task={task}
onOpenDetail={vi.fn()}
addToast={noopToast}
/>
);
// Should show raw status text, not "Awaiting Approval"
expect(screen.getByText("specifying")).toBeDefined();
expect(screen.queryByText("Awaiting Approval")).toBeNull();
});
it("does NOT apply agent-active class for awaiting-approval tasks", () => {
const task = makeTask({ column: "triage", status: "awaiting-approval" });
render(
<TaskCard
task={task}
onOpenDetail={vi.fn()}
addToast={noopToast}
/>
);
const card = document.querySelector('[data-id="FN-099"]');
expect(card?.classList.contains("agent-active")).toBe(false);
expect(card?.classList.contains("awaiting-approval")).toBe(true);
});
it("awaiting-approval badge uses triage color styling", () => {
const task = makeTask({ column: "triage", status: "awaiting-approval" });
render(
<TaskCard
task={task}
onOpenDetail={vi.fn()}
addToast={noopToast}
/>
);
const badge = screen.getByText("Awaiting Approval") as HTMLElement;
// The badge should use the triage amber color
expect(badge.style.color).toBe("var(--triage)");
// JSDOM may normalize rgba() with or without spaces, so normalize for comparison
expect(badge.style.background.replace(/\s+/g, "")).toBe("rgba(210,153,34,0.2)");
});
it("awaiting-approval card does NOT look like other states (no agent-active, failed, or paused)", () => {
const task = makeTask({ column: "triage", status: "awaiting-approval" });
render(
<TaskCard
task={task}
onOpenDetail={vi.fn()}
addToast={noopToast}
/>
);
const card = document.querySelector('[data-id="FN-099"]');
expect(card?.classList.contains("awaiting-approval")).toBe(true);
expect(card?.classList.contains("agent-active")).toBe(false);
expect(card?.classList.contains("failed")).toBe(false);
expect(card?.classList.contains("paused")).toBe(false);
});
});

View File

@@ -967,6 +967,24 @@ body {
word-break: break-word;
}
/* Awaiting manual plan approval: amber left border + subtle pulsing background to signal human review needed.
Distinct from agent-active (purple glow), failed (red left border + error block), and paused (dimmed opacity). */
.card.awaiting-approval {
border-left: 3px solid var(--triage);
background: rgba(210, 153, 34, 0.06);
animation: approval-pulse 3s ease-in-out infinite;
}
@keyframes approval-pulse {
0%,
100% {
background: rgba(210, 153, 34, 0.06);
}
50% {
background: rgba(210, 153, 34, 0.12);
}
}
.card.paused {
opacity: 0.55;
border-left: 3px solid var(--text-secondary, #888);