Skip to content

tasks: a blocked card names its approvals but cannot show or decide them — the board never got #1002's fix #1891

Closed

Description

@sanil-23

A paused board card says it is waiting on approvals. It cannot say what they are, and it cannot decide them.

taskApprovalBlock (frontend/src/lib/task-approvals.ts) already hands the card the full ApprovalSummary[] — payload, amount_usd, group, agent, expires_at_millis. BlockedRow (frontend/src/views/TaskCard.tsx) reads count and at_millis and drops the rest:

Fetch a web page
Waiting for your approval · 4m ago          Review

The detail screen is the same shape one line long — AwaitingApprovalRow (frontend/src/views/TaskDetailView.tsx) prints Waiting on your approval — fetch a web page and a link.

This is the complaint #1002 already answered one surface over. The run drawer told an operator their run had parked and gave them nowhere to act, so deciding meant leaving the run, finding the row in a flat queue, and coming back. run-approvals.ts + an inline ApprovalRow fixed it there. The task board is the blocker surface that never got the same treatment, and it is the one an operator actually works from.

What is dropped

Three fields, and each is a distinct failure:

  1. The payload lead. Two cards blocked on http_request read identically, so the card cannot distinguish a GET of a docs page from a DELETE against production. payloadLeadLabel is the function chat uses for exactly this and is already imported one directory over.
  2. group. The consequence chip — spend / send / sign / publish — is what tells an operator whether this is worth interrupting themselves for. The card renders no signal at all, so a payment.send and a file read are the same pixels.
  3. expires_at_millis. The card counts up from the park (waiting 4m) and never counts down to the deadline. An approval that default-denies on [policy].approval_ttl_hours gives the board no warning before it does — the operator's first news of the expiry is the work not having happened. Every other approval surface paints this; the board does not.

Proposed fix

Do not re-implement deciding. ApprovalRow (frontend/src/views/chat/ApprovalRow.tsx) owns it: batch all-or-nothing over pending, per-id resolve so each approved call still mints its own revocable grant (#739), detached POST (#391), the busy / failed / partially-settled states, role redaction (#618), and the needsFullReview rule that pulls Approve when the payload lead is truncated. A hand-rolled row on the card would lose the last two silently.

  1. A third layout on ApprovalRow, not a fork. A board column is w-65 — roughly 220px of card content — so CompactApprovalRow's horizontal flex does not fit. variant="card" is the same content stacked: action + lead, then consequence chip and deadline, then Approve/Decline as a full-width pair. Plus a detailsHref prop, since the two hardcoded #/approvals links inside it should address #/approvals/<taskId> from here. Defaults leave chat byte-identical.
  2. taskApprovalRows() in lib/task-approvals.ts, mirroring runApprovals(): the live queue joined with the shell's witnessed decided map, so a row settles in place across the gap between the resolve's answer and the feed's next poll rather than blinking out.
  3. Thread the decide bundle the way WorkflowsView already receives it in app-shell.tsx — decidingApprovals / decidedApprovals / failedApprovals / onDecideApproval — through LedgersView → BoardMode → TaskItem, and through TaskDetailRoute → TaskDetailView (which already receives parked).
  4. The detail screen gets the full card, one ApprovalRow per approval as RunApprovalsSection does. There is room there for the payload preview and the grant-scope control.

Constraints this must not break

Tests

Extending frontend/test/unit/task-blocked-card.test.ts and task-approvals.test.ts: the card names the payload lead rather than the kind; the chip and the countdown render; Approve fires onDecide once per id; a truncated lead swaps Approve for the details link; a witnessed decision settles the row in place; the detail screen's residual line appears when the queue has not delivered every pending approval.

Activity

  1. added
    enhancementNew feature or request
    cluster:approvalsHuman-in-the-loop boundary, policy, and approval surfaces
    priority: p2Soon. Real but survivable — a rough edge, a gap, a thing that will bite later.
    cluster:tasksWork board, task detail, and the task-execution surface
    on Aug 27, 2026
  2. sanil-23 commented on Aug 27, 2026

    @sanil-23
    CollaboratorAuthor

    Follow-up left open by #1895, recorded here rather than in a review thread.

    Resume on a paused card is disabled while the approvals queue still holds anything for it, which covers the sharp window: the resolve detaches (#391), so its answer comes back before the follow-up cycle the verdict released has run, and the queue still carries the decided row across that gap.

    It does not cover the window after that. Once the host drops the rows, the continuation can still be running while the card reads paused, and Resume is live again — pressing it re-dispatches from the top, duplicating work already in flight and parking the same effects again, which is the loop #883 exists to break.

    The console cannot close this on its own. The board reads …/tasks, whose card projection carries no approvals — that is why #883 joined the approvals feed for the blocked row in the first place — so there is nothing on the card that distinguishes "paused, waiting for a person" from "paused, continuation in flight". More client-side inference would mean holding a per-card memory of "I decided something here recently" with no event to expire it against, which trades a real hazard for a stale one.

    What would close it is a host-side signal: the card knowing a continuation is running for it, the way InflightRun already tells the detail screen an attempt is live. Then Resume gates on that and the guesswork goes away.

    Not new with #1895 — an operator who approved on the Approvals page could always come back to an enabled Resume on a still-paused card. But that PR puts Approve on the card itself, so the operator is now standing right next to the button when the window opens, which is what makes it worth its own issue.

  3. self-assigned this
    on Aug 28, 2026
  4. added 2 commits that reference this issue on Aug 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

cluster:approvalsHuman-in-the-loop boundary, policy, and approval surfacescluster:tasksWork board, task detail, and the task-execution surfaceenhancementNew feature or requestpriority: p2Soon. Real but survivable — a rough edge, a gap, a thing that will bite later.

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions