import { listen } from "@tauri-apps/api/event"; import { invoke } from "@tauri-apps/api/core"; import { get } from "svelte/store"; import { claudeStore } from "$lib/stores/claude"; import { characterState } from "$lib/stores/character"; import { configStore } from "$lib/stores/config"; import { initStatsListener, resetSessionStats } from "$lib/stores/stats"; import { initAchievementsListener } from "$lib/stores/achievements"; import type { ConnectionStatus, PermissionPromptEvent, UserQuestionEvent, } from "$lib/types/messages"; import type { CharacterState } from "$lib/types/states"; import { initializeNotificationRules, cleanupNotificationRules, handleConnectionStatusChange, handleNewUserMessage, } from "$lib/notifications/rules"; interface StateChangePayload { state: CharacterState; tool_name: string | null; conversation_id?: string; } const connectedConversations = new Set(); let unlisteners: Array<() => void> = []; let skipNextGreeting = false; export function setSkipNextGreeting(skip: boolean) { skipNextGreeting = skip; } function getTimeOfDay(): string { const hour = new Date().getHours(); if (hour >= 5 && hour < 12) { return "morning"; } else if (hour >= 12 && hour < 17) { return "afternoon"; } else if (hour >= 17 && hour < 21) { return "evening"; } else { return "late night"; } } function generateGreetingPrompt(): string { const timeOfDay = getTimeOfDay(); return `[System: A new session has started. It's currently ${timeOfDay}. Please greet the user warmly and briefly. Keep it short - just 1-2 sentences.]`; } async function sendGreeting(conversationId: string) { // Check if we should skip this greeting if (skipNextGreeting) { skipNextGreeting = false; // Reset the flag return; } const config = configStore.getConfig(); if (!config.greeting_enabled) { return; } const greetingPrompt = config.greeting_custom_prompt?.trim() || generateGreetingPrompt(); // Don't show the system prompt in the UI - just trigger Claude to respond characterState.setState("thinking"); // Reset notification state for greeting handleNewUserMessage(); try { await invoke("send_prompt", { conversationId, message: greetingPrompt, }); } catch (error) { console.error("Failed to send greeting:", error); claudeStore.addLineToConversation(conversationId, "error", `Failed to send greeting: ${error}`); characterState.setTemporaryState("error", 3000); } } interface OutputPayload { line_type: string; content: string; tool_name: string | null; conversation_id?: string; } interface ConnectionPayload { status: ConnectionStatus; conversation_id?: string; } interface SessionPayload { session_id: string; conversation_id?: string; } interface WorkingDirectoryPayload { directory: string; conversation_id?: string; } export async function cleanupConversationTracking(conversationId: string) { connectedConversations.delete(conversationId); // Clean up any temp files associated with this conversation try { await invoke("cleanup_temp_files", { conversationId }); } catch (error) { console.error("Failed to cleanup temp files for conversation:", error); } } export async function initializeTauriListeners() { // Cleanup any existing listeners first cleanupTauriListeners(); // Initialize notification rules initializeNotificationRules(); // Initialize stats listener await initStatsListener(); // Initialize achievements listener await initAchievementsListener(); const connectionUnlisten = await listen("claude:connection", async (event) => { const { status, conversation_id } = event.payload; // Update connection status for the specific conversation if (conversation_id) { claudeStore.setConnectionStatusForConversation(conversation_id, status); } else { // Fallback to active conversation if no conversation_id claudeStore.setConnectionStatus(status); } // Handle notification for connection status handleConnectionStatusChange(status); if (status === "connected") { // Get the actual conversation ID (fallback to active if not provided) const targetConversationId = conversation_id || get(claudeStore.activeConversationId); if (targetConversationId) { // Add system message to the correct conversation claudeStore.addLineToConversation( targetConversationId, "system", "Connected to Claude Code" ); // Update character state for this conversation claudeStore.setCharacterStateForConversation(targetConversationId, "idle"); // Check if this specific conversation has connected before if (!connectedConversations.has(targetConversationId)) { connectedConversations.add(targetConversationId); resetSessionStats(); // Reset session stats on new connection await sendGreeting(targetConversationId); } } } else if (status === "disconnected") { const targetConversationId = conversation_id || get(claudeStore.activeConversationId); // Only remove from connected set if we're not about to reconnect if (!skipNextGreeting && targetConversationId) { connectedConversations.delete(targetConversationId); } // Don't add system message if we're about to reconnect if (!skipNextGreeting && targetConversationId) { claudeStore.addLineToConversation( targetConversationId, "system", "Disconnected from Claude Code" ); } // Update character state for this conversation if (targetConversationId) { claudeStore.setCharacterStateForConversation(targetConversationId, "idle"); } } else if (status === "error") { const targetConversationId = conversation_id || get(claudeStore.activeConversationId); if (targetConversationId) { connectedConversations.delete(targetConversationId); claudeStore.addLineToConversation(targetConversationId, "error", "Connection error"); } characterState.setTemporaryState("error", 3000); } }); unlisteners.push(connectionUnlisten); const stateUnlisten = await listen("claude:state", (event) => { const { state, conversation_id } = event.payload; const stateMap: Record = { idle: "idle", thinking: "thinking", typing: "typing", searching: "searching", coding: "coding", mcp: "mcp", permission: "permission", success: "success", error: "error", }; const mappedState = stateMap[state.toLowerCase()] || "idle"; // Always update the conversation's state if (conversation_id) { claudeStore.setCharacterStateForConversation(conversation_id, mappedState); } else { // Fallback to active conversation if no conversation_id const activeConversationId = get(claudeStore.activeConversationId); if (activeConversationId) { claudeStore.setCharacterStateForConversation(activeConversationId, mappedState); } } // Only update global character state for active conversation const activeConversationId = get(claudeStore.activeConversationId); if (!conversation_id || conversation_id === activeConversationId) { if (mappedState === "success" || mappedState === "error") { characterState.setTemporaryState(mappedState, 3000); } else { characterState.setState(mappedState); } } }); unlisteners.push(stateUnlisten); const outputUnlisten = await listen("claude:output", (event) => { const { line_type, content, tool_name, conversation_id } = event.payload; // Always store the output to the correct conversation if (conversation_id) { claudeStore.addLineToConversation( conversation_id, line_type as "user" | "assistant" | "system" | "tool" | "error", content, tool_name || undefined ); } else { // Fallback to active conversation if no conversation_id provided claudeStore.addLine( line_type as "user" | "assistant" | "system" | "tool" | "error", content, tool_name || undefined ); } }); unlisteners.push(outputUnlisten); const streamUnlisten = await listen("claude:stream", () => { // no-op }); unlisteners.push(streamUnlisten); const sessionUnlisten = await listen("claude:session", (event) => { const { session_id, conversation_id } = event.payload; // Store session ID for the correct conversation if (conversation_id) { claudeStore.setSessionIdForConversation(conversation_id, session_id); claudeStore.addLineToConversation( conversation_id, "system", `Session: ${session_id.substring(0, 8)}...` ); } else { // Fallback to active conversation if no conversation_id claudeStore.setSessionId(session_id); claudeStore.addLine("system", `Session: ${session_id.substring(0, 8)}...`); } }); unlisteners.push(sessionUnlisten); const cwdUnlisten = await listen("claude:cwd", (event) => { const { directory, conversation_id } = event.payload; // Store working directory for the correct conversation if (conversation_id) { claudeStore.setWorkingDirectoryForConversation(conversation_id, directory); } else { // Fallback to active conversation if no conversation_id claudeStore.setWorkingDirectory(directory); } }); unlisteners.push(cwdUnlisten); const permissionUnlisten = await listen("claude:permission", (event) => { const { id, tool_name, tool_input, description, conversation_id } = event.payload; // Store permission request for the specific conversation if (conversation_id) { claudeStore.requestPermissionForConversation(conversation_id, { id, tool: tool_name, description, input: tool_input, }); claudeStore.addLineToConversation( conversation_id, "system", `Permission requested for: ${tool_name}` ); } else { // Fallback to active conversation if no conversation_id claudeStore.requestPermission({ id, tool: tool_name, description, input: tool_input, }); claudeStore.addLine("system", `Permission requested for: ${tool_name}`); } }); unlisteners.push(permissionUnlisten); const questionUnlisten = await listen("claude:question", (event) => { const questionEvent = event.payload; // Store question request for the specific conversation if (questionEvent.conversation_id) { claudeStore.requestQuestionForConversation(questionEvent.conversation_id, questionEvent); claudeStore.addLineToConversation( questionEvent.conversation_id, "system", `Question: ${questionEvent.question}` ); } else { // Fallback to active conversation if no conversation_id claudeStore.requestQuestion(questionEvent); claudeStore.addLine("system", `Question: ${questionEvent.question}`); } }); unlisteners.push(questionUnlisten); } export function cleanupTauriListeners() { // Cleanup all event listeners unlisteners.forEach((unlisten) => unlisten()); unlisteners = []; // Cleanup notification rules cleanupNotificationRules(); }