fix(mobile): retire approval cards decided elsewhere or timed out (CW-6880)

The mobile page kept live Allow/Deny buttons on an approval card after the
approval was decided from another client, cancelled by a turn teardown, or
timed out; pressing one then failed against a request that no longer
existed. Each card's actions now carry the approval id, and an
`approval.decided` or `approval.timeout` event for that id replaces the
buttons with "Decided: <decision>", "Cancelled" or "Timed out". Cards for
other approvals are untouched.

This is only the card-retirement item of CW-6880. Scope buttons, owner
labels, snapshot rebuild of pending approvals, the sticky banner and
notifications remain open there.

Verification on this tree:
- Real headless Chrome (playwright-core driving Google Chrome) against the
  page with the stream-messages token filled: four required cards; decided,
  timed-out and cancelled ids lost their buttons with the expected labels;
  the fourth card and an unknown id were unaffected; no page errors.
- Existing mobile tests, including the QuickJS evaluation of the page script
  (mobile_stream_statuses_use_selected_locale): 11 run, 11 passed,
  15340 skipped.
- npm test: 547 Node tests (540 passed, 7 skipped, 0 failed) and
  772 Vitest passed. npm run check:web: exit 0.
- Not run: a paired phone against a live Runtime.

Refs CW-6880, #6474

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NBKbhx19BRbZtvWK8rVKjz
This commit is contained in:
CodeWhale BotandClaude Opus 5.5 committed 2026-10-15 18:44:10 -07:00
1 parent 8a659250aa
commit b457a8affd
1 file changed
+16
+16
View File
@@ -572,6 +572,20 @@
return { id: approvalId };
}
function retireApproval(name, data) {
if (name !== "approval.decided" && name !== "approval.timeout") return;
const payload = eventPayload(data) || {};
const approvalId = payload.approval_id || payload.id;
if (!approvalId) return;
const label = name === "approval.timeout"
? "Timed out"
: payload.cancelled ? "Cancelled" : "Decided: " + (payload.decision || "decided");
for (const actions of document.querySelectorAll(".approval-actions")) {
if (actions.dataset.approvalId !== String(approvalId)) continue;
actions.innerHTML = "<span class='meta'>" + escapeHtml(label) + "</span>";
}
}
function pruneVisibleEvents() {
const events = $("events");
while (events.childElementCount > MAX_VISIBLE_EVENTS) {
@@ -597,6 +611,7 @@
function appendEvent(name, data) {
noteAgentEvent(name, data);
retireApproval(name, data);
const text = eventText(name, data);
if (!text) return;
const item = document.createElement("div");
@@ -612,6 +627,7 @@
if (approval) {
const actions = document.createElement("div");
actions.className = "approval-actions";
actions.dataset.approvalId = String(approval.id);
const allow = document.createElement("button");
allow.className = "primary";
allow.textContent = "Allow";