diff --git a/panel/src/components/dashboard/__tests__/playbook-review-queue.test.tsx b/panel/src/components/dashboard/__tests__/playbook-review-queue.test.tsx
new file mode 100644
index 00000000..6cadec87
--- /dev/null
+++ b/panel/src/components/dashboard/__tests__/playbook-review-queue.test.tsx
@@ -0,0 +1,96 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
+import { fireEvent, render, screen, waitFor } from "@testing-library/react";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import type { ReactNode } from "react";
+import type { Playbook } from "@/lib/api/playbooks";
+
+const { resolveApproveRef } = vi.hoisted(() => ({
+ resolveApproveRef: { current: null as null | ((v: unknown) => void) },
+}));
+
+const { listDrafts, approve, reject } = vi.hoisted(() => ({
+ listDrafts: vi.fn(
+ async () =>
+ [
+ {
+ id: "pb-1",
+ title: "Recover a stuck claim lock",
+ slug: "recover-claim-lock",
+ problem: "an agent's claim TOCTOU wedges the task",
+ procedure: "1. ...",
+ tags: ["backend"],
+ team: "backend",
+ scope: "cell",
+ status: "draft",
+ },
+ {
+ id: "pb-2",
+ title: "Rebase a behind-base branch",
+ slug: "rebase-behind-base",
+ problem: "the dev's branch fell behind master",
+ procedure: "1. ...",
+ tags: ["git"],
+ team: "backend",
+ scope: "cell",
+ status: "draft",
+ },
+ ] as Playbook[],
+ ),
+ // Deferred so the test can freeze the approve mid-flight.
+ approve: vi.fn(
+ () =>
+ new Promise((r) => {
+ resolveApproveRef.current = r as (v: unknown) => void;
+ }),
+ ),
+ reject: vi.fn(async () => ({})),
+}));
+
+vi.mock("@/lib/api", () => ({ playbooksApi: { listDrafts, approve, reject } }));
+
+import { PlaybookReviewQueue } from "../playbook-review-queue";
+
+function withQueryClient(ui: ReactNode) {
+ const client = new QueryClient({
+ defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
+ });
+ return {ui};
+}
+
+describe("PlaybookReviewQueue — per-row disable during an approve (F084)", () => {
+ beforeEach(() => {
+ listDrafts.mockClear();
+ approve.mockClear();
+ reject.mockClear();
+ resolveApproveRef.current = null;
+ });
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it("disables only the playbook being approved, not every row's Approve", async () => {
+ render(withQueryClient());
+
+ const approveButtons = await screen.findAllByRole("button", {
+ name: "Approve",
+ });
+ expect(approveButtons).toHaveLength(2);
+ expect(approveButtons[0]).not.toBeDisabled();
+ expect(approveButtons[1]).not.toBeDisabled();
+
+ // Approve the first playbook — the mutation stays pending (deferred fn).
+ fireEvent.click(approveButtons[0]);
+ await waitFor(() => expect(approve).toHaveBeenCalledWith("pb-1"));
+
+ // Row 1's Approve locks while its approve is in flight; row 2's Approve
+ // stays usable so the reviewer can act on an independent playbook at the
+ // same time. Before the fix every row shared `disabled={approveMutation.isPending}`.
+ await waitFor(() => expect(approveButtons[0]).toBeDisabled());
+ expect(approveButtons[1]).not.toBeDisabled();
+
+ // Mutation resolves → row 1's Approve unlocks again.
+ resolveApproveRef.current?.(undefined);
+ await waitFor(() => expect(approveButtons[0]).not.toBeDisabled());
+ expect(approveButtons[1]).not.toBeDisabled();
+ });
+});
diff --git a/panel/src/components/dashboard/playbook-review-queue.tsx b/panel/src/components/dashboard/playbook-review-queue.tsx
index bd7d4570..1ad87ec2 100644
--- a/panel/src/components/dashboard/playbook-review-queue.tsx
+++ b/panel/src/components/dashboard/playbook-review-queue.tsx
@@ -132,7 +132,10 @@ export function PlaybookReviewQueue({ className }: { className?: string }) {