You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
AI code review — automated review for reference, author can ignore or act on any point.
Solid fix for a real UX dead-end. The mechanism checks out end-to-end: both form endpoints declare declaredStatuses: [404, 409, 400, 401] (packages/client/src/promise/generated/client.ts:1331, :1343), so the promise client throws the parsed tagged body directly and the _tag-narrowing guards work as written. The early releaseMode + visible={!dismissed()} approach keeps cleanup ordering safe (mode stack splice by symbol id, layers inert once the mode pops), and the new regression tests cover both entry points.
Suggestions:
FormAlreadySettledError (409) still traps the user — packages/tui/src/routes/session/form.tsx:273-279 (reply) and :478-484 (cancel) only special-case FormNotFoundError. A form answered or cancelled by another client (TUI + web open concurrently — likely the most common staleness case) returns 409 {_tag: "FormAlreadySettledError"}, which falls through to setStore("error", ...) and leaves the prompt up even though server state has settled. Suggested fix: also import isFormAlreadySettledError (exported from @opencode-ai/client via generated/types.js) and route that case through dismissStale() as well — optionally after toast.info("Form was answered elsewhere") so users aren't confused by the silent disappearance. Keeping 400 inline is right since invalid-answer errors are user-fixable.
Idempotency guard in dismissStale — form.tsx:487-493. Two rapid Enter presses can put two replies in flight; both 404 and each runs invalidate + sync. A cheap if (dismissed()) return at the top avoids redundant refetch churn.
Custom handler flows — dismissal now also triggers when embedder-provided props.onReply/props.onCancel reject with FormNotFoundError (form.tsx:266-267, :471-472), releasing FORM_MODE and re-syncing the shared form store under a host that may own its own lifecycle. Probably desired for consistency; just confirming it's intentional.
Minor: nice touch that the test fixture already stubs /api/session/{id}/form GET (packages/tui/test/fixture/tui-client.ts:145), so the post-dismissal sync() resolves against an empty list rather than relying on the .catch(toast.error).
Superseded by #44516, which handles both missing and already-settled forms and restores the composer from local projection state before background revalidation.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
FormNotFoundErrorfrom form replies and cancellations as a terminal stale-form stateVerification
bun test test/cli/tui/form.test.tsx --timeout 30000 --only-failuresbun typecheckRequested by: @kitlangton (Kit via Slack)