refactor(desktop): ChatStore structured split + IDB persistence + stream cancel
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Split monolithic chatStore.ts (908 lines) into 4 focused stores: - chatStore.ts: facade layer, owns messages[], backward-compatible selectors - conversationStore.ts: conversation CRUD, agent switching, IndexedDB persistence - streamStore.ts: streaming orchestration, chat mode, suggestions - messageStore.ts: token tracking Key fixes from 3-round deep audit: - C1: Fix Rust serde camelCase vs TS snake_case mismatch (toolStart/toolEnd/iterationStart) - C2: Fix IDB async rehydration race with persist.hasHydrated() subscribe - C3: Add sessionKey to partialize to survive page refresh - H3: Fix IDB migration retry on failure (don't set migrated=true in catch) - M3: Fix ToolCallStep deduplication (toolStart creates, toolEnd updates) - M-NEW-2: Clear sessionKey on cancelStream Also adds: - Rust backend stream cancellation via AtomicBool + cancel_stream command - IndexedDB storage adapter with one-time localStorage migration - HMR cleanup for cross-store subscriptions
This commit is contained in:
98
desktop/src/store/chat/messageStore.ts
Normal file
98
desktop/src/store/chat/messageStore.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* MessageStore — manages token tracking and subtask mutations.
|
||||
*
|
||||
* Extracted from chatStore.ts as part of the structured refactor (Phase 3).
|
||||
*
|
||||
* Design note: The `messages[]` array stays in chatStore because
|
||||
* `sendMessage` and `initStreamListener` use Zustand's `set((s) => ...)`
|
||||
* pattern for high-frequency streaming deltas (dozens of updates per second).
|
||||
* Moving messages out would force every streaming callback through
|
||||
* `getState().updateMessage()` — adding overhead and breaking the
|
||||
* producer-writes, consumer-reads separation that Zustand excels at.
|
||||
*
|
||||
* This store owns:
|
||||
* - Token usage counters (totalInputTokens, totalOutputTokens)
|
||||
* - Subtask mutation helpers (addSubtask, updateSubtask)
|
||||
*
|
||||
* Messages are read from chatStore by consumers that need them.
|
||||
*
|
||||
* @see docs/superpowers/specs/2026-04-02-chatstore-refactor-design.md §3.3
|
||||
*/
|
||||
|
||||
import { create } from 'zustand';
|
||||
import type { Subtask } from '../../components/ai';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// State interface
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface MessageState {
|
||||
totalInputTokens: number;
|
||||
totalOutputTokens: number;
|
||||
|
||||
// Token tracking
|
||||
addTokenUsage: (inputTokens: number, outputTokens: number) => void;
|
||||
getTotalTokens: () => { input: number; output: number; total: number };
|
||||
resetTokenUsage: () => void;
|
||||
|
||||
// Subtask mutations (delegated to chatStore internally)
|
||||
addSubtask: (messageId: string, task: Subtask) => void;
|
||||
updateSubtask: (messageId: string, taskId: string, updates: Partial<Subtask>) => void;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Internal reference to chatStore for message mutations
|
||||
// ---------------------------------------------------------------------------
|
||||
let _chatStore: {
|
||||
getState: () => {
|
||||
addSubtask: (messageId: string, task: Subtask) => void;
|
||||
updateSubtask: (messageId: string, taskId: string, updates: Partial<Subtask>) => void;
|
||||
};
|
||||
} | null = null;
|
||||
|
||||
/**
|
||||
* Inject chatStore reference for subtask delegation.
|
||||
* Called by chatStore during initialization to avoid circular imports.
|
||||
*/
|
||||
export function setMessageStoreChatStore(store: typeof _chatStore): void {
|
||||
_chatStore = store;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Store
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const useMessageStore = create<MessageState>()((set, get) => ({
|
||||
totalInputTokens: 0,
|
||||
totalOutputTokens: 0,
|
||||
|
||||
addTokenUsage: (inputTokens: number, outputTokens: number) =>
|
||||
set((state) => ({
|
||||
totalInputTokens: state.totalInputTokens + inputTokens,
|
||||
totalOutputTokens: state.totalOutputTokens + outputTokens,
|
||||
})),
|
||||
|
||||
getTotalTokens: () => {
|
||||
const { totalInputTokens, totalOutputTokens } = get();
|
||||
return {
|
||||
input: totalInputTokens,
|
||||
output: totalOutputTokens,
|
||||
total: totalInputTokens + totalOutputTokens,
|
||||
};
|
||||
},
|
||||
|
||||
resetTokenUsage: () =>
|
||||
set({ totalInputTokens: 0, totalOutputTokens: 0 }),
|
||||
|
||||
addSubtask: (messageId: string, task: Subtask) => {
|
||||
if (_chatStore) {
|
||||
_chatStore.getState().addSubtask(messageId, task);
|
||||
}
|
||||
},
|
||||
|
||||
updateSubtask: (messageId: string, taskId: string, updates: Partial<Subtask>) => {
|
||||
if (_chatStore) {
|
||||
_chatStore.getState().updateSubtask(messageId, taskId, updates);
|
||||
}
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user