Skip to content

Treat an approval and a server request as one waiting state - #128

Merged
marmeladema merged 1 commit into
mainfrom
claude/sub-agent-approval-requests-vffexn
Jul 25, 2026
Merged

Treat an approval and a server request as one waiting state#128
marmeladema merged 1 commit into
mainfrom
claude/sub-agent-approval-requests-vffexn

Conversation

@marmeladema

@marmeladema marmeladema commented Jul 25, 2026

Copy link
Copy Markdown
Owner

Tier 1 of the approval/server-request unification. Presentation layer only — no protocol change.

The problem

An approval and a server request both stop a turn until the user answers. The browser only ever treated the first as blocking:

approval server request
activity rank 3 fell through to active_turn1
sidebar glyph ! o — indistinguishable from merely busy
sub-agent card "Needs approval" "Running"
notification yes none

So a sub-agent blocked on a question was exactly as invisible as the case fixed in #125 — on the path right next to it.

Codex itself already blurs the split: MCP tool approvals arrive as requestUserInput server requests and get promoted to approval cards by a marker (mapping.rs). The distinction is a protocol artifact, not something worth surfacing.

The change

Activity ranking, the sidebar glyph, the sub-agent monitor and notifications key off "waits on the user" rather than "is an approval".

Card rendering deliberately stays per-kind — a decision has fixed choices, a server request has a per-method form. That part was never duplicated and shouldn't be forced together.

Names follow behaviour. State that now holds both kinds loses its "approval" prefix (activity-waiting, waitingNotificationKey, notifiedRequests, waitingRequestRowById, …); pendingApprovals and answeredApprovals keep theirs, because they still hold only approvals. The card label becomes "Waiting on you", which is true of both. That accounts for most of the diff — it's a rename, and deliberately not left half-done, since names that lie about what they hold are how this divergence went unnoticed.

A second gap, found by writing the test

The waiting state has to clear when the user acts, not when the harness confirms. Answering an approval broadcasts ApprovalResolved; a server request's resolved event comes from the harness on its own schedule and may never come — so the sidebar kept demanding attention for something already answered. The browser now clears its own waiting state on send.

Also: the activity summary was the raw JSON-RPC method name, so notification bodies read item/tool/requestUserInput request. Now "Waiting for your input".

Verification

  • New e2e test: a thread waiting for input renders ! with activity-waiting and not activity-running, then drops back to plain running once answered.
  • 483 Rust tests, every target compiling, fmt --check and clippy --all-targets --all-features -D warnings clean.
  • e2e 31/31 on two of three full runs — see below for the third.

The ui.rs source assertions earned their keep here: they failed nine times through the rename, each on something real, including a genuine bug where the diagnostic payload's approval_idrequest_id rename left one reader still asking for approval_id, which would have keyed the notification untrack on undefined.

Known flake, not from this change

One of three full runs failed in thread-selection.spec.ts. I have the mechanism now:

14:40:00.569  starting new thread
14:40:00.575  turn completion event          ← scripted turn completes in 6ms, server-side
14:40:00.652  WS receive error: Connection reset by peer
14:40:00.773  auth: invalid or missing session cookie   ← ~6 more, ~1.3s apart

The turn finishes before the browser subscribes, so the reply can only arrive via history_page; the WebSocket then resets and the reconnect loop fails auth for ~7s, so the transcript never receives it and the 10s assertion times out. Same signature appeared during the #126 work, before server requests existed — it's a WS reconnect/auth issue in the replay setup, not this feature. Not fixed here; worth its own change.

Follow-up

Tier 2 is the remaining half: the server-side plumbing and wire shape. Since #127 landed, both pending_approval and pending_server_requests are verifiably redundant with accumulated (I confirmed by emptying each and watching the reload tests still pass), and pending_approval being Option was never a real cardinality limit — multiple simultaneous approvals work fine end to end. So that step is now mostly deletion plus merging the two answered_* lists, rather than reconciling two divergent shapes.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NhKuYcqLiesiQEDJJ1gBCY


Generated by Claude Code

