Task 11 — product-expert (PR 12)
Gate: PRs 5 (ux-drawers), 10 (product-expert-ff-agent), and 11 (product-expert-operator-agent) must all be merged first.
Vuex module: frontend/src/store/modules/product/expert/index.js
New files: frontend/src/stores/product-expert.js, frontend/src/stores/product-expert-context.js
Persistence: shouldWakeUpAssistant → localStorage, owned by product-expert-context (cleared on logout)
Cross-store dependencies: ux-drawers (open drawer), product-expert-context, product-expert-ff-agent, product-expert-operator-agent, context (expert getter), account bridge (featuresCheck)
Also: frontend/src/api/client.js top-level import store → lazy require to break a circular dependency introduced by this migration (see 11.x below)
11.1 — Key architectural change: sub-module state access
In Vuex, nested module state is merged into the parent. The expert store accesses sub-module state as state['ff-agent'].messages. In Pinia, each store is independent. All state[state.agentMode].* references must be replaced with imports of the relevant agent store:
// Vuex (current)
state[state.agentMode].messages
// Pinia (new)
const agentStore = this.agentMode === FF_AGENT
? useProductExpertFfAgentStore()
: useProductExpertOperatorAgentStore()
agentStore.messages
This affects: messages, hasMessages, hasUserMessages, lastMessage, isSessionExpired getters, and every action that writes to state[state.agentMode].*.
11.2 — Create the Pinia store
// frontend/src/stores/product-expert.js
import { defineStore } from 'pinia'
import { markRaw } from 'vue'
import { v4 as uuidv4 } from 'uuid'
import expertApi from '../api/expert.js'
// Note: ExpertDrawer is NOT imported here — importing it would form a circular dependency
// (product-expert.js → ExpertDrawer.vue → product-expert.js). ExpertButton.vue imports it
// directly; wakeUpAssistant uses a dynamic import() for the wake-up-from-context path.
import { FF_AGENT, OPERATOR_AGENT } from '../store/modules/product/expert/agents.js'
import { useUxDrawersStore } from './ux-drawers.js'
import { useContextStore } from './context.js'
import { useProductAssistantStore } from './product-assistant.js'
import { useProductExpertContextStore } from './product-expert-context.js'
import { useProductExpertFfAgentStore } from './product-expert-ff-agent.js'
import { useProductExpertOperatorAgentStore } from './product-expert-operator-agent.js'
import { useAccountBridge } from './_account-bridge.js'
export const useProductExpertStore = defineStore('product-expert', {
state: () => ({
// shouldWakeUpAssistant lives in product-expert-context — exposed here as a getter
agentMode: FF_AGENT,
loadingVariant: FF_AGENT,
}),
getters: {
// Helper to get the current agent store
_agentStore () {
return this.agentMode === FF_AGENT
? useProductExpertFfAgentStore()
: useProductExpertOperatorAgentStore()
},
messages () { return this._agentStore.messages },
hasMessages () { return this._agentStore.messages.length > 0 },
hasUserMessages () { return this._agentStore.messages.some(m => m.type === 'human') },
lastMessage () {
const msgs = this._agentStore.messages
return msgs.length > 0 ? msgs[msgs.length - 1] : null
},
shouldWakeUpAssistant () { return useProductExpertContextStore().shouldWakeUpAssistant },
isSessionExpired () { return this._agentStore.sessionExpiredShown },
isFfAgent: (state) => state.agentMode === FF_AGENT,
isOperatorAgent: (state) => state.agentMode === OPERATOR_AGENT,
hasSelectedCapabilities () {
return useProductExpertOperatorAgentStore().selectedCapabilities?.length > 0
},
canImportFlows () {
const assistantStore = useProductAssistantStore()
return !!assistantStore.immersiveInstance && !!assistantStore.supportedActions['custom:import-flow']
},
canManagePalette () {
const assistantStore = useProductAssistantStore()
return !!assistantStore.immersiveInstance && !!assistantStore.supportedActions['core:manage-palette']
}
},
actions: {
// Port all actions from Vuex, replacing:
// state[state.agentMode].* → this._agentStore.*
// commit('SET_SESSION_CHECK_TIMER', t) → agentStore.setSessionCheckTimer(t)
// commit('SET_ABORT_CONTROLLER', c) → this.abortController = markRaw(c)
// commit('SET_STREAMING_TIMER', t) → this.streamingTimer = markRaw(t)
// dispatch('ux/drawers/openRightDrawer', ...) → useUxDrawersStore().openRightDrawer(...)
// dispatch('ux/drawers/setRightDrawerWider', ...) → useUxDrawersStore().setRightDrawerWider(...)
// rootGetters['context/expert'] → useContextStore().expert
// rootGetters['account/featuresCheck'] → useAccountBridge().featuresCheck
// dispatch('product/expert/ff-agent/reset') → useProductExpertFfAgentStore().reset()
// dispatch('product/expert/operator-agent/...') → useProductExpertOperatorAgentStore().*
setAbortController (controller) {
this.abortController = controller ? markRaw(controller) : null
},
// ... all other actions ported verbatim with substitutions above
},
// No persist block — shouldWakeUpAssistant persistence is handled by product-expert-context
})
11.3 — Update context.js expert getter (final)
Now that all stores it depends on are on Pinia, replace the remaining bridge reads in the expert getter with direct Pinia store imports:
// In frontend/src/stores/context.js expert getter
import { useAccountBridge } from './_account-bridge.js' // keep for account (still Vuex)
import { useProductAssistantStore } from './product-assistant.js' // now Pinia
import { useProductExpertStore } from './product-expert.js' // now Pinia
11.4 — Find and update all consumers
grep -rl "product/expert\|mapState.*expert\|mapGetters.*expert\|mapActions.*expert" frontend/src/ | grep -v "ff-agent\|operator-agent"
Primary consumers: frontend/src/components/expert/*.vue, ExpertButton.vue, ExpertDrawer.vue.
For each consumer, check if it's inside a mixin or rendered as <component :is="..."> inside <ff-dialog> — use Pattern C from PINIA_COMPONENT_PATTERNS.md (mapState / mapActions from Pinia) — this works correctly in all component types.
Also update messaging.service.js:126: replace this.$store.dispatch('product/expert/setContext', payload) with useProductExpertContextStore().setContext(payload). The context store is a leaf — importing it here does not form a cycle.
11.5 — Delete the Vuex modules
Delete all three expert modules and the now-empty expert directory:
frontend/src/store/modules/product/expert/index.js
frontend/src/store/modules/product/expert/ff-agent/index.js
frontend/src/store/modules/product/expert/operator-agent/index.js
Remove expert from store/modules/product/index.js modules registration.
At this point the product Vuex root module only contains assistant (already migrated in PR 9), so this is also when you delete the product Vuex module entirely and remove it from store/index.js.
Logout bridge: uncomment useProductExpertStore().$reset() in the Vuex logout action (Task 0.7). At this point all product stores are migrated — the bridge should now have lines uncommented for Tasks 6, 7, 8, 9, 10, and 11.
11.6 — Write store tests
Two spec files are needed:
test/unit/frontend/stores/product-expert.spec.js — covers product-expert.js. Mock product-expert-context.js so shouldWakeUpAssistant resolves without the real store.
Key areas: _agentStore routing, isWaitingForResponse, addUserMessage, addAiMessage, addPredefinedAiMessage, addSystemMessage, updateMessageStreamedState, updateAnswerStreamedState, setAgentMode, setAbortController, reset.
test/unit/frontend/stores/product-expert-context.spec.js — covers product-expert-context.js. Mock product-expert-ff-agent.js with a module-level shared reference (not an inline factory) so mutations inside setContext are visible to test assertions.
Key areas: initial state, setContext (sets context/sessionId on ff-agent, sets flag, skips sessionId if absent, no-ops when feature disabled), clearWakeUp.
11.x — Circular import fixes
Migrating product-expert to Pinia exposes circular import cycles that ESM will crash on (TDZ errors). Three cycles exist; two are fixed here:
Cycle 1: product-expert.js → ExpertDrawer.vue → product-expert.js
Fix: ExpertButton.vue imports ExpertDrawer statically and opens the drawer directly. wakeUpAssistant (used for the wake-up-from-context path) uses a dynamic import() with a comment explaining why.
Cycle 2: product-expert.js → product-assistant.js → messaging.service.js → product-expert.js
Fix: extract shouldWakeUpAssistant + setContext into a new leaf store product-expert-context.js that imports only _account_bridge.js and product-expert-ff-agent.js (both leaves). messaging.service.js imports the context store directly — no cycle.
Side effect: setContext no longer calls wakeUpAssistant directly. pages/team/index.vue adds a watcher on shouldWakeUpAssistant to restore that reactivity.
Cycle 3: product-expert.js → api/client.js → store/index.js → … → product-expert.js
Deferred to Task 12 (account store migration). For now, api/client.js converts its top-level import store to a lazy require.
11.7 — Export from stores index
export { useProductExpertStore } from './product-expert.js'
Task 11 —
product-expert(PR 12)Gate: PRs 5 (
ux-drawers), 10 (product-expert-ff-agent), and 11 (product-expert-operator-agent) must all be merged first.Vuex module:
frontend/src/store/modules/product/expert/index.jsNew files:
frontend/src/stores/product-expert.js,frontend/src/stores/product-expert-context.jsPersistence:
shouldWakeUpAssistant→ localStorage, owned byproduct-expert-context(cleared on logout)Cross-store dependencies:
ux-drawers(open drawer),product-expert-context,product-expert-ff-agent,product-expert-operator-agent,context(expert getter), account bridge (featuresCheck)Also:
frontend/src/api/client.jstop-levelimport store→ lazyrequireto break a circular dependency introduced by this migration (see 11.x below)11.1 — Key architectural change: sub-module state access
In Vuex, nested module state is merged into the parent. The expert store accesses sub-module state as
state['ff-agent'].messages. In Pinia, each store is independent. Allstate[state.agentMode].*references must be replaced with imports of the relevant agent store:This affects:
messages,hasMessages,hasUserMessages,lastMessage,isSessionExpiredgetters, and every action that writes tostate[state.agentMode].*.11.2 — Create the Pinia store
11.3 — Update
context.jsexpert getter (final)Now that all stores it depends on are on Pinia, replace the remaining bridge reads in the
expertgetter with direct Pinia store imports:11.4 — Find and update all consumers
Primary consumers:
frontend/src/components/expert/*.vue,ExpertButton.vue,ExpertDrawer.vue.For each consumer, check if it's inside a mixin or rendered as
<component :is="...">inside<ff-dialog>— use Pattern C fromPINIA_COMPONENT_PATTERNS.md(mapState/mapActionsfrom Pinia) — this works correctly in all component types.Also update
messaging.service.js:126: replacethis.$store.dispatch('product/expert/setContext', payload)withuseProductExpertContextStore().setContext(payload). The context store is a leaf — importing it here does not form a cycle.11.5 — Delete the Vuex modules
Delete all three expert modules and the now-empty expert directory:
frontend/src/store/modules/product/expert/index.jsfrontend/src/store/modules/product/expert/ff-agent/index.jsfrontend/src/store/modules/product/expert/operator-agent/index.jsRemove
expertfromstore/modules/product/index.jsmodules registration.At this point the
productVuex root module only containsassistant(already migrated in PR 9), so this is also when you delete the product Vuex module entirely and remove it fromstore/index.js.Logout bridge: uncomment
useProductExpertStore().$reset()in the Vuex logout action (Task 0.7). At this point all product stores are migrated — the bridge should now have lines uncommented for Tasks 6, 7, 8, 9, 10, and 11.11.6 — Write store tests
Two spec files are needed:
test/unit/frontend/stores/product-expert.spec.js— coversproduct-expert.js. Mockproduct-expert-context.jssoshouldWakeUpAssistantresolves without the real store.Key areas:
_agentStorerouting,isWaitingForResponse,addUserMessage,addAiMessage,addPredefinedAiMessage,addSystemMessage,updateMessageStreamedState,updateAnswerStreamedState,setAgentMode,setAbortController,reset.test/unit/frontend/stores/product-expert-context.spec.js— coversproduct-expert-context.js. Mockproduct-expert-ff-agent.jswith a module-level shared reference (not an inline factory) so mutations insidesetContextare visible to test assertions.Key areas: initial state,
setContext(sets context/sessionId on ff-agent, sets flag, skips sessionId if absent, no-ops when feature disabled),clearWakeUp.11.x — Circular import fixes
Migrating
product-expertto Pinia exposes circular import cycles that ESM will crash on (TDZ errors). Three cycles exist; two are fixed here:Cycle 1:
product-expert.js → ExpertDrawer.vue → product-expert.jsFix:
ExpertButton.vueimportsExpertDrawerstatically and opens the drawer directly.wakeUpAssistant(used for the wake-up-from-context path) uses a dynamicimport()with a comment explaining why.Cycle 2:
product-expert.js → product-assistant.js → messaging.service.js → product-expert.jsFix: extract
shouldWakeUpAssistant+setContextinto a new leaf storeproduct-expert-context.jsthat imports only_account_bridge.jsandproduct-expert-ff-agent.js(both leaves).messaging.service.jsimports the context store directly — no cycle.Side effect:
setContextno longer callswakeUpAssistantdirectly.pages/team/index.vueadds a watcher onshouldWakeUpAssistantto restore that reactivity.Cycle 3:
product-expert.js → api/client.js → store/index.js → … → product-expert.jsDeferred to Task 12 (account store migration). For now,
api/client.jsconverts its top-levelimport storeto a lazyrequire.11.7 — Export from stores index