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
26 changes: 18 additions & 8 deletions src/components/admin/review-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ function InlineChip({
return (
<span
className={cn(
"inline-flex max-w-[220px] shrink-0 items-center gap-1.5 truncate rounded px-1.5 py-0.5 text-[12px]",
"inline-flex min-w-0 max-w-[220px] items-center gap-1.5 truncate rounded px-1.5 py-0.5 text-[12px]",
emphasis ? "font-semibold text-foreground" : "text-muted-foreground/90"
)}
>
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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 ────────────────────────────────────────
Expand Down Expand Up @@ -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 ? (
<span title={selectionLocked && !selected ? selectionLockedReason : undefined}>
Expand Down Expand Up @@ -730,15 +740,15 @@ export function ReviewRow({
</span>
)}

<div className="flex shrink-0 items-center gap-2 font-mono text-[10px] text-muted-foreground">
<div className="flex shrink-0 items-center gap-2 whitespace-nowrap font-mono text-[10px] text-muted-foreground">
{review.run_ref_id && <span>Run #{review.run_ref_id.slice(-5)}</span>}
{review.run_ref_id && <span>·</span>}
<span>{relativeTime}</span>
</div>

<div className="flex justify-end" onClick={(e) => e.stopPropagation()}>
<div className="flex shrink-0 justify-end" onClick={(e) => e.stopPropagation()}>
{isPending && isAdmin ? (
<div className="flex gap-1">
<div className="flex shrink-0 items-center gap-1">
<ConfirmActionPopover
tone="approve"
triggerLabel={labels.approve}
Expand Down Expand Up @@ -782,7 +792,7 @@ export function ReviewRow({
: "Send for human review"
}
className={cn(
"inline-flex items-center gap-1 rounded border px-2 py-0.5 text-[11px] font-medium transition-all",
"inline-flex shrink-0 items-center gap-1 whitespace-nowrap rounded border px-2 py-0.5 text-[11px] font-medium transition-all",
humanReviewInFlight || humanReviewTriggering
? "cursor-not-allowed border-sky-500/40 bg-sky-500/10 text-sky-300"
: humanReviewStatus === "COMPLETED"
Expand Down
26 changes: 24 additions & 2 deletions src/lib/hooks/use-stakwork-run-status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,27 @@ export interface UseStakworkRunStatusOptions {
* useful for capturing metadata like `project_id` from the run.
*/
onPoll?: (run: import("@/lib/graph-api").StakworkRun) => 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<string> {
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
Expand Down Expand Up @@ -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<StakworkRunStatus>(null)
const [loading, setLoading] = useState(false)
Expand All @@ -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) {
Expand Down Expand Up @@ -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)
}
Expand Down
Loading