Skip to content

Dismissing an approval card silences every later permission request, from all sessions #309

Description

@halindrome

Summary

Once any approval card is dismissed with the ✕, later permission requests — from every session, not just the dismissed one — arrive with no card and no sound. The panel stays quiet until the dismissed request is resolved some other way (answered in the terminal, or the CLI exits), so an agent can sit blocked on an approval the user is never shown.

Mechanism

Dismissing hides a request but deliberately leaves it queued, so the CLI stays blocked and the prompt can be recovered:

// dismissPermissionPrompt
dismissedPermissionSessionIds.insert(sessionId)   // hides; does NOT dequeue

But the card/sound trigger in handlePermissionRequest is gated on the queue being empty beforehand:

permissionQueue.append(request)

// Show UI only if this is the first (or only) queued item
if permissionQueue.count == 1 {
    ...
    if surface != .sessionList, shouldAutoOpenPendingSurface(for: sessionId) {
        surface = .approvalCard(sessionId: sessionId)
    }
    SoundManager.shared.handleEvent("PermissionRequest")
}

A dismissed-but-queued request keeps permissionQueue.count at ≥ 1 forever, so every subsequent request lands on count >= 2 and takes neither branch. The count is a proxy for "is a card already showing", and dismissal is exactly the state where those two stop agreeing.

nextVisiblePermissionIndex() already models this correctly — it skips dismissed sessions when choosing what to show next. The enqueue path just doesn't consult it.

Repro

  1. Have two sessions running (A, B).
  2. A requests an approval; dismiss the card with ✕ (do not allow or deny).
  3. B requests an approval.

Expected: B's approval card appears with the usual sound.
Actual: nothing. B shows waitingApproval in the session list if you expand the notch manually, but there is no card and no sound. B stays blocked.

Suggested fix

Gate on visibility rather than on queue size — the same predicate the display path already uses:

let wasShowingSomething = nextVisiblePermissionIndex() != nil   // evaluated BEFORE the append
permissionQueue.append(request)
if !wasShowingSomething {
    ...
}

That keeps the "don't steal the card from a request already on screen" intent while treating dismissed entries as what they are: not on screen.

Environment

  • CodeIsland 1.0.31 / main @ 9e3a1eb, macOS 15.x (Darwin 25.5.0), Apple Silicon
  • Present since bb0eb72 (initial release); not introduced by any recent change.

Found while reviewing the fix for #308; it is an independent defect and that PR does not touch this path.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions