Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/components/admin/review-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -851,6 +855,12 @@ export function ReviewRow({
{review.run_ref_id && <span>Run #{review.run_ref_id.slice(-5)}</span>}
{review.run_ref_id && <span>·</span>}
<span>{relativeTime}</span>
{decider && (
<>
<span>·</span>
<span data-testid="review-decider">by {decider}</span>
</>
)}
</div>

<div className="flex shrink-0 justify-end" onClick={(e) => e.stopPropagation()}>
Expand Down
35 changes: 35 additions & 0 deletions src/lib/__tests__/reviews.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<ReviewRow
schemas={[]}
review={makeReview({ status: "dismissed", decided_by: "stakwork" })}
onRefresh={noop}
/>
)
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(
<ReviewRow
schemas={[]}
review={makeReview({ status: "pending", decided_by: "admin" })}
onRefresh={noop}
/>
)
expect(pending.container.querySelector("[data-testid='review-decider']")).toBeNull()

const legacy = render(
<ReviewRow
schemas={[]}
review={makeReview({ status: "dismissed", decided_by: "" })}
onRefresh={noop}
/>
)
expect(legacy.container.querySelector("[data-testid='review-decider']")).toBeNull()
})

// ── Approve / Dismiss only for pending ─────────────────────────────────────

it("shows Approve and Dismiss buttons only for pending rows", () => {
Expand Down
12 changes: 9 additions & 3 deletions src/lib/graph-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown> = { 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<string, unknown> = {
cancel_active_runs: true,
decided_by: "admin",
}
if (overridePayload) body.override_payload = overridePayload
return api.post<ReviewDecisionResponse>(
`/v2/reviews/${refId}/approve`,
Expand All @@ -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<ReviewDecisionResponse>(
`/v2/reviews/${refId}/dismiss`,
{ reason, cancel_active_runs: true },
{ reason, cancel_active_runs: true, decided_by: "admin" },
undefined,
signal
)
Expand Down
Loading