diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 1bafa39..6862fa8 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -25,6 +25,12 @@ pub async fn stop_claude(app: AppHandle, bridge: State<'_, SharedBridge>) -> Res Ok(()) } +#[tauri::command] +pub async fn interrupt_claude(app: AppHandle, bridge: State<'_, SharedBridge>) -> Result<(), String> { + let mut bridge = bridge.lock(); + bridge.interrupt(&app) +} + #[tauri::command] pub async fn send_prompt(bridge: State<'_, SharedBridge>, message: String) -> Result<(), String> { let mut bridge = bridge.lock(); diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index 8aaf580..a801550 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -19,6 +19,9 @@ pub struct ClaudeStartOptions { #[serde(default)] pub allowed_tools: Vec, + + #[serde(default)] + pub skip_greeting: bool, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index d7646eb..c595bbf 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -32,6 +32,7 @@ pub fn run() { .invoke_handler(tauri::generate_handler![ start_claude, stop_claude, + interrupt_claude, send_prompt, is_claude_running, get_working_directory, diff --git a/src-tauri/src/wsl_bridge.rs b/src-tauri/src/wsl_bridge.rs index b3744f2..12e78ba 100644 --- a/src-tauri/src/wsl_bridge.rs +++ b/src-tauri/src/wsl_bridge.rs @@ -332,6 +332,30 @@ impl WslBridge { Ok(()) } + pub fn interrupt(&mut self, app: &AppHandle) -> Result<(), String> { + // Due to persistent bug in Claude Code where ESC/Ctrl+C doesn't work, + // we have to kill the process. This is the only reliable way to stop it. + // See: https://github.com/anthropics/claude-code/issues/3455 + if let Some(mut process) = self.process.take() { + // Kill the process immediately + let _ = process.kill(); + let _ = process.wait(); + + // Clear stdin + self.stdin = None; + + // Keep session_id and working directory for user reference + // The user will see what session was interrupted + + // Emit disconnected status + emit_connection_status(app, ConnectionStatus::Disconnected); + + Ok(()) + } else { + Err("No active process to interrupt".to_string()) + } + } + pub fn stop(&mut self, app: &AppHandle) { if let Some(mut process) = self.process.take() { let _ = process.kill(); diff --git a/src/lib/components/InputBar.svelte b/src/lib/components/InputBar.svelte index 3517c19..14af289 100644 --- a/src/lib/components/InputBar.svelte +++ b/src/lib/components/InputBar.svelte @@ -1,17 +1,33 @@ -
-
- -
+ > + - {:else} - Send + {/if} - +
+ + diff --git a/src/lib/components/MessageModeSelector.svelte b/src/lib/components/MessageModeSelector.svelte new file mode 100644 index 0000000..1088424 --- /dev/null +++ b/src/lib/components/MessageModeSelector.svelte @@ -0,0 +1,157 @@ + + + + +
+ + + {#if isOpen} + + {/if} +
+ + diff --git a/src/lib/stores/claude.ts b/src/lib/stores/claude.ts index 29931f2..1f89cab 100644 --- a/src/lib/stores/claude.ts +++ b/src/lib/stores/claude.ts @@ -1,6 +1,14 @@ import { derived } from "svelte/store"; import { conversationsStore } from "./conversations"; import type { ConnectionStatus, PermissionRequest, TerminalLine } from "$lib/types/messages"; +import { characterState } from "$lib/stores/character"; +import { + setShouldRestoreHistory, + setSavedHistory, + getShouldRestoreHistory, + getSavedHistory, + clearHistoryRestore, +} from "./historyRestore"; // Re-export TerminalLine type for backwards compatibility export type { TerminalLine }; @@ -51,6 +59,12 @@ export const claudeStore = { return tools; }, + // History restoration methods from main branch + setShouldRestoreHistory: setShouldRestoreHistory, + setSavedConversationHistory: setSavedHistory, + getShouldRestoreHistory: getShouldRestoreHistory, + getSavedConversationHistory: getSavedHistory, + reset: () => { // Reset only the active conversation conversationsStore.clearTerminal(); @@ -58,6 +72,8 @@ export const claudeStore = { conversationsStore.setWorkingDirectory(""); conversationsStore.setProcessing(false); conversationsStore.revokeAllTools(); + // Also clear history restoration + clearHistoryRestore(); }, }; @@ -65,3 +81,15 @@ export const hasPermissionPending = derived( claudeStore.pendingPermission, ($permission) => $permission !== null ); + +// Derived store to check if Claude is currently processing (can be interrupted) +export const isClaudeProcessing = derived( + [claudeStore.connectionStatus, characterState], + ([$connectionStatus, $characterState]) => { + // Must be connected and in one of the processing states + return ( + $connectionStatus === "connected" && + ["thinking", "typing", "searching", "coding", "mcp"].includes($characterState) + ); + } +); diff --git a/src/lib/stores/historyRestore.ts b/src/lib/stores/historyRestore.ts new file mode 100644 index 0000000..a984b59 --- /dev/null +++ b/src/lib/stores/historyRestore.ts @@ -0,0 +1,29 @@ +// Separate module for history restoration to ensure persistence across reconnects +let shouldRestore = false; +let savedHistory: string | null = null; + +export function setShouldRestoreHistory(should: boolean) { + shouldRestore = should; + console.log("Setting shouldRestoreHistory to:", should); +} + +export function setSavedHistory(history: string | null) { + savedHistory = history; + console.log("Setting savedHistory, length:", history?.length || 0); +} + +export function getShouldRestoreHistory(): boolean { + console.log("Getting shouldRestoreHistory:", shouldRestore); + return shouldRestore; +} + +export function getSavedHistory(): string | null { + console.log("Getting savedHistory, length:", savedHistory?.length || 0); + return savedHistory; +} + +export function clearHistoryRestore() { + console.log("Clearing history restore flags"); + shouldRestore = false; + savedHistory = null; +} diff --git a/src/lib/stores/messageMode.ts b/src/lib/stores/messageMode.ts new file mode 100644 index 0000000..2936080 --- /dev/null +++ b/src/lib/stores/messageMode.ts @@ -0,0 +1,20 @@ +import { writable } from "svelte/store"; + +// Default to chat mode +const messageModeStore = writable("chat"); + +export const messageMode = { + subscribe: messageModeStore.subscribe, + set: (mode: string) => { + console.log("Setting message mode to:", mode); + messageModeStore.set(mode); + }, + reset: () => messageModeStore.set("chat"), +}; + +// Helper to get current mode +export function getCurrentMode(): string { + let currentMode = "chat"; + messageMode.subscribe((mode) => (currentMode = mode))(); + return currentMode; +} diff --git a/src/lib/tauri.ts b/src/lib/tauri.ts index 406ecf0..0eeb0cc 100644 --- a/src/lib/tauri.ts +++ b/src/lib/tauri.ts @@ -21,6 +21,11 @@ interface StateChangePayload { let hasConnectedThisSession = false; let unlisteners: Array<() => void> = []; +let skipNextGreeting = false; + +export function setSkipNextGreeting(skip: boolean) { + skipNextGreeting = skip; +} function getTimeOfDay(): string { const hour = new Date().getHours(); @@ -42,6 +47,12 @@ function generateGreetingPrompt(): string { } async function sendGreeting() { + // Check if we should skip this greeting + if (skipNextGreeting) { + skipNextGreeting = false; // Reset the flag + return; + } + const config = configStore.getConfig(); if (!config.greeting_enabled) { @@ -100,8 +111,16 @@ export async function initializeTauriListeners() { await sendGreeting(); } } else if (status === "disconnected") { - hasConnectedThisSession = false; - claudeStore.addLine("system", "Disconnected from Claude Code"); + // Only reset session flag if we're not about to reconnect + if (!skipNextGreeting) { + hasConnectedThisSession = false; + } + + // Don't add system message if we're about to reconnect + if (!skipNextGreeting) { + claudeStore.addLine("system", "Disconnected from Claude Code"); + } + characterState.setState("idle"); } else if (status === "error") { hasConnectedThisSession = false; diff --git a/src/lib/types/messageMode.ts b/src/lib/types/messageMode.ts new file mode 100644 index 0000000..2930562 --- /dev/null +++ b/src/lib/types/messageMode.ts @@ -0,0 +1,69 @@ +export interface MessageMode { + id: string; + name: string; + description: string; + prefix?: string; + icon: string; +} + +export const MESSAGE_MODES: MessageMode[] = [ + { + id: "chat", + name: "Chat", + description: "Normal conversation mode", + icon: "💬", + }, + { + id: "architect", + name: "Architect", + description: "High-level design and architecture planning", + prefix: "[Architect Mode] ", + icon: "🏗️", + }, + { + id: "code", + name: "Code", + description: "Focused on writing and editing code", + prefix: "[Code Mode] ", + icon: "💻", + }, + { + id: "debug", + name: "Debug", + description: "Help with debugging and troubleshooting", + prefix: "[Debug Mode] ", + icon: "🐛", + }, + { + id: "ask", + name: "Ask", + description: "Technical questions and explanations", + prefix: "[Ask Mode] ", + icon: "❓", + }, + { + id: "review", + name: "Review", + description: "Code review and feedback", + prefix: "[Review Mode] ", + icon: "👀", + }, +]; + +export function getMessageMode(id: string): MessageMode | undefined { + return MESSAGE_MODES.find((mode) => mode.id === id); +} + +export function formatMessageWithMode(message: string, modeId: string): string { + const mode = getMessageMode(modeId); + if (!mode || !mode.prefix) { + return message; + } + + // Don't double-prefix if the message already starts with the prefix + if (message.startsWith(mode.prefix)) { + return message; + } + + return mode.prefix + message; +}