diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 9854341..1be5dcd 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1636,7 +1636,7 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hikari-desktop" -version = "1.3.0" +version = "1.4.0" dependencies = [ "chrono", "dirs 5.0.1", diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index a7a7827..70e8b60 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -28,6 +28,18 @@ "identifier": "fs:allow-write-file", "allow": [{ "path": "**" }] }, + { + "identifier": "fs:scope", + "allow": [ + { "path": "$HOME/.claude/**" } + ] + }, + { + "identifier": "fs:allow-read-text-file", + "allow": [ + { "path": "$HOME/.claude/**" } + ] + }, "core:window:allow-set-size", "core:window:allow-set-always-on-top", "core:window:allow-inner-size", diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 69c418c..8dd6082 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -1167,6 +1167,76 @@ pub async fn close_application(app_handle: AppHandle) -> Result<(), String> { Ok(()) } +#[derive(serde::Serialize)] +pub struct MemoryFilesResponse { + pub files: Vec, +} + +#[tauri::command] +pub async fn list_memory_files() -> Result { + use std::fs; + + // Get the .claude directory in the user's home + let home_dir = match dirs::home_dir() { + Some(dir) => dir, + None => return Err("Could not find home directory".to_string()), + }; + + let claude_dir = home_dir.join(".claude"); + let projects_dir = claude_dir.join("projects"); + + if !projects_dir.exists() { + return Ok(MemoryFilesResponse { files: Vec::new() }); + } + + let mut memory_files = Vec::new(); + + // Recursively find all memory directories + fn find_memory_files(dir: &std::path::Path, files: &mut Vec) -> std::io::Result<()> { + if !dir.is_dir() { + return Ok(()); + } + + for entry in fs::read_dir(dir)? { + let entry = entry?; + let path = entry.path(); + + if path.is_dir() { + // Check if this is a "memory" directory + if path.file_name().and_then(|n| n.to_str()) == Some("memory") { + // List all files in the memory directory + for mem_entry in fs::read_dir(&path)? { + let mem_entry = mem_entry?; + let mem_path = mem_entry.path(); + + if mem_path.is_file() { + if let Some(path_str) = mem_path.to_str() { + files.push(path_str.to_string()); + } + } + } + } else { + // Recurse into subdirectories + find_memory_files(&path, files)?; + } + } + } + + Ok(()) + } + + if let Err(e) = find_memory_files(&projects_dir, &mut memory_files) { + return Err(format!("Failed to list memory files: {}", e)); + } + + // Sort files alphabetically + memory_files.sort(); + + Ok(MemoryFilesResponse { + files: memory_files, + }) +} + #[cfg(test)] mod tests { use super::*; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 54a0969..1f7d9a3 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -197,6 +197,7 @@ pub fn run() { stop_discord_rpc, log_discord_rpc, close_application, + list_memory_files, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/src/wsl_bridge.rs b/src-tauri/src/wsl_bridge.rs index 8f877c7..bf0f3de 100644 --- a/src-tauri/src/wsl_bridge.rs +++ b/src-tauri/src/wsl_bridge.rs @@ -1505,10 +1505,21 @@ fn get_tool_state(tool_name: &str) -> CharacterState { } fn format_tool_description(name: &str, input: &serde_json::Value) -> String { + // Helper function to check if a path is a memory file + fn is_memory_path(path: &str) -> bool { + path.contains("/.claude/") && (path.contains("/memory/") || path.ends_with("/MEMORY.md")) + } + match name { "Read" => { if let Some(path) = input.get("file_path").and_then(|v| v.as_str()) { - format!("Reading file: {}", path) + if is_memory_path(path) { + // Extract just the filename for cleaner display + let filename = path.split('/').last().unwrap_or(path); + format!("📝 Reading memory: {}", filename) + } else { + format!("Reading file: {}", path) + } } else { "Reading file...".to_string() } @@ -1527,9 +1538,26 @@ fn format_tool_description(name: &str, input: &serde_json::Value) -> String { "Searching in files...".to_string() } } - "Edit" | "Write" => { + "Edit" => { if let Some(path) = input.get("file_path").and_then(|v| v.as_str()) { - format!("Editing: {}", path) + if is_memory_path(path) { + let filename = path.split('/').last().unwrap_or(path); + format!("💾 Updating memory: {}", filename) + } else { + format!("Editing: {}", path) + } + } else { + "Editing file...".to_string() + } + } + "Write" => { + if let Some(path) = input.get("file_path").and_then(|v| v.as_str()) { + if is_memory_path(path) { + let filename = path.split('/').last().unwrap_or(path); + format!("💾 Writing memory: {}", filename) + } else { + format!("Editing: {}", path) + } } else { "Editing file...".to_string() } @@ -1714,6 +1742,39 @@ mod tests { assert_eq!(desc, "Using tool: CustomTool"); } + #[test] + fn test_format_tool_description_memory_read() { + let input = + serde_json::json!({"file_path": "/home/user/.claude/projects/test/memory/MEMORY.md"}); + let desc = format_tool_description("Read", &input); + assert_eq!(desc, "📝 Reading memory: MEMORY.md"); + } + + #[test] + fn test_format_tool_description_memory_write() { + let input = serde_json::json!( + {"file_path": "/home/user/.claude/projects/test/memory/notes.md"} + ); + let desc = format_tool_description("Write", &input); + assert_eq!(desc, "💾 Writing memory: notes.md"); + } + + #[test] + fn test_format_tool_description_memory_edit() { + let input = serde_json::json!( + {"file_path": "/home/user/.claude/projects/test/memory/patterns.md"} + ); + let desc = format_tool_description("Edit", &input); + assert_eq!(desc, "💾 Updating memory: patterns.md"); + } + + #[test] + fn test_format_tool_description_non_memory_read() { + let input = serde_json::json!({"file_path": "/home/user/code/test.txt"}); + let desc = format_tool_description("Read", &input); + assert_eq!(desc, "Reading file: /home/user/code/test.txt"); + } + #[test] fn test_wsl_bridge_new() { let bridge = WslBridge::new(); diff --git a/src/lib/components/MemoryBrowserPanel.svelte b/src/lib/components/MemoryBrowserPanel.svelte new file mode 100644 index 0000000..1e19400 --- /dev/null +++ b/src/lib/components/MemoryBrowserPanel.svelte @@ -0,0 +1,456 @@ + + + + +{#if isPanelOpen} +
+
+
+ + + +

Memory Files

+
+ +
+ +
+ {#if isLoading && memoryFiles.length === 0} +
+ + + + Loading memory files... +
+ {:else if error} +
+

{error}

+ +
+ {:else if memoryFiles.length === 0} +
+

No memory files found.

+

Memory files are created automatically as I learn from our conversations!

+
+ {:else} +
+
+ {#each memoryFiles as file} + + {/each} +
+ +
+ {#if selectedFile && fileContent} +
+

{getFileName(selectedFile)}

+
+
+ +
+ {:else if selectedFile && isLoading} +
+ + + + Loading file... +
+ {:else} +
+

Select a memory file to view its contents

+
+ {/if} +
+
+ {/if} +
+
+{/if} + + diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 08c6f66..7cab3e8 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -33,6 +33,7 @@ import AchievementsPanel from "$lib/components/AchievementsPanel.svelte"; import UpdateNotification from "$lib/components/UpdateNotification.svelte"; import CloseAppConfirmModal from "$lib/components/CloseAppConfirmModal.svelte"; + import MemoryBrowserPanel from "$lib/components/MemoryBrowserPanel.svelte"; import { debugConsoleStore } from "$lib/stores/debugConsole"; let initialized = false; @@ -513,6 +514,7 @@ +