diff --git a/apps/web/src/app.css b/apps/web/src/app.css index 37a6dd404..1375f8afa 100644 --- a/apps/web/src/app.css +++ b/apps/web/src/app.css @@ -1733,7 +1733,6 @@ tr.insights-row-clickable:hover { justify-content: flex-end; gap: 0.4rem; } -.workbench-info-approval-list, .workbench-info-schedule-list { display: flex; flex-direction: column; @@ -1747,11 +1746,6 @@ tr.insights-row-clickable:hover { color: var(--muted-foreground); text-decoration: line-through; } -.workbench-info-approval-row { - display: flex; - flex-direction: column; - gap: 0.4rem; -} /* A message's attachments: a package gets a Deploy card, anything else a quiet file list. */ diff --git a/apps/web/src/approval-actions.ts b/apps/web/src/approval-actions.ts index b6b069ef3..190cef190 100644 --- a/apps/web/src/approval-actions.ts +++ b/apps/web/src/approval-actions.ts @@ -10,6 +10,7 @@ import { approveApproval, rejectApproval } from "./api"; import type { Approval } from "./api"; import { getApprovalDetail } from "./pending-approvals"; import { tenantKeys } from "./query-client"; +import { markApprovalAnswered } from "./worker-status"; export function createChatApprovalActions( tenantId: string, @@ -35,6 +36,7 @@ export function createChatApprovalActions( // A 409 (already resolved, run no longer running) is kept distinct from // a generic error: never something a retry fixes. async function resolve( + approvalId: string, call: () => Promise, status: "approved" | "rejected", forbiddenMessage: string, @@ -44,6 +46,7 @@ export function createChatApprovalActions( // The native route only ever returns 200 with the exact terminal // status asked for, so `status` here is not a guess. await call(); + markApprovalAnswered(queryClient, tenantId, approvalId); invalidate(); return { kind: "resolved", status }; } catch (cause) { @@ -94,6 +97,7 @@ export function createChatApprovalActions( }, approve(approvalId) { return resolve( + approvalId, () => approveApproval(tenantId, approvalId), "approved", CHAT_STRINGS.blockApproveActionForbidden, @@ -102,6 +106,7 @@ export function createChatApprovalActions( }, allowStanding(approvalId) { return resolve( + approvalId, () => approveApproval(tenantId, approvalId, "always"), "approved", CHAT_STRINGS.blockApproveActionForbidden, @@ -110,6 +115,7 @@ export function createChatApprovalActions( }, reject(approvalId) { return resolve( + approvalId, () => rejectApproval(tenantId, approvalId), "rejected", CHAT_STRINGS.blockDenyActionForbidden, diff --git a/apps/web/src/chat/approval-row.tsx b/apps/web/src/bench/approval-row.tsx similarity index 54% rename from apps/web/src/chat/approval-row.tsx rename to apps/web/src/bench/approval-row.tsx index 5562b4967..f2061dd80 100644 --- a/apps/web/src/chat/approval-row.tsx +++ b/apps/web/src/bench/approval-row.tsx @@ -1,14 +1,22 @@ -// One pending approval with its approve/deny buttons. Shared by the -// workbench workbench's info column and the chat transcript, so a person can -// answer an agent's ask wherever they are looking when it lands. +// One pending approval in the bench drawer's "Needs you" section: who is +// asking, the exact call it is gating, and the approve/deny buttons that +// unpark the run. import { Button, formatRelativeTime } from "@corbits/react-ui"; import { toast } from "@corbits/react-ui/ui/toast"; import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { ApiQueryError } from "@/lib/api-query"; import { approveApproval, rejectApproval } from "../api"; +import { IdentityAvatar } from "../chat/avatar"; import type { PendingApproval } from "../pending-approvals"; import { tenantKeys } from "../query-client"; +import { markApprovalAnswered } from "../worker-status"; + +// The hub answers 409 when the ask was already resolved or the run that +// raised it has stopped (e.g. a hub restart); either way no retry helps. +const STALE_APPROVAL = + "This request can't be answered anymore: it was already resolved or its worker stopped."; export function ApprovalRow({ item, @@ -22,11 +30,16 @@ export function ApprovalRow({ mutationFn: (action: "approve" | "deny") => action === "approve" ? approveApproval(tenantId, item.id) : rejectApproval(tenantId, item.id), onSuccess: () => { + markApprovalAnswered(queryClient, tenantId, item.id); void queryClient.invalidateQueries({ queryKey: tenantKeys.pendingApprovals(tenantId), }); }, onError: (cause, action) => { + if (cause instanceof ApiQueryError && cause.status === 409) { + toast(STALE_APPROVAL); + return; + } toast( cause instanceof Error ? cause.message @@ -37,25 +50,23 @@ export function ApprovalRow({ const pending = resolveMutation.isPending ? resolveMutation.variables : null; return ( -
  • -
    - {item.headline} -
    - {item.toolName !== undefined && ( - <> - {/* Arguments are untrusted agent output: rendered as plain text - * nodes only, never as markup. */} - - {item.toolName} - {item.argumentsSummary !== undefined ? `: ${item.argumentsSummary}` : ""} - -
    - - )} - - {item.agentName} · {formatRelativeTime(item.createdAt)} +
    +
    + + + {item.headline} + + {item.agentName} + {item.toolName === undefined ? "" : ` · ${item.toolName}`} ·{" "} + {formatRelativeTime(item.createdAt)} +
    + {/* Arguments are untrusted agent output: rendered as a plain text node + * only, never as markup. */} + {item.argumentsSummary !== undefined ? ( + {item.argumentsSummary} + ) : null}
    -
  • + ); } diff --git a/apps/web/src/bench/bench-pill.tsx b/apps/web/src/bench/bench-pill.tsx index da4982391..191032304 100644 --- a/apps/web/src/bench/bench-pill.tsx +++ b/apps/web/src/bench/bench-pill.tsx @@ -23,6 +23,7 @@ export function BenchPill({ readonly onToggle: () => void; }) { const working = status.tone === "working"; + const needsYou = status.text === "Needs you"; return (