import { get } from "svelte/store"; import { invoke } from "@tauri-apps/api/core"; import { claudeStore } from "$lib/stores/claude"; import { characterState } from "$lib/stores/character"; import { setSkipNextGreeting, updateDiscordRpc } from "$lib/tauri"; import { searchState } from "$lib/stores/search"; import { conversationsStore } from "$lib/stores/conversations"; import { configStore } from "$lib/stores/config"; export interface SlashCommand { name: string; description: string; usage: string; execute: (args: string) => Promise | void; } async function changeDirectory(path: string): Promise { const conversationId = get(claudeStore.activeConversationId); if (!conversationId) { claudeStore.addLine("error", "No active conversation"); return; } if (!path.trim()) { const currentDir = get(claudeStore.currentWorkingDirectory); claudeStore.addLine("system", `Current directory: ${currentDir}`); return; } try { characterState.setState("thinking"); claudeStore.addLine("system", `Changing directory to: ${path}`); const currentDir = get(claudeStore.currentWorkingDirectory); const validatedPath = await invoke("validate_directory", { path, currentDir }); // Capture conversation history before disconnecting const conversationHistory = claudeStore.getConversationHistory(); // Get currently granted tools and config auto-granted tools const activeConversation = get(conversationsStore.activeConversation); const grantedTools = activeConversation ? Array.from(activeConversation.grantedTools) : []; const config = configStore.getConfig(); const allAllowedTools = [...new Set([...grantedTools, ...config.auto_granted_tools])]; await invoke("stop_claude", { conversationId }); // Wait for clean shutdown await new Promise((resolve) => setTimeout(resolve, 500)); claudeStore.setWorkingDirectory(validatedPath); setSkipNextGreeting(true); await invoke("start_claude", { conversationId, options: { working_dir: validatedPath, allowed_tools: allAllowedTools, }, }); // Update Discord RPC when reconnecting after directory change if (activeConversation) { await updateDiscordRpc( activeConversation.name, config.model || "claude", activeConversation.startedAt ); } // Wait for connection to establish await new Promise((resolve) => setTimeout(resolve, 1000)); // Restore context if there was conversation history if (conversationHistory) { const contextMessage = `[CONTEXT RESTORATION] I just changed the working directory from ${currentDir} to ${validatedPath}. Here's our conversation so far: ${conversationHistory} Please continue where we left off. You are now operating in the new directory.`; await invoke("send_prompt", { conversationId, message: contextMessage, }); } claudeStore.addLine("system", `Changed directory to: ${validatedPath}`); characterState.setState("idle"); } catch (error) { claudeStore.addLine("error", `Failed to change directory: ${error}`); characterState.setTemporaryState("error", 3000); } } async function startNewConversation(): Promise { const conversationId = get(claudeStore.activeConversationId); if (!conversationId) { claudeStore.addLine("error", "No active conversation"); return; } try { const workingDir = await invoke("get_working_directory", { conversationId, }); claudeStore.addLine("system", "Starting new conversation..."); characterState.setState("thinking"); await invoke("interrupt_claude", { conversationId }); claudeStore.clearTerminal(); setSkipNextGreeting(true); await invoke("start_claude", { conversationId, options: { working_dir: workingDir, }, }); // Update Discord RPC when starting new conversation const config = configStore.getConfig(); const activeConversation = get(conversationsStore.activeConversation); if (activeConversation) { await updateDiscordRpc( activeConversation.name, config.model || "claude", activeConversation.startedAt ); } claudeStore.addLine("system", "New conversation started!"); characterState.setState("idle"); } catch (error) { claudeStore.addLine("error", `Failed to start new conversation: ${error}`); characterState.setTemporaryState("error", 3000); } } export const slashCommands: SlashCommand[] = [ { name: "cd", description: "Change the working directory", usage: "/cd ", execute: changeDirectory, }, { name: "clear", description: "Clear the terminal display (keeps conversation context)", usage: "/clear", execute: () => { claudeStore.clearTerminal(); claudeStore.addLine("system", "Terminal cleared"); }, }, { name: "new", description: "Start a fresh conversation (resets context)", usage: "/new", execute: startNewConversation, }, { name: "help", description: "Show available slash commands", usage: "/help", execute: () => { const helpText = slashCommands .map((cmd) => ` ${cmd.usage.padEnd(12)} - ${cmd.description}`) .join("\n"); claudeStore.addLine("system", `Available commands:\n${helpText}`); }, }, { name: "search", description: "Search within the conversation (use /search to clear)", usage: "/search [query]", execute: (args: string) => { if (!args.trim()) { searchState.clear(); claudeStore.addLine("system", "Search cleared"); return; } searchState.setQuery(args.trim()); claudeStore.addLine("system", `Searching for: "${args.trim()}"`); }, }, { name: "summarise", description: "Get a summary of the entire conversation", usage: "/summarise", execute: async () => { const conversationId = get(claudeStore.activeConversationId); if (!conversationId) { claudeStore.addLine("error", "No active conversation"); return; } try { claudeStore.addLine("system", "Requesting conversation summary..."); await invoke("send_prompt", { conversationId, message: "Please provide a comprehensive summary of our entire conversation so far, including the key topics we've discussed, decisions made, and any important context.", }); } catch (error) { claudeStore.addLine("error", `Failed to request summary: ${error}`); } }, }, { name: "skill", description: "Invoke a Claude Code skill from ~/.claude/skills/", usage: "/skill [name] [data]", execute: async (args: string) => { const conversationId = get(claudeStore.activeConversationId); if (!conversationId) { claudeStore.addLine("error", "No active conversation"); return; } const parts = args.trim().split(/\s+/); const skillName = parts[0]; const skillData = parts.slice(1).join(" "); // If no skill name provided, list available skills if (!skillName) { try { const skills = await invoke("list_skills"); if (skills.length === 0) { claudeStore.addLine( "system", "No skills found in ~/.claude/skills/\nCreate a skill by adding a folder with a SKILL.md file." ); } else { const skillList = skills.map((s) => ` • ${s}`).join("\n"); claudeStore.addLine( "system", `Available skills:\n${skillList}\n\nUsage: /skill [data]` ); } } catch (error) { claudeStore.addLine("error", `Failed to list skills: ${error}`); } return; } try { claudeStore.addLine("system", `Invoking skill: ${skillName}`); characterState.setState("thinking"); const message = skillData ? `Please run the /${skillName} skill with the following data:\n\n${skillData}` : `Please run the /${skillName} skill.`; await invoke("send_prompt", { conversationId, message, }); } catch (error) { claudeStore.addLine("error", `Failed to invoke skill: ${error}`); characterState.setTemporaryState("error", 3000); } }, }, ]; export function parseSlashCommand(input: string): { command: SlashCommand | null; args: string; } { const trimmed = input.trim(); if (!trimmed.startsWith("/")) { return { command: null, args: "" }; } const parts = trimmed.slice(1).split(/\s+/); const commandName = parts[0]?.toLowerCase(); const args = parts.slice(1).join(" "); const command = slashCommands.find((cmd) => cmd.name.toLowerCase() === commandName); return { command: command || null, args }; } export function getMatchingCommands(input: string): SlashCommand[] { const trimmed = input.trim(); if (!trimmed.startsWith("/")) { return []; } const partial = trimmed.slice(1).toLowerCase(); if (partial === "") { return slashCommands; } return slashCommands.filter((cmd) => cmd.name.toLowerCase().startsWith(partial)); } export function isSlashCommand(input: string): boolean { return input.trim().startsWith("/"); }