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
- Have two sessions running (
A, B).
A requests an approval; dismiss the card with ✕ (do not allow or deny).
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.
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:
But the card/sound trigger in
handlePermissionRequestis gated on the queue being empty beforehand:A dismissed-but-queued request keeps
permissionQueue.countat ≥ 1 forever, so every subsequent request lands oncount >= 2and 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
A,B).Arequests an approval; dismiss the card with ✕ (do not allow or deny).Brequests an approval.Expected:
B's approval card appears with the usual sound.Actual: nothing.
BshowswaitingApprovalin the session list if you expand the notch manually, but there is no card and no sound.Bstays blocked.Suggested fix
Gate on visibility rather than on queue size — the same predicate the display path already uses:
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
main@ 9e3a1eb, macOS 15.x (Darwin 25.5.0), Apple SiliconFound while reviewing the fix for #308; it is an independent defect and that PR does not touch this path.