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:
@@ -8,12 +8,10 @@
|
||||
*/
|
||||
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import { generateRandomString } from '../lib/crypto-utils';
|
||||
import { createLogger } from '../lib/logger';
|
||||
import type { Message } from './chatStore';
|
||||
|
||||
const log = createLogger('ConversationStore');
|
||||
import { persist, createJSONStorage } from 'zustand/middleware';
|
||||
import { generateRandomString } from '../../lib/crypto-utils';
|
||||
import { createIdbStorageAdapter } from '../../lib/idb-storage';
|
||||
import type { ChatMessage } from '../../types/chat';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
@@ -22,7 +20,7 @@ const log = createLogger('ConversationStore');
|
||||
export interface Conversation {
|
||||
id: string;
|
||||
title: string;
|
||||
messages: Message[];
|
||||
messages: ChatMessage[];
|
||||
sessionKey: string | null;
|
||||
agentId: string | null;
|
||||
createdAt: Date;
|
||||
@@ -45,9 +43,6 @@ export interface AgentProfileLike {
|
||||
role?: string;
|
||||
}
|
||||
|
||||
// Re-export Message for internal use (avoids circular imports during migration)
|
||||
export type { Message };
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// State interface
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -61,10 +56,10 @@ export interface ConversationState {
|
||||
currentModel: string;
|
||||
|
||||
// Actions
|
||||
newConversation: (currentMessages: Message[]) => Conversation[];
|
||||
switchConversation: (id: string, currentMessages: Message[]) => {
|
||||
newConversation: (currentMessages: ChatMessage[]) => Conversation[];
|
||||
switchConversation: (id: string, currentMessages: ChatMessage[]) => {
|
||||
conversations: Conversation[];
|
||||
messages: Message[];
|
||||
messages: ChatMessage[];
|
||||
sessionKey: string | null;
|
||||
currentAgent: Agent;
|
||||
currentConversationId: string;
|
||||
@@ -74,10 +69,10 @@ export interface ConversationState {
|
||||
conversations: Conversation[];
|
||||
resetMessages: boolean;
|
||||
};
|
||||
setCurrentAgent: (agent: Agent, currentMessages: Message[]) => {
|
||||
setCurrentAgent: (agent: Agent, currentMessages: ChatMessage[]) => {
|
||||
conversations: Conversation[];
|
||||
currentAgent: Agent;
|
||||
messages: Message[];
|
||||
messages: ChatMessage[];
|
||||
sessionKey: string | null;
|
||||
isStreaming: boolean;
|
||||
currentConversationId: string | null;
|
||||
@@ -87,7 +82,7 @@ export interface ConversationState {
|
||||
currentAgent: Agent;
|
||||
};
|
||||
setCurrentModel: (model: string) => void;
|
||||
upsertActiveConversation: (currentMessages: Message[]) => Conversation[];
|
||||
upsertActiveConversation: (currentMessages: ChatMessage[]) => Conversation[];
|
||||
getCurrentConversationId: () => string | null;
|
||||
getCurrentAgent: () => Agent | null;
|
||||
getSessionKey: () => string | null;
|
||||
@@ -101,7 +96,7 @@ function generateConvId(): string {
|
||||
return `conv_${Date.now()}_${generateRandomString(4)}`;
|
||||
}
|
||||
|
||||
function deriveTitle(messages: Message[]): string {
|
||||
function deriveTitle(messages: ChatMessage[]): string {
|
||||
const firstUser = messages.find(m => m.role === 'user');
|
||||
if (firstUser) {
|
||||
const text = firstUser.content.trim();
|
||||
@@ -155,7 +150,7 @@ export function resolveAgentForConversation(agentId: string | null, agents: Agen
|
||||
|
||||
function upsertActiveConversation(
|
||||
conversations: Conversation[],
|
||||
messages: Message[],
|
||||
messages: ChatMessage[],
|
||||
sessionKey: string | null,
|
||||
currentConversationId: string | null,
|
||||
currentAgent: Agent | null,
|
||||
@@ -199,7 +194,7 @@ export const useConversationStore = create<ConversationState>()(
|
||||
sessionKey: null,
|
||||
currentModel: 'glm-4-flash',
|
||||
|
||||
newConversation: (currentMessages: Message[]) => {
|
||||
newConversation: (currentMessages: ChatMessage[]) => {
|
||||
const state = get();
|
||||
const conversations = upsertActiveConversation(
|
||||
[...state.conversations], currentMessages, state.sessionKey,
|
||||
@@ -213,7 +208,7 @@ export const useConversationStore = create<ConversationState>()(
|
||||
return conversations;
|
||||
},
|
||||
|
||||
switchConversation: (id: string, currentMessages: Message[]) => {
|
||||
switchConversation: (id: string, currentMessages: ChatMessage[]) => {
|
||||
const state = get();
|
||||
const conversations = upsertActiveConversation(
|
||||
[...state.conversations], currentMessages, state.sessionKey,
|
||||
@@ -251,7 +246,7 @@ export const useConversationStore = create<ConversationState>()(
|
||||
return { conversations, resetMessages };
|
||||
},
|
||||
|
||||
setCurrentAgent: (agent: Agent, currentMessages: Message[]) => {
|
||||
setCurrentAgent: (agent: Agent, currentMessages: ChatMessage[]) => {
|
||||
const state = get();
|
||||
if (state.currentAgent?.id === agent.id) {
|
||||
set({ currentAgent: agent });
|
||||
@@ -328,7 +323,7 @@ export const useConversationStore = create<ConversationState>()(
|
||||
|
||||
setCurrentModel: (model: string) => set({ currentModel: model }),
|
||||
|
||||
upsertActiveConversation: (currentMessages: Message[]) => {
|
||||
upsertActiveConversation: (currentMessages: ChatMessage[]) => {
|
||||
const state = get();
|
||||
const conversations = upsertActiveConversation(
|
||||
[...state.conversations], currentMessages, state.sessionKey,
|
||||
@@ -344,11 +339,12 @@ export const useConversationStore = create<ConversationState>()(
|
||||
}),
|
||||
{
|
||||
name: 'zclaw-conversation-storage',
|
||||
storage: createJSONStorage(() => createIdbStorageAdapter()),
|
||||
partialize: (state) => ({
|
||||
conversations: state.conversations,
|
||||
currentModel: state.currentModel,
|
||||
currentAgentId: state.currentAgent?.id,
|
||||
currentConversationId: state.currentConversationId,
|
||||
sessionKey: state.sessionKey,
|
||||
}),
|
||||
onRehydrateStorage: () => (state) => {
|
||||
if (state?.conversations) {
|
||||
|
||||
Reference in New Issue
Block a user