anomalyco / anomalyco/opencode
TUI: question tool prompt not visible mid-stream — must interrupt to see it (Solid store key-addition reactivity gap)
@kommander is already working on this.
Since Aug 18, 2026.
- Dominant language
- TypeScript
- Stars
- 209k
- Forks
- 27.5k
- PR merge metrics
- PR metrics pending
Description
Summary
In the opencode TUI (macOS Terminal), when the assistant calls the question tool mid-response while tokens are still streaming, the question prompt is NOT visible to the user. The user must interrupt (Ctrl+C); only then does the question appear.
Root cause (corrected after live debugging)
A user-installed global plugin (~/.config/opencode/plugins/notify.ts) blocks tool execution on a synchronous macOS desktop notification.
The notify plugin registers a tool.execute.before hook that special-cases the question tool:
"tool.execute.before": async (input) => {
if (input.tool === "question") {
applyRuntimeSessionStatusTransition(...)
await notifyQuestionIfNeeded(...) // blocks here
}
}
notifyQuestionIfNeeded → handleQuestionAsked → sendDesktopNotification → sendMacOSAlerterNotification → Bun.spawn(["/opt/homebrew/bin/alerter", ...]) → await process.exited.
alerter is a blocking macOS notification tool — it does not exit until the user dismisses the notification. This blocks plugin.trigger("tool.execute.before"), so the question tool's execute body never runs, Question.ask never runs, Event.Asked is never published, the question.asked SSE event is never emitted, and the TUI never renders the prompt. On interrupt, the worker is killed → alerter dies → the blocked await rejects → the tool aborts. The "appears after interrupt" was the abort path, not a recovery path.
Why bash/read/grep tools worked
The notify plugin's hook only special-cases input.tool === "question". For all other tools the hook returns immediately.
What was disproved
- "Solid store key-addition reactivity gap" (original hypothesis) — WRONG. A unit test proved Solid 1.9.10 recomputes the
questions()memo on key addition. The store reactivity is fine; the event simply never arrived because the tool body never ran. - "SSE events unreliable mid-stream" — WRONG. The event was never emitted; the server-side
Question.ask(which publishesEvent.Asked) never executed. - 16ms event batching — bounded, not the cause.
Corroborating evidence
globalAskCountcounter inQuestion.askstayed0during repro — the function never ran.Tool.executewrapper fired forbashbut never forquestion— the tool body was never reached.- The blocking hook was identified as hook#11 (idx 11/12, keys
[tool.execute.before, event]) via per-hook tracing inplugin.trigger.
Defense-in-depth fix (this PR)
The user-side fix (changing await to void in the notify plugin) is outside this repo. However, the TUI currently has no recovery path for a genuinely missed question.asked/permission.asked SSE event, unlike the CLI transport which polls question.list as a fallback (packages/opencode/src/cli/cmd/run/stream.transport.ts:556-598). This PR adds the same defense-in-depth to the TUI so a missed event (network blip, slow hydration, plugin misbehavior) does not leave the prompt invisible.
Changes
packages/tui/src/context/sync.tsx—recoverPending(kind, sessionID, partID, messageID): when a tool part isrunningand the correspondingquestion/permissionstore key is empty, poll the v1question.list/permission.listendpoints every 250ms until the prompt appears, the part finishes, or the context unmounts. Mirrors the CLI'srecoverQuestion. Gated onpermission.mode !== "auto"for the permission branch (auto mode auto-replies and never stores). Also extendssync.session.syncto pre-initialize the keys on session open (cold-start).packages/tui/src/context/sdk.tsx— bypass the 16ms event batch forquestion.asked/question.replied/question.rejected/permission.asked/permission.repliedso they flush immediately.- Tests — 4 integration tests (
sync-recovery.test.tsx) and 4 unit tests (solid-reactivity.test.ts).
Environment
- opencode:
1.18.15(dev branch, HEAD4e81a0b) - Interface: TUI (macOS Terminal)
- OS: macOS (Darwin)
Repro
- Install the notify plugin (or any plugin with a blocking
tool.execute.beforehook for thequestiontool). - Start a TUI session with a prompt that causes the assistant to call
questionmid-response. - Observe no question prompt is visible; the stream appears stuck.
- Press Ctrl+C — the question appears (abort path).
Will submit PR
Yes — defense-in-depth TUI recovery, matching the CLI transport's existing pattern.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.