refactor(web): convert interaction store to direct named actions + createSelectors#31142
Conversation
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
Test Results — Interaction State Machine Zustand RefactorRan shell-only unit tests to verify the Zustand store integration. Full E2E UI testing was not possible (app can't build locally due to gitignored generated API client, no preview deployment, interaction prompts require a running assistant backend). Zustand Store Integration Tests (NEW — 10/10 passed, 46 assertions)
Existing Reducer Tests (34/34 passed, 62 assertions)All 34 pre-existing Static Analysis & CI
Limitations
|
…actions Replace all dispatchInteraction/interactionStateRef prop-threading with direct useInteractionStore calls. Stream handlers, hooks, and components now call named actions (showSecret, resetAll, etc.) via getState() for non-reactive reads and selector hooks for reactive subscriptions. Removes InteractionEvent type, dispatch pattern, and interactionStateRef from all consumer interfaces. Co-Authored-By: ashlee@vellum.ai <ashlee@vellum.ai>
…t-interaction-state-machine-usereducer-to-zustand
Wrap useInteractionStore with createSelectors() per CONVENTIONS.md, enabling .use.field() auto-generated selector hooks. Removes manual useInteractionState/useInteractionActions convenience hooks (dead code). Updates consumers from useInteractionStore((s) => s.field) to useInteractionStore.use.field() for idiomatic per-field subscriptions. Also removes dispatchInteraction from chat-store.test.ts which was testing the removed bridge property. Co-Authored-By: ashlee@vellum.ai <ashlee@vellum.ai>
68ab4bd to
0cf8fad
Compare
Resolved conflicts in 10 files where both branches removed their respective dispatch prop-threading: - Our branch removed dispatchInteraction (interaction Zustand store) - Main's #31144 removed dispatchTurn (turn Zustand store) Resolution: remove both dispatchers, use both Zustand stores directly. Also incorporates main's #31194 (organization store migration) and Co-Authored-By: ashlee@vellum.ai <ashlee@vellum.ai> #31110 (OpenAPI spec sync).
There was a problem hiding this comment.
✦ APPROVE (HEAD 07c7354e22)
Value: Removes ~300 lines of dispatch boilerplate and prop-threading while giving each interaction UI atomic subscriptions — components re-render only when their specific prompt field changes, not when any interaction event fires anywhere.
What this does: Replaces interactionReducer + InteractionEvent union + dispatchInteraction prop-drilling with a useInteractionStore Zustand store using direct named actions (showSecret(), dismissConfirmation(), etc.) and createSelectors. The Dispatch<InteractionEvent> wire is pulled from ChatStore, ChatPage, ChatRouteContent, and ~6 hooks. Stream handlers and async callbacks switch to .getState().action() for non-reactive reads.
Pattern compliance: Exactly follows the auth-store.ts / organization-store.ts canonical — createSelectors(create<Store>()(…)) → consumers call useInteractionStore.use.field() for atomic per-field subscriptions. The InteractionEvent union type is gone entirely; no stale .dispatch refs in closure. ✅
Test migration: beforeEach(() => useInteractionStore.getState().resetAll()) is the right reset strategy for module-level Zustand singletons — no more constructing { ...INITIAL_INTERACTION_STATE, field: override } per-test. The 211 deleted test lines are boilerplate removal, not coverage loss; every flow (secret, confirmation, contact-request, question) retains full transition coverage. ✅
Stale closure elimination: The old interactionStateRef.current pattern is fully removed. Stream handlers call useInteractionStore.getState().showSecret(…) at invocation time — same semantics as the ref, but without threading the ref through StreamHandlerContext. ✅
Non-blocking notes:
ChatRouteContentPropshas a blank line artifact where the interaction props were removed (around line 260 in the diff) — cosmetic.- Diff truncated before
dismissQuestion's implementation. The oldDISMISS_QUESTIONreducer explicitly zeroedisQuestionCardDismissed. Worth a quick confirm that the new action does the same — if it's covered by a test or the CI, no action needed.
Vellum Constitution — Inviting: direct named actions (showSecret, dismissConfirmation) lower the cognitive cost of touching interaction flows — any contributor can call them without studying the InteractionEvent union first.
Prompt / plan
Closes LUM-1624. Converts the interaction store from a reducer/dispatch pattern to idiomatic Zustand direct named actions with
createSelectors, and removes alldispatchInteractionprop-threading from consumers.Why
Reducer/dispatch is Zustand's escape hatch, not its recommendation. The Zustand flux guide positions
dispatch({ type, payload })as a fallback for Redux migrations. The idiomatic pattern is colocated named actions that callset()directly — no action-type strings, no switch statement.dispatchInteractionprop-threading was unnecessary. Every consumer imported the dispatch function through 3+ layers of props/context. With Zustand, components calluseInteractionStore.getState().showSecret(payload)directly — no props, no context, no ref sync.interactionStateRefwas manual Zustand. The oldMutableRefObject<InteractionState>synced viauseEffectsolved the same problem asstore.getState()— non-reactive reads without stale closures. Zustand eliminates this entirely.createSelectorsis the established pattern. Per CONVENTIONS.md and the auth store, every store wraps withcreateSelectors()for auto-generated.use.field()hooks with per-field re-render optimization. Zustand docs.What changed
interaction-store.tscreateSelectors(). Removed manual convenience hookschat-route-content.tsxinteractionState/dispatchInteractionprops. Uses.use.field()selectorsuse-interaction-actions.tsdispatchInteraction/interactionStateRefparams. 40+ dispatch calls →getState().action()use-stream-event-handler.tsdispatchInteractionfrom params andStreamHandlerContextuse-conversation-history.tsdispatchInteraction/interactionStateRef. UsesgetState()use-send-message.tsdispatchInteraction. 6 dispatch calls → store actionsuse-conversation-loader.tsdispatchInteraction/interactionStateRefinteraction-handlers.tsuseInteractionStore.getState()directly instead of receiving dispatch in contextchat-store.tsdispatchInteractionanddispatchTurnbridge properties — both stores are accessed directlychat-page.tsxuseReducerfor interactions, removedinteractionStateReffrom refsNet: 15 files, +448 −885 lines (net −437)
Safety
Alternatives not taken
use-interaction-actions.ts(734→600 lines) — still large, but splitting per prompt type is a separate concern. Deferred to follow-up.src/stores/— interaction state is domain-specific (only consumed withindomains/chat/), not cross-domain like auth. Stays indomains/interactions/.Test plan
@/generated/and design-library ref types)Link to Devin session: https://app.devin.ai/sessions/63b67eda9ffc47608417f0726b49a155
Requested by: @ashleeradka