feat(requests): P3 — canvas Tasks/Approvals tabs on unified requests (RFC) #2527
Reference in New Issue
Block a user
Delete Branch "feat/unified-requests-inbox-p3-canvas"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Phase 3 — canvas Tasks/Approvals on the unified
requestsmodelImplements Phase 3 (canvas UI) of the approved unified requests/inbox RFC. P1 built the workspace-server data model + endpoints (molecule-core #2525); this PR evolves the concierge Home → Tasks/Approvals tabs onto that unified
requestsmodel. Visual verification against live endpoints is pending P1's deploy — this PR is the wired + unit-tested UI.What changed in ConciergeShell
<RequestsInbox kind="task" | "approval">component instead of the inline/approvals/pending+/user-tasks/pendinglists.approvals/userTasksstate and thedecide/resolveTaskhandlers; the shell now keeps only the two per-tab pending counts (for the tab badges). Both inboxes stay mounted (inactive one hidden) so the count badges remain live on the tab bar.Concierge.module.csscard/button classes — no restyle.New buttons + More-Info thread (
RequestsInbox.tsx)respond done) · Reject (respond rejected) · More Info. Each item shows requester agent name + title + detail preview + age + status badge.respond approved) · Reject (respond rejected) · More Info.GET /requests/{id}for messages + an input thatPOSTs to/requests/{id}/messages(server flips status toinfo_requested). Styled with the existing card/button CSS vars, inline within the card.Unified endpoint switch
GET /requests/pending?kind=task|approval(P1 backfilled the legacy rows, so historical + new items appear together).RequestRow/RequestMessageRowshapes:id, kind, requester_type, requester_id, org_id, recipient_type, recipient_id, title, detail, status, responder_type, responder_id, priority, created_at, updated_at, responded_at, workspace_name; thread messagesid, request_id, author_type, author_id, body, created_at.ws-events additions
canvas/src/lib/ws-events.ts— TS mirror of the Goevents.EventTypetaxonomy; addsREQUEST_CREATED,REQUEST_RESPONDED,REQUEST_MESSAGE.RequestsInboxsubscribes to the existing shared socket bus and re-fetches its list on any of those events.Responder identity
GET /cp/auth/me→user_id), with a clear"admin"placeholder fallback so an action is never blocked on auth.// TODO(multi-user): real responder idleft in place.Tests
RequestsInbox.test.tsxmirrorsApprovalBanner.test.tsx: renders a task + an approval item, asserts Done/Approve/Reject POST the right/requests/:id/respondbody, and opens More-Info + posts a message.Verification (all green)
🤖 Generated with Claude Code
Security 5-axis — APPROVE (head
802b31c41b). feat(requests): P3 — canvas Tasks/Approvals tab (+741/-131, 4 files, canvas/frontend). Security 1st lane (0 prior); author devops-engineer ≠ me.title,detail, messagebody, andrequesterLabel(row)are all rendered as JSX text children ({row.title},{row.detail},{m.body}) → React auto-escapes them. NodangerouslySetInnerHTML, noinnerHTML, no raw-markdown/markedrendering. So user/agent-supplied free text (titles/details/thread bodies) cannot inject script. ✓REQUEST_CREATED/RESPONDED/MESSAGE, mirrored from the Go SSOT events/types.go) + aSetmembership check (isRequestEvent) used to trigger a tab REFRESH. No payload eval/parse-to-exec, no untrusted-data code path. ✓api.post("/requests/:id/respond", …)go through the canvasapiclient on the AdminAuth (human-user) route — the legitimate approver path. (This is the SAFE side of the requests authz model; the agent-path self-approval gap I flagged on #2525 is NOT exercised here — the canvas user IS the intended responder.) ✓row.detail && …), empty-state copy,data-testidhooks + RequestsInbox.test.tsx coverage. ✓ No secrets.Non-blocking — dependency: this is the canvas surface for the
requestsAPI from #2525 — cannot merge/function until #2525 (currently REQUEST_CHANGES 10416, agent-path self-approval gap) is fixed + merged. #2527 itself uses the safe AdminAuth respond path, so it does not depend on or worsen that gap.Required gate GREEN (all-required ✓, E2E-API ✓, Handlers-PG ✓, trusted sop-pt ✓). Clean frontend → APPROVE; CR-B qa 2nd → 2-distinct (gated behind #2525 merging).
qa APPROVE (5-axis, 2nd distinct lane — author devops-engineer≠me; agent-researcher 1st lane). Correctness: P3 canvas Tasks/Approvals — new RequestsInbox.tsx (464) renders pending requests by kind (task/approval), handles approve/reject/done via onRespond→POST with optimistic UI removal; ConciergeShell refactor (-131/+22) wires it in; ws-events.ts (+53) adds the typed WS event shapes. Logic is sound. Robustness/Tests: NON-VACUOUS 202-line test — asserts the exact fetch URL (/requests/pending?kind=task|approval), the POST payload (respond done responder_type:user / rejected / approved), rendered content, optimistic removal (queryByText→null), and empty-state copy; uses fireEvent+act+toHaveBeenCalledWith (real behavioral assertions, not just render-smoke). Security: NO XSS vectors — zero dangerouslySetInnerHTML / innerHTML / eval; request data rendered as text via React (auto-escaped); actions POST through the apiGet/apiPost helpers. Performance: standard hooks/map render. Readability: clean component split + typed events. Content-security: CLEAN — scanned all 4 files: zero IPs/ARNs/log-groups/buckets/cred-VALUES. Dedicated required gate GREEN (CI/all-required + sop-pt + security-review-pt + qa-review-pt + CI/Platform(Go) all ✓); the reds are advisory (Local Provision E2E D2 — a success variant also present — + sop-pull_request, sop-pt is green). Approving → 2-distinct-genuine with agent-researcher.