Summary by CodeRabbit

  • New Features
    • Introduced a unified “Waiting on you” experience across approvals and server requests, including consistent “Waiting for your input” status, indicators, and sidebar ranking.
    • Waiting notifications are now request-focused (click-to-focus) and are cleared immediately after the user responds.
  • Documentation
    • Updated sub-agent and specification wording to match the new waiting-state semantics.
  • Bug Fixes
    • Improved waiting notification deduplication and close behavior to avoid stale prompts.
  • UI / Style
    • Refreshed waiting visual states and focused notification highlight styling.
    • Updated notification permission wording and accessibility labels.
  • Tests
    • Updated UI smoke and Playwright suites to assert waiting behavior, titles, navigation, and notification diagnostics.

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Server requests and approvals now share a “waiting on you” activity state. The frontend updates rendering, notifications, focus handling, resolution cleanup, sub-agent indicators, documentation, and regression tests.

Changes

Waiting-on-user activity

Layer / File(s) Summary
Activity contracts and summaries
crates/giskard-server/src/registry.rs, crates/giskard-server/src/routes.rs, specs/giskard-specification.md
Server-request summaries now use “Waiting for your input”; specification 1.60 documents unified waiting-state ranking and immediate clearing after user action.
Client activity rendering
crates/giskard-server/static/app.js, crates/giskard-server/static/app.css, crates/giskard-server/static/index.html, crates/giskard-server/static/sw.js
Thread activity ranking, indicators, replay handling, server-request events, notification labels, and related selectors now use waiting semantics.
Waiting notifications and focus
crates/giskard-server/static/app.js, crates/giskard-server/tests/ui.rs, tests/e2e/tests/server-requests.spec.ts, tests/e2e/tests/approvals.spec.ts, tests/e2e/tests/helpers.ts
Notification deduplication, diagnostics, click-to-focus behavior, closure, request identifiers, and resolution handling now cover approvals and server requests through waiting-request helpers.
Sub-agent waiting UI and coverage
crates/giskard-server/static/app.js, crates/giskard-server/static/app.css, docs/subagents.md, crates/giskard-server/tests/ui.rs, tests/e2e/tests/subagent-approvals.spec.ts
Sub-agent states and copy change to “Waiting on you”, with updated aggregation, styling, documentation, smoke tests, and end-to-end assertions.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Server
  participant WebSocketClient
  participant Notification
  participant ThreadUI
  Server->>WebSocketClient: send server_request_received
  WebSocketClient->>ThreadUI: mark thread activity-waiting
  WebSocketClient->>Notification: create waiting_request notification
  Notification->>ThreadUI: focus waiting request on click
  ThreadUI->>Server: send user answer
  ThreadUI->>Notification: close waiting notification
  ThreadUI->>ThreadUI: clear waiting activity
Loading

Possibly related PRs

  • marmeladema/Giskard#126: Modifies the connection-time bootstrap path for pending user-attention activities, overlapping with this PR’s server-request waiting activity and notification handling.
  • marmeladema/Giskard#127: Updates server-request state across reloads, overlapping with this PR’s waiting indicators and answered-request handling.
  • marmeladema/Giskard#96: Modifies notification-click focus handling, overlapping with this PR’s generalized waiting-request focus flow.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the core change: unifying approvals and server requests into one waiting state.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/sub-agent-approval-requests-vffexn

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
crates/giskard-server/static/app.js (1)

2226-2231: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Orphaned comment above releaseWaitingNotificationClaim.

Lines 2226-2228 describe the connect-bootstrap replay handler (handleThreadActivityBootstrap), not claim release. As it stands, two unrelated doc blocks stack on one function.

♻️ Move or drop the stale block
-// Cross-thread activity the server replayed because we were not connected when it happened. Paints
-// the badge exactly like a live event; notification is gated separately, because a reconnect is not
-// news — see `maybeNotifyWaitingRequest`.
 // Undo a notification claim when the notification did not actually reach the user. Both records
 // must be released together: leaving the session-scoped one set would silence every later replay of
 // an approval the user was never shown.
 function releaseWaitingNotificationClaim(notificationKey) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/giskard-server/static/app.js` around lines 2226 - 2231, Remove or
relocate the stale cross-thread activity comment so it documents
handleThreadActivityBootstrap rather than releaseWaitingNotificationClaim. Keep
the claim-release comment directly above releaseWaitingNotificationClaim and
ensure the two unrelated documentation blocks no longer stack on that function.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/giskard-server/static/app.js`:
- Around line 2325-2330: Update maybeNotifyWaitingRequest to derive the
notification headline from activity.kind, using input-specific wording for
requestUserInput and approval wording for approval requests while preserving the
existing sub-agent distinction. Update the corresponding assertion in
crates/giskard-server/tests/ui.rs to expect the new requestUserInput headline.

