Skip to content

fix(tui): publish synthetic reject event when permission/question ask is interrupted#35199

Open
sjawhar wants to merge 1 commit into
anomalyco:devfrom
sjawhar:fix/tui-permission-question-orphan-reject
Open

fix(tui): publish synthetic reject event when permission/question ask is interrupted#35199
sjawhar wants to merge 1 commit into
anomalyco:devfrom
sjawhar:fix/tui-permission-question-orphan-reject

Conversation

@sjawhar

@sjawhar sjawhar commented Jul 3, 2026

Copy link
Copy Markdown

Issue for this PR

Closes #28312

Type of change

  • Bug fix

What does this PR do?

When a permission or question ask is interrupted (tool cancel, session end, fiber kill), the cleanup finalizer used to just remove the pending entry without publishing anything. The TUI subscribes to Event.Replied/Event.Rejected to dismiss prompts, so without that final event the prompt stayed visible forever and Enter did nothing because the underlying entry was already gone.

This adds a synthetic reject publish inside the finalizer, but only when the entry is still in pending — the reply path deletes the entry before publishing its event, so this only fires on the orphan path and never races with a real reply.

How did you verify your code works?

  • New live test in test/permission/next.test.ts that forks ask(), interrupts the fiber, and asserts a reject event was published.
  • Equivalent live test in test/question/question.test.ts.
  • bun typecheck + focused tests pass locally.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Closes #28312

Supersedes #29352 (closed by the inactivity bot). Rebased onto latest dev with all merge conflicts resolved; typecheck and targeted tests pass on the fork branch.

@sjawhar sjawhar force-pushed the fix/tui-permission-question-orphan-reject branch from b8aff92 to 06756fa Compare July 12, 2026 13:25
When the underlying "ask" effect for a Permission or Question prompt is
interrupted (tool cancelled, session ended, parent killed, scope torn down)
rather than completing via a normal reply/reject, the Effect.ensuring
finalizer deleted the pending entry but never published a corresponding
terminal bus event. The TUI tracks prompt state via these events, so the
orphaned request stayed in sync.data.permission / sync.data.question
forever. Pressing Allow/Reject/typing a custom answer / pressing Esc all
issued SDK calls against a requestID that the server no longer had in
pending — the handler logs "reply for unknown request" and returns 200
OK silently with no event, so the TUI never dismissed and the prompt
became completely unresponsive (every option re-rendered the same view,
even exit was unreachable while the prompt held the input).

Fix: replace the unconditional pending.delete in the finalizer with an
Effect.gen that checks pending.has(id). The reply and reject paths delete
the entry themselves before publishing, so this guard fires only on the
interrupt path. When it does, delete the entry and publish a synthetic
terminal event so the TUI dismisses the orphan:

  - question/index.ts publishes Event.Rejected
  - permission/index.ts publishes Event.Replied with reply: "reject"
    (Permission has no dedicated cancellation event; "reject" is the
    accepted Reply value that signals dismissal)

Adds regression tests in both packages that fork the ask effect, wait
for it to be pending, interrupt the fiber, and assert the bus emits the
expected terminal event and the pending list is empty.
@sjawhar sjawhar force-pushed the fix/tui-permission-question-orphan-reject branch from 06756fa to 079f6dd Compare July 13, 2026 17:01
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.

TUI permission dialog can become stale: Enter confirm does nothing while /permission returns

1 participant