diff --git a/src/lib/commands/slashCommands.ts b/src/lib/commands/slashCommands.ts new file mode 100644 index 0000000..cfab588 --- /dev/null +++ b/src/lib/commands/slashCommands.ts @@ -0,0 +1,139 @@ +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 } from "$lib/tauri"; + +export interface SlashCommand { + name: string; + description: string; + usage: string; + execute: (args: string) => Promise | void; +} + +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, + }, + }); + + 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: "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: "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}`); + } + }, + }, +]; + +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("/"); +} diff --git a/src/lib/components/InputBar.svelte b/src/lib/components/InputBar.svelte index 5d78367..302d877 100644 --- a/src/lib/components/InputBar.svelte +++ b/src/lib/components/InputBar.svelte @@ -13,13 +13,23 @@ clearHistoryRestore, } from "$lib/stores/historyRestore"; import MessageModeSelector from "$lib/components/MessageModeSelector.svelte"; + import SlashCommandMenu from "$lib/components/SlashCommandMenu.svelte"; import { getCurrentMode } from "$lib/stores/messageMode"; import { formatMessageWithMode } from "$lib/types/messageMode"; + import { + parseSlashCommand, + getMatchingCommands, + isSlashCommand, + type SlashCommand, + } from "$lib/commands/slashCommands"; let inputValue = $state(""); let isSubmitting = $state(false); let isConnected = $state(false); let isProcessing = $state(false); + let showCommandMenu = $state(false); + let matchingCommands = $state([]); + let selectedCommandIndex = $state(0); claudeStore.connectionStatus.subscribe((status) => { isConnected = status === "connected"; @@ -29,11 +39,56 @@ isProcessing = processing; }); + function handleInputChange() { + if (isSlashCommand(inputValue)) { + matchingCommands = getMatchingCommands(inputValue); + showCommandMenu = matchingCommands.length > 0; + selectedCommandIndex = 0; + } else { + showCommandMenu = false; + matchingCommands = []; + } + } + + function selectCommand(command: SlashCommand) { + inputValue = `/${command.name} `; + showCommandMenu = false; + matchingCommands = []; + } + + async function executeSlashCommand(): Promise { + const { command, args } = parseSlashCommand(inputValue); + if (command) { + inputValue = ""; + showCommandMenu = false; + matchingCommands = []; + await command.execute(args); + return true; + } + return false; + } + async function handleSubmit(event: Event) { event.preventDefault(); const message = inputValue.trim(); - if (!message || isSubmitting || !isConnected) return; + if (!message || isSubmitting) return; + + // Check for slash commands first (these work even when disconnected) + if (isSlashCommand(message)) { + const wasCommand = await executeSlashCommand(); + if (wasCommand) return; + // If it started with / but wasn't a valid command, show error + claudeStore.addLine( + "error", + `Unknown command: ${message.split(" ")[0]}. Type /help for available commands.` + ); + inputValue = ""; + return; + } + + // Regular messages require connection + if (!isConnected) return; isSubmitting = true; inputValue = ""; @@ -139,6 +194,34 @@ User: ${formattedMessage}`; } function handleKeyDown(event: KeyboardEvent) { + // Handle command menu navigation + if (showCommandMenu && matchingCommands.length > 0) { + if (event.key === "ArrowDown") { + event.preventDefault(); + selectedCommandIndex = (selectedCommandIndex + 1) % matchingCommands.length; + return; + } + if (event.key === "ArrowUp") { + event.preventDefault(); + selectedCommandIndex = + (selectedCommandIndex - 1 + matchingCommands.length) % matchingCommands.length; + return; + } + if (event.key === "Tab") { + event.preventDefault(); + const selected = matchingCommands[selectedCommandIndex]; + if (selected) { + selectCommand(selected); + } + return; + } + if (event.key === "Escape") { + event.preventDefault(); + showCommandMenu = false; + return; + } + } + if (event.key === "Enter" && !event.shiftKey) { handleSubmit(event); } @@ -152,11 +235,19 @@ User: ${formattedMessage}`;
+