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 type { AgentStartPayload, AgentEndPayload } from "$lib/types/agents"; import { agentStore } from "$lib/stores/agents"; 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; cost?: { input_tokens: number; output_tokens: number; cost_usd: number; }; parent_tool_use_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); // Mark all running agents as errored on disconnect, but not during reconnects // (permission prompts trigger reconnects and agents may complete before reconnect) if (!skipNextGreeting && targetConversationId) { agentStore.markAllErrored(targetConversationId); } // 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, cost, parent_tool_use_id } = event.payload; // Convert snake_case cost to camelCase for TypeScript const costData = cost ? { inputTokens: cost.input_tokens, outputTokens: cost.output_tokens, costUsd: cost.cost_usd, } : undefined; // 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, costData, parent_tool_use_id ); } else { // Fallback to active conversation if no conversation_id provided claudeStore.addLine( line_type as "user" | "assistant" | "system" | "tool" | "error", content, tool_name || undefined, costData, parent_tool_use_id ); } }); 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 agentStartUnlisten = await listen("claude:agent-start", (event) => { const { tool_use_id, agent_id, description, subagent_type, started_at, conversation_id, parent_tool_use_id, } = event.payload; const targetConversationId = conversation_id || get(claudeStore.activeConversationId); if (targetConversationId) { agentStore.addAgent(targetConversationId, { toolUseId: tool_use_id, agentId: agent_id, description, subagentType: subagent_type, startedAt: started_at, status: "running", parentToolUseId: parent_tool_use_id, }); } }); unlisteners.push(agentStartUnlisten); const agentUpdateUnlisten = await listen<{ conversationId: string; toolUseId: string; agentId: string; }>("claude:agent-update", (event) => { const { conversationId, toolUseId, agentId } = event.payload; agentStore.updateAgentId(conversationId, toolUseId, agentId); }); unlisteners.push(agentUpdateUnlisten); const agentEndUnlisten = await listen("claude:agent-end", (event) => { const { tool_use_id, ended_at, is_error, conversation_id } = event.payload; const targetConversationId = conversation_id || get(claudeStore.activeConversationId); if (targetConversationId) { agentStore.endAgent(targetConversationId, tool_use_id, ended_at, is_error); } }); unlisteners.push(agentEndUnlisten); 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(); } export async function initializeDiscordRpc() { const config = configStore.getConfig(); if (config.discord_rpc_enabled) { try { const startedAt = new Date(); const startedAtUnixSeconds = Math.floor(startedAt.getTime() / 1000); const model = config.model || "claude"; await invoke("log_discord_rpc", { message: `[FRONTEND] Attempting to initialize Discord RPC: session='Idle', model='${model}', timestamp=${startedAtUnixSeconds}`, }); console.log("Initializing Discord RPC with initial activity:", { session_name: "Idle", model, started_at: startedAtUnixSeconds, }); await invoke("init_discord_rpc", { sessionName: "Idle", model, startedAt: startedAtUnixSeconds, }); await invoke("log_discord_rpc", { message: "[FRONTEND] Discord RPC initialized successfully!", }); console.log("Discord RPC initialized successfully with initial presence"); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); await invoke("log_discord_rpc", { message: `[FRONTEND] ERROR: Failed to initialize Discord RPC: ${errorMessage}`, }); console.error("Failed to initialize Discord RPC:", error); console.warn("Discord RPC will be unavailable. Make sure Discord is running."); } } else { await invoke("log_discord_rpc", { message: "[FRONTEND] Discord RPC is disabled in config, skipping initialization", }); } } export async function updateDiscordRpc(sessionName: string, model: string, startedAt: Date) { const config = configStore.getConfig(); if (!config.discord_rpc_enabled) { return; } try { const startedAtUnixSeconds = Math.floor(startedAt.getTime() / 1000); await invoke("update_discord_rpc", { sessionName: sessionName, model, startedAt: startedAtUnixSeconds, }); } catch (error) { console.error("Failed to update Discord RPC:", error); } } export async function stopDiscordRpc() { try { await invoke("stop_discord_rpc"); } catch (error) { console.error("Failed to stop Discord RPC:", error); } }