In `@tests/e2e/tests/approvals.spec.ts`:
- Line 58: Scope the activity-waiting assertion in the approval test to the
specific thread created and answered, using its retained identifier or row
locator rather than the page-wide .thread.activity-waiting locator. Assert that
only that thread’s row has no .activity-waiting element.

---

Nitpick comments:
In `@crates/giskard-server/static/app.js`:
- Around line 2226-2231: Remove or relocate the stale cross-thread activity
comment so it documents handleThreadActivityBootstrap rather than
releaseWaitingNotificationClaim. Keep the claim-release comment directly above
releaseWaitingNotificationClaim and ensure the two unrelated documentation
blocks no longer stack on that function.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b310614e-52d3-4c79-904f-51f6e62d06cd

📥 Commits

Reviewing files that changed from the base of the PR and between e5ba346 and 4efc06f.

📒 Files selected for processing (10)
  • crates/giskard-server/src/registry.rs
  • crates/giskard-server/src/routes.rs
  • crates/giskard-server/static/app.css
  • crates/giskard-server/static/app.js
  • crates/giskard-server/tests/ui.rs
  • docs/subagents.md
  • specs/giskard-specification.md
  • tests/e2e/tests/approvals.spec.ts
  • tests/e2e/tests/server-requests.spec.ts
  • tests/e2e/tests/subagent-approvals.spec.ts

Comment thread crates/giskard-server/static/app.js Outdated
Comment thread tests/e2e/tests/approvals.spec.ts Outdated
@marmeladema
marmeladema force-pushed the claude/sub-agent-approval-requests-vffexn branch from 4efc06f to d6efa37 Compare July 25, 2026 15:29

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
crates/giskard-server/static/app.js (2)

3060-3096: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

pending_server_requests replay never notifies, unlike pending_approval.

On reconnect, snap.pending_approval is routed through handleIncomingApprovalRequestnotifyIncomingApprovalmaybeNotifyWaitingRequest, so a still-pending approval re-arms the desktop notification. The pending_server_requests loop only calls setActiveThreadActivity(...) and renderServerRequest(request) — it never calls maybeNotifyWaitingRequest. A user who reconnects while a server request is outstanding gets the sidebar glyph but no notification, breaking the parity this PR is meant to establish between the two waiting kinds.

