From ac5a85cfa5ccd23172e1e1845b674f4f1e65f1f4 Mon Sep 17 00:00:00 2001 From: Rassl Date: Tue, 28 Jul 2026 10:11:15 +0700 Subject: [PATCH] feat: human review fix --- src/components/admin/review-row.tsx | 26 ++++++++++++++++-------- src/lib/hooks/use-stakwork-run-status.ts | 26 ++++++++++++++++++++++-- 2 files changed, 42 insertions(+), 10 deletions(-) diff --git a/src/components/admin/review-row.tsx b/src/components/admin/review-row.tsx index fe5c79e..fefe769 100644 --- a/src/components/admin/review-row.tsx +++ b/src/components/admin/review-row.tsx @@ -187,7 +187,7 @@ function InlineChip({ return ( @@ -407,7 +407,7 @@ function ConfirmActionPopover({ if (!disabled) setOpen((v) => !v) }} className={cn( - "rounded border px-2 py-0.5 text-[11px] font-medium transition-all text-center", + "shrink-0 whitespace-nowrap rounded border px-2 py-0.5 text-[11px] font-medium transition-all text-center", minWidthClass, disabled ? "cursor-not-allowed border-border/40 bg-transparent text-muted-foreground/40" @@ -568,7 +568,17 @@ export function ReviewRow({ triggering: humanReviewTriggering, } = useStakworkRunStatus( isMergeReviewEligible ? review.ref_id : "__noop__", - "node_merge_review" + "node_merge_review", + { + // The workflow reports back through the ingest webhook, which turns its + // recommendations into new Reviews. Without this refresh they sit unseen + // until the operator reloads the page. + onCompleted: () => { + onRefresh() + onCountRefresh?.() + }, + onTriggerError: setInlineError, + } ) // ── Merge-specific interactive state ──────────────────────────────────────── @@ -688,7 +698,7 @@ export function ReviewRow({ setExpanded((v) => !v) } }} - className="grid w-full cursor-pointer grid-cols-[22px_20px_16px_1fr_auto_170px] items-center gap-3 px-3 py-2 text-left transition-colors hover:bg-muted/20" + className="grid w-full cursor-pointer grid-cols-[22px_20px_16px_minmax(0,1fr)_auto_auto] items-center gap-3 px-3 py-2 text-left transition-colors hover:bg-muted/20" > {selectable && onSelectChange ? ( @@ -730,15 +740,15 @@ export function ReviewRow({ )} -
+
{review.run_ref_id && Run #{review.run_ref_id.slice(-5)}} {review.run_ref_id && ·} {relativeTime}
-
e.stopPropagation()}> +
e.stopPropagation()}> {isPending && isAdmin ? ( -
+
void + /** + * Called when the trigger request itself fails, with the server's message. + * Without this the reason is swallowed and the user only sees a red retry + * button — the backend returns real causes (INVALID_PAYLOAD when a node in + * the merge no longer exists, DISPATCH_FAILED when Stakwork rejects it). + */ + onTriggerError?: (message: string) => void /** Poll interval in milliseconds (default 5 000). */ pollIntervalMs?: number } +/** Pull a human-readable message out of a thrown fetch Response. */ +async function errorMessage(err: unknown, fallback: string): Promise { + if (!(err instanceof Response)) return fallback + const body = (await err.json().catch(() => ({}))) as { + error?: string + errorCode?: string + } + return body.error || body.errorCode || fallback +} + export interface UseStakworkRunStatusResult { /** Current mapped run status, or null if no run exists / not yet hydrated. */ status: StakworkRunStatus @@ -74,7 +91,7 @@ export function useStakworkRunStatus( jobType: string, options: UseStakworkRunStatusOptions = {} ): UseStakworkRunStatusResult { - const { onCompleted, onPoll, pollIntervalMs = 5000 } = options + const { onCompleted, onPoll, onTriggerError, pollIntervalMs = 5000 } = options const [status, setStatus] = useState(null) const [loading, setLoading] = useState(false) @@ -85,6 +102,8 @@ export function useStakworkRunStatus( onCompletedRef.current = onCompleted const onPollRef = useRef(onPoll) onPollRef.current = onPoll + const onTriggerErrorRef = useRef(onTriggerError) + onTriggerErrorRef.current = onTriggerError function stopPoll() { if (pollRef.current) { @@ -152,8 +171,11 @@ export function useStakworkRunStatus( try { await triggerFn() startPoll(refId) - } catch { + } catch (err) { setStatus("FAILED") + if (onTriggerErrorRef.current) { + onTriggerErrorRef.current(await errorMessage(err, "Request failed")) + } } finally { setTriggering(false) }