Skip to content

refactor(web): convert conversation-list-store to Zustand with direct named actions#31214

Merged
ashleeradka merged 3 commits into
mainfrom
devin/1779229528-lum-1622-conversation-list-store-zustand
May 19, 2026
Merged

refactor(web): convert conversation-list-store to Zustand with direct named actions#31214
ashleeradka merged 3 commits into
mainfrom
devin/1779229528-lum-1622-conversation-list-store-zustand

Conversation

@ashleeradka
Copy link
Copy Markdown
Contributor

@ashleeradka ashleeradka commented May 19, 2026

Prompt / plan

Closes LUM-1622. Converts the last remaining reducer-pattern shared store (conversationListReducer) to Zustand with create() + createSelectors + direct named actions — completing the migration started by #31201 (subagent-store) and #31209 (viewer-store).

Why needed

conversationListReducer used useReducer with a 20-type discriminated union (ConversationListAction) and threaded dispatchConversationList: Dispatch<ConversationListAction> through 12 consumer files and StreamHandlerContext. This is the exact prop-drilling / parameter-threading anti-pattern that Zustand is designed to eliminate.

What changed

Store conversion (conversation-list-store.ts):

  • Replaced conversationListReducer + ConversationListAction (20 type interfaces + discriminated union) with a Zustand store using create() + createSelectors
  • 20 named actions: setConversations(), patchConversation(), markConversationSeen(), prependConversation(), removeConversation(), resolveDraftKey(), setGroups(), appendGroup(), patchGroup(), replaceOptimisticGroup(), removeGroup(), deleteGroupAndResetConversations(), setActiveKey(), setEditingKey(), addProcessingKey(), removeProcessingKey(), removeMultipleProcessingKeys(), transferProcessingKey(), addAttentionKey(), removeAttentionKey(), graduateProcessingKey(), reset()
  • Renamed the exported patchConversation() helper to applyConversationPatch() to avoid collision with the store action
  • Removed INITIAL_CONVERSATION_LIST_STATE, conversationListReducer, ConversationListAction, and all 20 action type interfaces

Consumer updates (12 files):

  • Removed dispatchConversationList parameter from all hook interfaces and function signatures
  • Replaced all dispatchConversationList({ type: "ACTION_NAME", ... }) calls with useConversationListStore.getState().actionName(...) — correct pattern for event handlers/callbacks per CONVENTIONS.md
  • Removed dispatchConversationList from StreamHandlerContext (the last remaining Dispatch<Action> in the context)
  • Cleaned up unused Dispatch, ConversationListAction imports throughout

Tests:

  • Rewrote 31 store tests from conversationListReducer(state, action) pattern to getState().actionName() with afterEach reset
  • Updated metadata-handlers test to verify store state directly instead of mock dispatch calls

Benefits

  • Eliminates dispatch threading: 12 files no longer need dispatchConversationList as a parameter
  • Completes the migration: All shared stores now use Zustand with direct named actions (subagent, viewer, interaction, turn, conversation-list)
  • Net -281 lines: 748 insertions, 1029 deletions — the discriminated union + action interfaces are gone
  • Type-safe actions: Named methods with typed parameters replace stringly-typed { type: "ACTION_NAME" } dispatch objects

Safety

What was NOT done and why

  • Did not extract navigation state from conversation-list-store: activeConversationKey and editingConversationKey could arguably live in a separate store, but this would add churn without benefit — LUM-1651 (mainView → URL routing) will likely reshape this state anyway
  • Did not convert component-local useReducer instances (terminal, voice, navigation-history): Per Zustand guidance, component-local state that isn't shared doesn't need a Zustand store

References

Test plan

  • 31 unit tests pass (bun test conversation-list-store.test.ts)
  • bunx tsc --noEmit — clean
  • bun run lint — clean
  • CI

Link to Devin session: https://app.devin.ai/sessions/565d827296144ac9bf12bd108169e5ef
Requested by: @ashleeradka


Open in Devin Review

… named actions

Closes LUM-1622

- Convert conversationListReducer (20 action types + discriminated union) to
  Zustand store with create() + createSelectors + 20 direct named actions
- Remove dispatchConversationList threading from 12 consumer files and
  StreamHandlerContext — consumers now call
  useConversationListStore.getState().actionName() directly
- Rename exported patchConversation() helper to applyConversationPatch() to
  avoid collision with the new store action of the same name
- Rewrite 31 tests from reducer pattern to Zustand store API
- Update stale docstrings referencing useReducer/dispatch patterns
- Clean up unused Dispatch/ConversationListAction imports across all files

Co-Authored-By: ashlee@vellum.ai <ashlee@vellum.ai>
@devin-ai-integration
Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@linear
Copy link
Copy Markdown

linear Bot commented May 19, 2026

LUM-1622

Co-Authored-By: ashlee@vellum.ai <ashlee@vellum.ai>
vex-assistant-bot[bot]
vex-assistant-bot Bot previously approved these changes May 19, 2026
Copy link
Copy Markdown
Contributor

@vex-assistant-bot vex-assistant-bot Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APPROVE

Value: Completes the Zustand migration for the last major reducer-pattern store. Eliminates dispatchConversationList: Dispatch<ConversationListAction> threading across 8 hooks and StreamHandlerContext — 20 discriminated-union action interfaces down to 0, ~120 lines of parameter passing deleted from consumers. LUM-1622 done.


Store design (conversation-list-store.ts) — all correct:

  • createSelectors(create<ConversationListStore>(...)) — canonical pattern ✅
  • Named actions co-located in store (Flux-inspired Zustand pattern) ✅
  • applyConversationPatch / applyGroupPatch rename avoids shadowing the store action methods — clean ✅
  • reset() creates fresh Set() instances — not reusing mutable state ✅
  • deleteGroupAndResetConversations + graduateProcessingKey update multiple fields atomically in one set() — correct ✅
  • Set helpers return same reference on no-ops — preserves Zustand shallow equality bailout ✅

Consumer hooks — all correct:

  • getState().action() in event handlers / effects (not hooks) — correct pattern ✅
  • dispatchConversationList removed from all useCallback dep arrays ✅
  • StreamHandlerContext.dispatchConversationList eliminated at the type level in types.ts
  • test-helpers.ts mock removed, metadata-handlers.test.ts pre-seeds + reads store state — correct testing approach ✅

Two non-blocking observations:

graduateProcessingKey in the store calls get() twice in one set():

set({
  processingKeys: removeFromSet(get().processingKeys, key),
  attentionKeys: hasPendingInteraction ? addToSet(get().attentionKeys, key) : get().attentionKeys,
});

Both get() calls read the same pre-mutation snapshot, so this is correct. Functional set(state => ...) form would be marginally more explicit about intent, but this works fine in practice.

use-attention-tracking.tsdecideGraduationDispatches still returns typed action objects (.type field), and the new loop branches on ADD_ATTENTION_KEY vs else. Since graduateProcessingKey now exists as a compound action, this could eventually be simplified — but the current mapping is correct: ADD_ATTENTION_KEY → addAttentionKey, everything else → removeProcessingKey. No behavioral regression.


One housekeeping note: #31140 (previous LUM-1622 PR — feat(chat): Convert conversation-list useReducer to Zustand store) is now closed without merge, superseded by this PR. May want to confirm the Linear ticket reflects this PR as the closing one.

Anti-patterns clean: No useShallow on owned store. No Dispatch threading. No barrel files. No useReducer in consumers. ✅

The 20-interface discriminated union was load-bearing complexity for no architectural gain over named actions. This is the right call.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6fc9a030ca

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

* so Zustand can bail out of re-renders.
*/
export function patchConversation(
export function applyConversationPatch(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve patchConversation export alias

Renaming the exported helper to applyConversationPatch removes the patchConversation symbol, but apps/web/src/domains/chat/hooks/use-conversation-actions.test.ts still imports patchConversation from this module. That causes the test file to fail at module load when run, so this refactor leaves part of the test surface broken unless all importers are updated in the same commit (or a compatibility alias is kept).

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already fixed in 7de0174 — the test file was updated to import applyConversationPatch in the follow-up commit. CI is green.

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

View 5 additional findings in Devin Review.

Open in Devin Review

Comment on lines 72 to 73
return changed ? result : conversations;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 Test file imports renamed function patchConversation (now applyConversationPatch)

The PR renamed the exported helper from patchConversation to applyConversationPatch in the store (apps/web/src/domains/conversations/conversation-list-store.ts:61), but apps/web/src/domains/chat/hooks/use-conversation-actions.test.ts:5 still imports patchConversation. This will be caught by TypeScript (bunx tsc --noEmit), so it's not reported as a bug per review guidelines. Additionally, applyConversationPatch is exported but only consumed internally by the store's own patchConversation action — the export is no longer needed externally.

(Refers to lines 61-73)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rename was already applied to the test file in 7de0174 — CI is green.

The export is still needed: use-conversation-actions.test.ts imports and tests applyConversationPatch directly (verifying immutability, no-match behavior, multi-field patches). The function is a pure utility used by the store's patchConversation action, and keeping it exported + tested is intentional.

…state access

Co-Authored-By: ashlee@vellum.ai <ashlee@vellum.ai>
@ashleeradka ashleeradka merged commit 3bc4202 into main May 19, 2026
3 checks passed
@ashleeradka ashleeradka deleted the devin/1779229528-lum-1622-conversation-list-store-zustand branch May 19, 2026 22:53
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.

1 participant