🐛 Proposed fix
   for (const request of (snap.pending_server_requests || [])) {
-    setActiveThreadActivity("server_request_received", true, "Waiting for your input", {
-      server_request_id:request && request.id ? String(request.id) : null
+    const serverRequestId = request && request.id ? String(request.id) : null;
+    setActiveThreadActivity("server_request_received", true, "Waiting for your input", {
+      server_request_id:serverRequestId
     });
     renderServerRequest(request);
+    maybeNotifyWaitingRequest(String(snap.thread_id || state.threadId || ""), {
+      kind:"server_request_received",
+      active_turn:true,
+      approval_id:null,
+      server_request_id:serverRequestId,
+      summary:"Waiting for your input",
+      source:"live_turn_snapshot_pending_server_request",
+      unread:false
+    });
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/giskard-server/static/app.js` around lines 3060 - 3096, Update the
pending_server_requests replay loop in renderLiveTurnSnapshot to invoke
maybeNotifyWaitingRequest for each still-pending server request, matching the
notification path used by pending_approval via handleIncomingApprovalRequest.
Preserve the existing activity update and renderServerRequest behavior while
ensuring reconnects re-arm the desktop notification.

284-293: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Generalize the notification diagnostics bucket

maybeNotifyWaitingRequest() and handleNotificationClick() still emit approval-prefixed diagnostics for server_request_received, while browserDiagnosticsSnapshot() groups them under last_approval_decision / recent_approval_decisions via isApprovalNotificationDecision(). That keeps the two waiting modes indistinguishable in the diagnostics summary. If this snapshot is meant to cover both approval and server-request waits, the event naming or predicate needs to be widened.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/giskard-server/static/app.js` around lines 284 - 293, Generalize the
notification diagnostics classification used by maybeNotifyWaitingRequest(),
handleNotificationClick(), and browserDiagnosticsSnapshot() so
server_request_received events are grouped alongside approval waits. Update
isApprovalNotificationDecision() and the related
last_approval_decision/recent_approval_decisions fields or event naming to use a
shared notification/waiting-request category while preserving existing approval
and server-request diagnostics.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@crates/giskard-server/static/app.js`:
- Around line 3060-3096: Update the pending_server_requests replay loop in
renderLiveTurnSnapshot to invoke maybeNotifyWaitingRequest for each
still-pending server request, matching the notification path used by
pending_approval via handleIncomingApprovalRequest. Preserve the existing
activity update and renderServerRequest behavior while ensuring reconnects
re-arm the desktop notification.
- Around line 284-293: Generalize the notification diagnostics classification
used by maybeNotifyWaitingRequest(), handleNotificationClick(), and
browserDiagnosticsSnapshot() so server_request_received events are grouped
alongside approval waits. Update isApprovalNotificationDecision() and the
related last_approval_decision/recent_approval_decisions fields or event naming
to use a shared notification/waiting-request category while preserving existing
approval and server-request diagnostics.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 63d0f853-849e-432c-adee-6d27558e486e

📥 Commits

Reviewing files that changed from the base of the PR and between 4efc06f and d6efa37.

📒 Files selected for processing (10)
  • crates/giskard-server/src/registry.rs
  • crates/giskard-server/src/routes.rs
  • crates/giskard-server/static/app.css
  • crates/giskard-server/static/app.js
  • crates/giskard-server/tests/ui.rs
  • docs/subagents.md
  • specs/giskard-specification.md
  • tests/e2e/tests/approvals.spec.ts
  • tests/e2e/tests/server-requests.spec.ts
  • tests/e2e/tests/subagent-approvals.spec.ts
🚧 Files skipped from review as they are similar to previous changes (9)
  • tests/e2e/tests/approvals.spec.ts
  • crates/giskard-server/src/registry.rs
  • crates/giskard-server/static/app.css
  • tests/e2e/tests/server-requests.spec.ts
  • docs/subagents.md
  • crates/giskard-server/src/routes.rs
  • specs/giskard-specification.md
  • tests/e2e/tests/subagent-approvals.spec.ts
  • crates/giskard-server/tests/ui.rs

@marmeladema
marmeladema force-pushed the claude/sub-agent-approval-requests-vffexn branch from d6efa37 to 3b24625 Compare July 25, 2026 15:50

Copy link
Copy Markdown
Owner Author

Addressed the review on d6efa37 — now at 3b24625.

Orphaned comment — valid, fixed. Inserting releaseWaitingNotificationClaim put it between the bootstrap doc block and the function it described. Each comment is back on its own function.

Diagnostics bucket — valid, and worse than reported. This wasn't only a naming inconsistency: the predicate matched detail.kind === "approval", and this PR renamed that payload to "waiting_request", so every browser_notification_* entry had silently dropped out of the diagnostics snapshot. Two more casualties of the same rename that the review didn't reach:

  • the panel read detail.approval_id, now request_id, so the approvalId: line always printed none;
  • the recent-entries renderer had the same stale field in two more places.

Fixed the predicate and renamed the bucket to match what it holds: isWaitingNotificationDiagnostic, last_waiting_notification / recent_waiting_notifications, and the approval_notify_* reasons became waiting_notify_* — a panel reading "approval_notify_received" for a question the agent asked is actively misleading while debugging notifications.


pending_server_requests replay never notifies — not reproducible; I believe this one is incorrect.

The premise is that a still-pending server request re-arms no notification on reconnect. It does, just not from the loop the comment points at.

renderLiveTurnSnapshot replays snap.accumulated through handleEvent before it reaches the pending_server_requests loop, and this PR added maybeNotifyWaitingRequest to the server_request_received case. Every ServerRequestReceived in the buffer goes through it, so the notification is already armed by the time that loop runs.

Verified rather than reasoned. Reconnecting with an outstanding request and the tab unfocused (so the visible-and-focused suppression doesn't mask the result):

PROBE notifications after reconnect: [
 { "t": "Giskard: input needed", "id": "scripted-server-request-1" }
]

Adding the suggested call would fire a second time for the same request and be swallowed by the dedup key — dead code that reads like it does something.

Worth noting the symmetry, since it explains the shape rather than just excusing it: snap.pending_approval is redundant the same way. It re-runs handleIncomingApprovalRequest for a card the accumulated replay already rendered, and renderApprovalRequest drops it on the pendingApprovals.has(id) guard. Both pending_* snapshot fields are derivable from accumulated — confirmed on #127 by emptying each and watching the reload tests still pass. Collapsing them is the follow-up noted in the PR description, and I'd rather do that than add a second notification path to the field that's on its way out.

Happy to reconsider if there's a reconnect path that skips the accumulated replay — I didn't find one.

Verification: 483 Rust tests with every target compiling, fmt --check and clippy --all-targets --all-features -D warnings clean, 32/32 e2e across three consecutive full runs.


Generated by Claude Code

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
crates/giskard-server/static/app.js (2)

360-391: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Diagnostics filter has gaps for some renamed reason strings.

isWaitingNotificationDiagnostic only matches waiting_notify_received, waiting_notify_suppressed_*, waiting_notify_constructor_failed, waiting_notify_created, and browser_notification_* with kind:"waiting_request". Several reasons emitted elsewhere in this file (waiting_notification_clicked, waiting_notify_skipped_invalid_call, approval_notification_closed/approval_notification_close_failed) don't match any of these patterns, so they're silently excluded from browserDiagnosticsSnapshot()'s last_waiting_notification/recent_waiting_notifications and thus from renderBrowserDiagnosticsPanel. This undermines the panel's usefulness for debugging why a notification did or didn't fire — see the consolidated comment for all affected sites and a combined fix.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/giskard-server/static/app.js` around lines 360 - 391, Update
isWaitingNotificationDiagnostic so it also recognizes the emitted
waiting-notification reasons waiting_notification_clicked,
waiting_notify_skipped_invalid_call, approval_notification_closed, and
approval_notification_close_failed, ensuring these entries appear in
browserDiagnosticsSnapshot() and renderBrowserDiagnosticsPanel diagnostics.

2438-2468: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Stale approval_notification_* diagnostic reason/key breaks the waiting-diagnostics filter and the id column.

closeWaitingNotification still records approval_notification_closed/approval_notification_close_failed with an approval_id key. This doesn't match isWaitingNotificationDiagnostic's waiting_notify_* patterns (excluded from the waiting panel), and since renderBrowserDiagnosticsPanel's generic event list reads detail.request_id (Line 481), not approval_id, these entries also lose their id in recentBrowserEvents. Consolidated fix below.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/giskard-server/static/app.js` around lines 2438 - 2468, The
diagnostics emitted by closeWaitingNotification use obsolete
approval_notification_* reasons and approval_id metadata. Update both success
and failure recordNotificationDiagnostic calls in closeWaitingNotification to
use the established waiting_notify_* reason/key conventions expected by
isWaitingNotificationDiagnostic and renderBrowserDiagnosticsPanel, preserving
the existing close behavior and failure error details.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/giskard-server/static/app.js`:
- Around line 284-293: Update the diagnostic classification used by
isWaitingNotificationDiagnostic so it recognizes the
waiting_notification_clicked reason emitted by handleNotificationClick, while
preserving recognition of the existing waiting_notify_* reasons and ensuring the
event appears in recent_waiting_notifications and last_waiting_notification.
- Around line 2245-2250: Rename the diagnostic reason emitted by
maybeNotifyWaitingRequest from waiting_notify_skipped_invalid_call to use the
established suppressed_ prefix, and update any corresponding diagnostics filter
references consistently.

---

Outside diff comments:
In `@crates/giskard-server/static/app.js`:
- Around line 360-391: Update isWaitingNotificationDiagnostic so it also
recognizes the emitted waiting-notification reasons
waiting_notification_clicked, waiting_notify_skipped_invalid_call,
approval_notification_closed, and approval_notification_close_failed, ensuring
these entries appear in browserDiagnosticsSnapshot() and
renderBrowserDiagnosticsPanel diagnostics.
- Around line 2438-2468: The diagnostics emitted by closeWaitingNotification use
obsolete approval_notification_* reasons and approval_id metadata. Update both
success and failure recordNotificationDiagnostic calls in
closeWaitingNotification to use the established waiting_notify_* reason/key
conventions expected by isWaitingNotificationDiagnostic and
renderBrowserDiagnosticsPanel, preserving the existing close behavior and
failure error details.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b083afb4-01da-4752-a86a-fa074fd39475

📥 Commits

Reviewing files that changed from the base of the PR and between d6efa37 and 3b24625.

📒 Files selected for processing (10)
  • crates/giskard-server/src/registry.rs
  • crates/giskard-server/src/routes.rs
  • crates/giskard-server/static/app.css
  • crates/giskard-server/static/app.js
  • crates/giskard-server/tests/ui.rs
  • docs/subagents.md
  • specs/giskard-specification.md
  • tests/e2e/tests/approvals.spec.ts
  • tests/e2e/tests/server-requests.spec.ts
  • tests/e2e/tests/subagent-approvals.spec.ts
🚧 Files skipped from review as they are similar to previous changes (8)
  • tests/e2e/tests/approvals.spec.ts
  • crates/giskard-server/src/registry.rs
  • crates/giskard-server/static/app.css
  • tests/e2e/tests/server-requests.spec.ts
  • docs/subagents.md
  • tests/e2e/tests/subagent-approvals.spec.ts
  • specs/giskard-specification.md
  • crates/giskard-server/tests/ui.rs

Comment thread crates/giskard-server/static/app.js
Comment thread crates/giskard-server/static/app.js
@marmeladema
marmeladema force-pushed the claude/sub-agent-approval-requests-vffexn branch 2 times, most recently from 0100360 to 077ffb0 Compare July 25, 2026 19:04

Copy link
Copy Markdown
Owner Author

Both findings valid — fixed in 077ffb0. They shared one root cause, and it was mine.

The consistency pass on 0100360 renamed the close-path reasons but landed them on a second prefix: waiting_notify_* for most of the family, waiting_notification_* for clicked / closed / close_failed. isWaitingNotificationDiagnostic enumerated four reasons individually, so it missed both the new prefix and waiting_notify_skipped_invalid_call, which had the right prefix but wasn't on the list.

Enumerating every emitted reason rather than fixing the two that were reported:

before after
waiting_notify_received
waiting_notify_created
waiting_notify_constructor_failed
waiting_notify_suppressed_* (×4)
waiting_notify_skipped_invalid_call
waiting_notification_clicked ✅ (renamed)
waiting_notification_closed ✅ (renamed)
waiting_notification_close_failed ✅ (renamed)

So four reasons were missing from the panel, not the two reported. The approval_id key on the close diagnostics was already fixed in 0100360 — worth noting since the review was written against 3b24625 and that part no longer applies.

The filter is now one prefix test rather than a list:

return reason.startsWith("waiting_notify_") ||
  (reason.startsWith("browser_notification_") && detail.kind === "waiting_request");

That's the actual fix. An enumeration silently omits whatever reason is added next, which is precisely how these fell out — adding four more entries to the list would have left the same trap in place. Added a ui.rs assertion on the prefix test specifically, so a future reason can't quietly drop out.

Skipped: renaming waiting_notify_skipped_invalid_call to a suppressed_ prefix. "Skipped invalid call" is a programming error — bad arguments reached the function — not a decision about whether to alert the user. Folding it in with the suppression reasons would make the panel less informative at exactly the moment you're reading it to find out why a notification didn't fire. It's included in the bucket now regardless, which was the real problem.

Verification: 483 Rust tests with every target compiling, fmt --check and clippy --all-targets --all-features -D warnings clean, e2e 32/32 on two of three full runs — the third hit the known thread-selection WS-reconnect flake documented in the PR description, unrelated to this area.


Generated by Claude Code

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/giskard-server/static/app.js`:
- Around line 3005-3021: The server_request_received branch in handleEvent must
skip replayed or already-answered requests before setting activity or calling
maybeNotifyWaitingRequest. Mirror the approval path’s answered-request guard,
using the request state consumed by renderServerRequest/resolveServerRequest,
then only run setActiveThreadActivity, renderServerRequest, and
maybeNotifyWaitingRequest for unresolved requests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 106d58ab-13d3-488d-91c6-b33e2ed49e73

📥 Commits

Reviewing files that changed from the base of the PR and between 0100360 and 077ffb0.

📒 Files selected for processing (13)
  • crates/giskard-server/src/registry.rs
  • crates/giskard-server/src/routes.rs
  • crates/giskard-server/static/app.css
  • crates/giskard-server/static/app.js
  • crates/giskard-server/static/index.html
  • crates/giskard-server/static/sw.js
  • crates/giskard-server/tests/ui.rs
  • docs/subagents.md
  • specs/giskard-specification.md
  • tests/e2e/tests/approvals.spec.ts
  • tests/e2e/tests/helpers.ts
  • tests/e2e/tests/server-requests.spec.ts
  • tests/e2e/tests/subagent-approvals.spec.ts
🚧 Files skipped from review as they are similar to previous changes (12)
  • tests/e2e/tests/approvals.spec.ts
  • crates/giskard-server/static/sw.js
  • crates/giskard-server/static/app.css
  • tests/e2e/tests/helpers.ts
  • crates/giskard-server/src/registry.rs
  • crates/giskard-server/static/index.html
  • docs/subagents.md
  • crates/giskard-server/src/routes.rs
  • tests/e2e/tests/server-requests.spec.ts
  • specs/giskard-specification.md
  • tests/e2e/tests/subagent-approvals.spec.ts
  • crates/giskard-server/tests/ui.rs

Comment thread crates/giskard-server/static/app.js
An approval and a server request both stop a turn until the user answers.
The browser only ever treated the first as blocking: a thread waiting on a
server request fell through to the generic active-turn branch, rendered as
`o` — indistinguishable from merely busy — ranked below an approval when a
row had to represent several descendants, never reached "Needs approval"
on a sub-agent card, and fired no notification at all. A sub-agent blocked
on a question was exactly as invisible as the case fixed in #125, on the
path next to it.

Codex itself already blurs the split: MCP tool approvals arrive as
`requestUserInput` server requests and get promoted to approval cards by a
marker. The distinction is a protocol artifact, not something worth
surfacing, so activity ranking, the sidebar glyph, the sub-agent monitor
and notifications now key off "waits on the user" rather than "is an
approval". Card rendering deliberately stays per-kind — a decision has
fixed choices, a server request has a per-method form — because that part
was never duplicated.

Names follow the behaviour. State that now holds both kinds is renamed
away from "approval" (`activity-waiting`, `waitingNotificationKey`,
`notifiedRequests`, `waitingRequestRowById`, and so on) so it stops
claiming to be approval-only; `pendingApprovals` and `answeredApprovals`
keep their names, because they still hold only approvals. The card label
becomes "Waiting on you", which is true of both.

Writing the test for it surfaced a second gap: the waiting state has to
clear when the user acts, not when the harness confirms. Answering an
approval broadcasts `ApprovalResolved`, but a server request's resolved
event comes from the harness on its own schedule and may never come, so
the sidebar kept demanding attention for something already answered. The
browser now clears its own waiting state on send. The activity summary
also stops being the raw JSON-RPC method name, which was reaching the
notification body.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NhKuYcqLiesiQEDJJ1gBCY
@marmeladema
marmeladema force-pushed the claude/sub-agent-approval-requests-vffexn branch from 077ffb0 to 2ead1fc Compare July 25, 2026 19:23

Copy link
Copy Markdown
Owner Author

Valid, and a real bug I introduced in this PR. Fixed in 2ead1fc.

Adding maybeNotifyWaitingRequest to the server_request_received branch made that path alert on every replayed event, including answered ones. handleIncomingApprovalRequest has always guarded this (isApprovalAnswered → render resolved and return); I added the notification without mirroring the guard.

Reproduced before fixing — answered a request, reloaded with the tab unfocused so the visible-and-focused suppression couldn't mask it:

PROBE notifications after reload of an ANSWERED request: [
 { "t": "Giskard: input needed", "id": "scripted-server-request-1" }
]

A backgrounded tab was being pinged about work the user had already done.

The branch now checks state.answeredServerRequests — seeded from answered_server_requests before the replay begins — and skips both the activity update and the notification, letting renderServerRequest settle the card into its resolved state from that same set.

Covered by a new e2e test, and confirmed to fail without the guard: removing it, rebuilding, and re-running fails exactly that test while the other four still pass.

One thing worth recording, since it bit me twice now: my first version of that test filtered notifications by request id alone and failed for the wrong reason. Earlier tests in the file leave outstanding requests behind, all reusing the same scripted id, and the connect bootstrap from #126 replays them — so the assertion was counting another test's leftovers. Now scoped by thread id as well. The shared, never-completing replay state makes any page-wide or id-only assertion quietly order-dependent.

Verification: 483 Rust tests with every target compiling, fmt --check and clippy --all-targets --all-features -D warnings clean, 33/33 e2e across three consecutive full runs — including the thread-selection spec, which passed all three this time.


Generated by Claude Code

@marmeladema
marmeladema merged commit fa93224 into main Jul 25, 2026
5 checks passed
@marmeladema
marmeladema deleted the claude/sub-agent-approval-requests-vffexn branch July 25, 2026 19:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants