import { writable, derived, get } from "svelte/store"; import type { TerminalLine, ConnectionStatus, PermissionRequest, UserQuestionEvent, } from "$lib/types/messages"; import type { CharacterState } from "$lib/types/states"; import { cleanupConversationTracking } from "$lib/tauri"; import { characterState } from "$lib/stores/character"; export interface Conversation { id: string; name: string; terminalLines: TerminalLine[]; sessionId: string | null; connectionStatus: ConnectionStatus; workingDirectory: string; characterState: CharacterState; isProcessing: boolean; grantedTools: Set; pendingPermission: PermissionRequest | null; pendingQuestion: UserQuestionEvent | null; scrollPosition: number; createdAt: Date; lastActivityAt: Date; } function createConversationsStore() { const conversations = writable>(new Map()); const activeConversationId = writable(null); const pendingRetryMessage = writable(null); let conversationCounter = 0; let lineIdCounter = 0; function generateConversationId(): string { return `conv-${Date.now()}-${conversationCounter++}`; } function generateLineId(): string { return `line-${Date.now()}-${lineIdCounter++}`; } function createNewConversation(name?: string): Conversation { const id = generateConversationId(); return { id, name: name || `Conversation ${conversationCounter}`, terminalLines: [], sessionId: null, connectionStatus: "disconnected", workingDirectory: "", characterState: "idle", isProcessing: false, grantedTools: new Set(), pendingPermission: null, pendingQuestion: null, scrollPosition: -1, // -1 means "scroll to bottom" (auto-scroll) createdAt: new Date(), lastActivityAt: new Date(), }; } // Initialize with first conversation lazily let initialized = false; function ensureInitialized() { if (!initialized) { initialized = true; const initialConversation = createNewConversation("Main"); conversations.update((convs) => { convs.set(initialConversation.id, initialConversation); return convs; }); activeConversationId.set(initialConversation.id); } } // Derived store for current conversation const activeConversation = derived( [conversations, activeConversationId], ([$conversations, $activeId]) => { if (!$activeId) return null; return $conversations.get($activeId) || null; } ); // Derived stores for compatibility with existing code const connectionStatus = derived( activeConversation, ($conv) => $conv?.connectionStatus || "disconnected" ); const terminalLines = derived(activeConversation, ($conv) => { return $conv?.terminalLines || []; }); const sessionId = derived(activeConversation, ($conv) => $conv?.sessionId || null); const currentWorkingDirectory = derived( activeConversation, ($conv) => $conv?.workingDirectory || "" ); const isProcessing = derived(activeConversation, ($conv) => $conv?.isProcessing || false); const grantedTools = derived( activeConversation, ($conv) => $conv?.grantedTools || new Set() ); const pendingPermission = derived( activeConversation, ($conv) => $conv?.pendingPermission || null ); const pendingQuestion = derived(activeConversation, ($conv) => $conv?.pendingQuestion || null); const scrollPosition = derived(activeConversation, ($conv) => $conv?.scrollPosition ?? -1); return { // Expose derived stores for compatibility connectionStatus: { subscribe: connectionStatus.subscribe }, sessionId: { subscribe: sessionId.subscribe }, currentWorkingDirectory: { subscribe: currentWorkingDirectory.subscribe }, terminalLines: { subscribe: terminalLines.subscribe }, pendingPermission: { subscribe: pendingPermission.subscribe }, pendingQuestion: { subscribe: pendingQuestion.subscribe }, isProcessing: { subscribe: isProcessing.subscribe }, grantedTools: { subscribe: grantedTools.subscribe }, pendingRetryMessage: { subscribe: pendingRetryMessage.subscribe }, scrollPosition: { subscribe: scrollPosition.subscribe }, // New conversation-specific stores conversations: { subscribe: conversations.subscribe }, activeConversationId: { subscribe: activeConversationId.subscribe }, activeConversation: { subscribe: activeConversation.subscribe }, // Connection management setConnectionStatus: (status: ConnectionStatus) => { const activeId = get(activeConversationId); if (!activeId) return; conversations.update((convs) => { const conv = convs.get(activeId); if (conv) { conv.connectionStatus = status; conv.lastActivityAt = new Date(); } return convs; }); }, setConnectionStatusForConversation: (conversationId: string, status: ConnectionStatus) => { conversations.update((convs) => { const conv = convs.get(conversationId); if (conv) { conv.connectionStatus = status; conv.lastActivityAt = new Date(); } return convs; }); }, setCharacterStateForConversation: (conversationId: string, state: CharacterState) => { conversations.update((convs) => { const conv = convs.get(conversationId); if (conv) { conv.characterState = state; // If this is the active conversation, update the global character state if (conversationId === get(activeConversationId)) { characterState.setState(state); } } return convs; }); }, requestPermission: (request: PermissionRequest) => { const activeId = get(activeConversationId); if (!activeId) return; conversations.update((convs) => { const conv = convs.get(activeId); if (conv) { conv.pendingPermission = request; conv.lastActivityAt = new Date(); } return convs; }); }, clearPermission: () => { const activeId = get(activeConversationId); if (!activeId) return; conversations.update((convs) => { const conv = convs.get(activeId); if (conv) { conv.pendingPermission = null; conv.lastActivityAt = new Date(); } return convs; }); }, requestPermissionForConversation: (conversationId: string, request: PermissionRequest) => { conversations.update((convs) => { const conv = convs.get(conversationId); if (conv) { conv.pendingPermission = request; conv.lastActivityAt = new Date(); } return convs; }); }, clearPermissionForConversation: (conversationId: string) => { conversations.update((convs) => { const conv = convs.get(conversationId); if (conv) { conv.pendingPermission = null; conv.lastActivityAt = new Date(); } return convs; }); }, requestQuestion: (question: UserQuestionEvent) => { const activeId = get(activeConversationId); if (!activeId) return; conversations.update((convs) => { const conv = convs.get(activeId); if (conv) { conv.pendingQuestion = question; conv.lastActivityAt = new Date(); } return convs; }); }, clearQuestion: () => { const activeId = get(activeConversationId); if (!activeId) return; conversations.update((convs) => { const conv = convs.get(activeId); if (conv) { conv.pendingQuestion = null; conv.lastActivityAt = new Date(); } return convs; }); }, requestQuestionForConversation: (conversationId: string, question: UserQuestionEvent) => { conversations.update((convs) => { const conv = convs.get(conversationId); if (conv) { conv.pendingQuestion = question; conv.lastActivityAt = new Date(); } return convs; }); }, clearQuestionForConversation: (conversationId: string) => { conversations.update((convs) => { const conv = convs.get(conversationId); if (conv) { conv.pendingQuestion = null; conv.lastActivityAt = new Date(); } return convs; }); }, setPendingRetryMessage: (message: string | null) => pendingRetryMessage.set(message), // Conversation management createConversation: (name?: string) => { ensureInitialized(); const newConv = createNewConversation(name); conversations.update((convs) => { convs.set(newConv.id, newConv); return convs; }); activeConversationId.set(newConv.id); return newConv.id; }, deleteConversation: (id: string) => { ensureInitialized(); const convs = get(conversations); const activeId = get(activeConversationId); if (convs.size <= 1) { // Don't delete the last conversation return false; } // Clean up tracking for this conversation cleanupConversationTracking(id); conversations.update((c) => { c.delete(id); return c; }); // If we deleted the active conversation, switch to another if (activeId === id) { const remaining = Array.from(get(conversations).keys()); if (remaining.length > 0) { activeConversationId.set(remaining[0]); } } return true; }, switchConversation: async (id: string) => { const convs = get(conversations); if (!convs.has(id)) return; const currentId = get(activeConversationId); const targetConv = convs.get(id); // If switching to a different conversation if (currentId !== id) { activeConversationId.set(id); // Update the global character state to match the conversation's state if (targetConv) { characterState.setState(targetConv.characterState); } } }, renameConversation: (id: string, newName: string) => { conversations.update((convs) => { const conv = convs.get(id); if (conv) { conv.name = newName; conv.lastActivityAt = new Date(); } return convs; }); }, saveScrollPosition: (id: string, position: number) => { conversations.update((convs) => { const conv = convs.get(id); if (conv) { conv.scrollPosition = position; } return convs; }); }, getScrollPosition: (id: string): number => { const convs = get(conversations); const conv = convs.get(id); return conv?.scrollPosition ?? -1; }, // Methods that operate on the active conversation setSessionId: (id: string | null) => { ensureInitialized(); const activeId = get(activeConversationId); if (!activeId) return; conversations.update((convs) => { const conv = convs.get(activeId); if (conv) { conv.sessionId = id; conv.lastActivityAt = new Date(); } return convs; }); }, setSessionIdForConversation: (conversationId: string, id: string | null) => { conversations.update((convs) => { const conv = convs.get(conversationId); if (conv) { conv.sessionId = id; conv.lastActivityAt = new Date(); } return convs; }); }, setWorkingDirectory: (dir: string) => { const activeId = get(activeConversationId); if (!activeId) return; conversations.update((convs) => { const conv = convs.get(activeId); if (conv) { conv.workingDirectory = dir; conv.lastActivityAt = new Date(); } return convs; }); }, setWorkingDirectoryForConversation: (conversationId: string, dir: string) => { conversations.update((convs) => { const conv = convs.get(conversationId); if (conv) { conv.workingDirectory = dir; conv.lastActivityAt = new Date(); } return convs; }); }, setProcessing: (processing: boolean) => { const activeId = get(activeConversationId); if (!activeId) return; conversations.update((convs) => { const conv = convs.get(activeId); if (conv) { conv.isProcessing = processing; conv.lastActivityAt = new Date(); } return convs; }); }, addLine: (type: TerminalLine["type"], content: string, toolName?: string) => { ensureInitialized(); const activeId = get(activeConversationId); if (!activeId) return ""; const line: TerminalLine = { id: generateLineId(), type, content, timestamp: new Date(), toolName, }; conversations.update((convs) => { const conv = convs.get(activeId); if (conv) { conv.terminalLines.push(line); conv.lastActivityAt = new Date(); } return convs; }); return line.id; }, addLineToConversation: ( conversationId: string, type: TerminalLine["type"], content: string, toolName?: string ) => { ensureInitialized(); const line: TerminalLine = { id: generateLineId(), type, content, timestamp: new Date(), toolName, }; conversations.update((convs) => { const conv = convs.get(conversationId); if (conv) { conv.terminalLines.push(line); conv.lastActivityAt = new Date(); } return convs; }); return line.id; }, updateLine: (id: string, content: string) => { const activeId = get(activeConversationId); if (!activeId) return; conversations.update((convs) => { const conv = convs.get(activeId); if (conv) { const line = conv.terminalLines.find((l) => l.id === id); if (line) { line.content = content; conv.lastActivityAt = new Date(); } } return convs; }); }, appendToLine: (id: string, additionalContent: string) => { const activeId = get(activeConversationId); if (!activeId) return; conversations.update((convs) => { const conv = convs.get(activeId); if (conv) { const line = conv.terminalLines.find((l) => l.id === id); if (line) { line.content += additionalContent; conv.lastActivityAt = new Date(); } } return convs; }); }, clearTerminal: () => { const activeId = get(activeConversationId); if (!activeId) return; conversations.update((convs) => { const conv = convs.get(activeId); if (conv) { conv.terminalLines = []; conv.lastActivityAt = new Date(); } return convs; }); }, getConversationHistory: (): string => { const activeId = get(activeConversationId); if (!activeId) return ""; const convs = get(conversations); const conv = convs.get(activeId); if (!conv) return ""; const relevantLines = conv.terminalLines.filter( (line) => line.type === "user" || line.type === "assistant" ); if (relevantLines.length === 0) return ""; return relevantLines .map((line) => { const role = line.type === "user" ? "User" : "Assistant"; return `${role}: ${line.content}`; }) .join("\n\n"); }, grantTool: (toolName: string) => { const activeId = get(activeConversationId); if (!activeId) return; conversations.update((convs) => { const conv = convs.get(activeId); if (conv) { conv.grantedTools.add(toolName); conv.lastActivityAt = new Date(); } return convs; }); }, revokeAllTools: () => { const activeId = get(activeConversationId); if (!activeId) return; conversations.update((convs) => { const conv = convs.get(activeId); if (conv) { conv.grantedTools.clear(); conv.lastActivityAt = new Date(); } return convs; }); }, isToolGranted: (toolName: string): boolean => { const activeId = get(activeConversationId); if (!activeId) return false; const convs = get(conversations); const conv = convs.get(activeId); return conv?.grantedTools.has(toolName) || false; }, // Add initialization helper initialize: () => { ensureInitialized(); }, }; } export const conversationsStore = createConversationsStore(); // Initialize immediately conversationsStore.initialize();