diff --git a/src/components/admin/review-row.tsx b/src/components/admin/review-row.tsx
index 0da9f62..155e131 100644
--- a/src/components/admin/review-row.tsx
+++ b/src/components/admin/review-row.tsx
@@ -644,6 +644,10 @@ export function ReviewRow({
// time (decided_at is unset until approve/dismiss).
const rowTimestamp = review.decided_at ?? review.created_at
const relativeTime = formatDateRelative(rowTimestamp, rowTimestamp ?? "")
+ // Who made the decision ("admin" from the UI, "stakwork" from workflows).
+ // Trim guards legacy reviews decided before decided_by was reliable ("").
+ const decider =
+ review.status !== "pending" ? review.decided_by?.trim() : undefined
const direction = useMemo(
() => extractDirection(review.action_name, review.action_payload),
@@ -851,6 +855,12 @@ export function ReviewRow({
{review.run_ref_id && Run #{review.run_ref_id.slice(-5)}}
{review.run_ref_id && ·}
{relativeTime}
+ {decider && (
+ <>
+ ·
+ by {decider}
+ >
+ )}
e.stopPropagation()}>
diff --git a/src/lib/__tests__/reviews.test.tsx b/src/lib/__tests__/reviews.test.tsx
index 730534d..e936dd6 100644
--- a/src/lib/__tests__/reviews.test.tsx
+++ b/src/lib/__tests__/reviews.test.tsx
@@ -139,6 +139,41 @@ describe("ReviewRow", () => {
expect(badge!.className).toContain("bg-red-500")
})
+ // ── Decider attribution ─────────────────────────────────────────────────────
+
+ it("shows who decided on decided reviews", () => {
+ const { container } = render(
+
+ )
+ const decider = container.querySelector("[data-testid='review-decider']")
+ expect(decider).toBeTruthy()
+ expect(decider!.textContent).toBe("by stakwork")
+ })
+
+ it("hides decider on pending reviews and on legacy blank decided_by", () => {
+ const pending = render(
+
+ )
+ expect(pending.container.querySelector("[data-testid='review-decider']")).toBeNull()
+
+ const legacy = render(
+
+ )
+ expect(legacy.container.querySelector("[data-testid='review-decider']")).toBeNull()
+ })
+
// ── Approve / Dismiss only for pending ─────────────────────────────────────
it("shows Approve and Dismiss buttons only for pending rows", () => {
diff --git a/src/lib/graph-api.ts b/src/lib/graph-api.ts
index 3e3154f..a0f2a46 100644
--- a/src/lib/graph-api.ts
+++ b/src/lib/graph-api.ts
@@ -1178,7 +1178,12 @@ export async function approveReview(
// cancel_active_runs: an admin deciding here makes any in-flight
// human-review workflow moot, so ask the backend to stop it. Workflows
// deciding their own reviews omit this — cancelling would kill their run.
- const body: Record = { cancel_active_runs: true }
+ // decided_by distinguishes admin-UI decisions from workflow ones on the
+ // review node (workflows send their own identity, e.g. "stakwork").
+ const body: Record = {
+ cancel_active_runs: true,
+ decided_by: "admin",
+ }
if (overridePayload) body.override_payload = overridePayload
return api.post(
`/v2/reviews/${refId}/approve`,
@@ -1204,10 +1209,11 @@ export async function dismissReview(
}
return { status: "Success", review }
}
- // cancel_active_runs: same admin-decided semantics as approveReview above.
+ // cancel_active_runs + decided_by: same admin-decided semantics as
+ // approveReview above.
return api.post(
`/v2/reviews/${refId}/dismiss`,
- { reason, cancel_active_runs: true },
+ { reason, cancel_active_runs: true, decided_by: "admin" },
undefined,
signal
)