feat: add memory system activity display

Implements issue #118 to display auto-memory system operations and
provide a browser for viewing memory files.

Backend changes:
- Detect memory file operations in format_tool_description()
- Add emoji icons (📝/💾) for memory Read/Write/Edit operations
- Add list_memory_files() Tauri command to find all memory files
- Add 4 new tests for memory detection logic
- Update Tauri capabilities to allow reading .claude directory

Frontend changes:
- Create MemoryBrowserPanel component with file list and viewer
- Add memory panel to main app layout
- Support Markdown rendering of memory file contents
- Add loading states and error handling

Testing:
- All 354 Rust tests passing
- TypeScript type-checks pass

Co-Authored-By: Hikari <hikari@nhcarrigan.com>
This commit is contained in:
2026-02-07 12:01:31 -08:00
committed by Naomi Carrigan
parent e40ae989f7
commit 89e99b1524
7 changed files with 606 additions and 4 deletions
+70
View File
@@ -1167,6 +1167,76 @@ pub async fn close_application(app_handle: AppHandle) -> Result<(), String> {
Ok(())
}
#[derive(serde::Serialize)]
pub struct MemoryFilesResponse {
pub files: Vec<String>,
}
#[tauri::command]
pub async fn list_memory_files() -> Result<MemoryFilesResponse, String> {
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<String>) -> 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::*;
+1
View File
@@ -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");
+64 -3
View File
@@ -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();