Found while reviewing #1068 (finding 4). Pre-existing, shared across every provider — filed rather than fixed there because the blast radius is all of conditions-core, not the OpenCode question flow.
What happens
src/shared/conditions-core/dispatch.ts:51 does await resolveCustom(action) and discards the result. Views call void dispatch(action). So a { ok: false, reason: 'invalid-payload' | 'no-resolver' | 'aborted' } produces no toast, no log, no state change — the button simply does nothing, and nothing anywhere tells the user why.
Why it matters more now
Before #1025 almost nothing but an HTTP failure could return invalid-payload on the question path. #1025 added a validator (correctly — it is the trust boundary that stops a renderer answering with a label OpenCode never offered), so a refusal is now reachable on a path a normal user hits: click an option for a question that has just been replaced, and the click is refused, silently.
The two bugs that made this acute are fixed in #1068 (a stale selection carried across a question swap; a reply validated against one question and sent to another). But the silence itself is still there, and it will hide the next one the same way.
What to do
Decide where a refused action surfaces. Options, roughly in order of appetite:
- Return the result from
dispatch and let each view decide (smallest change, but every view has to opt in).
- Surface
{ok:false} as a toast centrally in dispatch, with the reason mapped to something a person can act on ("that question was replaced — answer the new one").
- At minimum, journal it, so a bug report has something to point at.
aborted already carries failedAtStep, which is the useful half of a message and is currently thrown away too.
Found while reviewing #1068 (finding 4). Pre-existing, shared across every provider — filed rather than fixed there because the blast radius is all of
conditions-core, not the OpenCode question flow.What happens
src/shared/conditions-core/dispatch.ts:51doesawait resolveCustom(action)and discards the result. Views callvoid dispatch(action). So a{ ok: false, reason: 'invalid-payload' | 'no-resolver' | 'aborted' }produces no toast, no log, no state change — the button simply does nothing, and nothing anywhere tells the user why.Why it matters more now
Before #1025 almost nothing but an HTTP failure could return
invalid-payloadon the question path. #1025 added a validator (correctly — it is the trust boundary that stops a renderer answering with a label OpenCode never offered), so a refusal is now reachable on a path a normal user hits: click an option for a question that has just been replaced, and the click is refused, silently.The two bugs that made this acute are fixed in #1068 (a stale selection carried across a question swap; a reply validated against one question and sent to another). But the silence itself is still there, and it will hide the next one the same way.
What to do
Decide where a refused action surfaces. Options, roughly in order of appetite:
dispatchand let each view decide (smallest change, but every view has to opt in).{ok:false}as a toast centrally indispatch, with the reason mapped to something a person can act on ("that question was replaced — answer the new one").abortedalready carriesfailedAtStep, which is the useful half of a message and is currently thrown away too.