mirror of
https://github.com/mfakbar127/Claude-Samurai
synced 2026-06-21 13:57:42 +00:00
improve memory page
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
# Coding Style
|
||||
You are an expert software engineer who values simplicity, readability, and minimal code.
|
||||
|
||||
## Core Principles
|
||||
* **Prioritize Clarity and Simplicity**: Write the simplest, most direct code that solves the problem.
|
||||
* **Avoid Over-engineering**: Do not add unnecessary abstractions, comments, or boilerplate.
|
||||
* **Focus on the Task**: Only output the requested code. Avoid lengthy explanations or conversational text in your final response unless specifically asked.
|
||||
* **Follow Conventions**: Adhere to standard conventions for the specific programming language you are using.
|
||||
* **Be Concise**: Use "modify in place, no new files, keep it minimal" instructions for refactoring tasks to guide the model to make only necessary changes.
|
||||
|
||||
### Code Change Guidelines
|
||||
|
||||
- **Full file read before edits**: Before editing any file, read it in full first to ensure complete context; partial reads lead to corrupted edits
|
||||
- **Minimize diffs**: Prefer the smallest change that satisfies the request. Avoid unrelated refactors or style rewrites unless necessary for correctness
|
||||
- **Fail fast**: Write code with fail-fast logic by default. Do not swallow exceptions with errors or warnings
|
||||
- **No fallback logic**: Do not add fallback logic unless explicitly told to and agreed with the user
|
||||
- **No guessing**: Do not say "The issue is..." before you actually know what the issue is. Investigate first.
|
||||
|
||||
|
||||
## Output Format
|
||||
* Respond only with the code block(s) for the requested task.
|
||||
* If explanations are necessary, keep them brief and place them outside the code block.
|
||||
* Do not add extensive headers, footers, or markdown that is not strictly necessary for presenting the code.
|
||||
+366
-19
@@ -1584,6 +1584,112 @@ pub struct MemoryFile {
|
||||
pub exists: bool,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug)]
|
||||
pub struct MemoryEntry {
|
||||
pub name: String,
|
||||
pub path: String,
|
||||
pub content: String,
|
||||
pub exists: bool,
|
||||
#[serde(rename = "source")]
|
||||
pub source: String, // "global" | "project"
|
||||
#[serde(rename = "projectPath", skip_serializing_if = "Option::is_none")]
|
||||
pub project_path: Option<String>,
|
||||
pub disabled: bool,
|
||||
}
|
||||
|
||||
fn global_memory_paths(home_dir: &std::path::Path) -> (std::path::PathBuf, std::path::PathBuf) {
|
||||
let active = home_dir.join(".claude/CLAUDE.md");
|
||||
let disabled = home_dir.join(".claude/CLAUDE.md.disabled");
|
||||
(active, disabled)
|
||||
}
|
||||
|
||||
fn project_memory_paths(project_path: &str) -> (std::path::PathBuf, std::path::PathBuf) {
|
||||
let base = std::path::Path::new(project_path);
|
||||
let active = base.join("CLAUDE.md");
|
||||
let disabled = base.join("CLAUDE.md.disabled");
|
||||
(active, disabled)
|
||||
}
|
||||
|
||||
fn read_memory_entry_from_paths(
|
||||
active_path: &std::path::Path,
|
||||
disabled_path: &std::path::Path,
|
||||
name: String,
|
||||
source: String,
|
||||
project_path: Option<String>,
|
||||
) -> Result<MemoryEntry, String> {
|
||||
let (content_path, disabled) = if active_path.is_file() {
|
||||
(active_path, false)
|
||||
} else if disabled_path.is_file() {
|
||||
(disabled_path, true)
|
||||
} else {
|
||||
// No file yet – treat as non-existing, empty memory
|
||||
return Ok(MemoryEntry {
|
||||
name,
|
||||
path: path_to_string(active_path),
|
||||
content: String::new(),
|
||||
exists: false,
|
||||
source,
|
||||
project_path,
|
||||
disabled: false,
|
||||
});
|
||||
};
|
||||
|
||||
let content = std::fs::read_to_string(content_path).map_err(|e| {
|
||||
format!(
|
||||
"Failed to read memory file {}: {}",
|
||||
content_path.display(),
|
||||
e
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(MemoryEntry {
|
||||
name,
|
||||
path: path_to_string(content_path),
|
||||
content,
|
||||
exists: true,
|
||||
source,
|
||||
project_path,
|
||||
disabled,
|
||||
})
|
||||
}
|
||||
|
||||
fn get_project_paths_for_memory(
|
||||
home_dir: &std::path::Path,
|
||||
) -> Result<Vec<String>, String> {
|
||||
let claude_json_path = home_dir.join(".claude.json");
|
||||
|
||||
if !claude_json_path.exists() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
let json_value = read_json_file(&claude_json_path, ".claude.json")?;
|
||||
|
||||
let projects_obj = json_value
|
||||
.get("projects")
|
||||
.and_then(|projects| projects.as_object())
|
||||
.cloned()
|
||||
.unwrap_or_else(serde_json::Map::new);
|
||||
|
||||
Ok(projects_obj.keys().cloned().collect())
|
||||
}
|
||||
|
||||
fn resolve_memory_paths(
|
||||
source: &str,
|
||||
home_dir: &std::path::Path,
|
||||
project_path: &Option<String>,
|
||||
) -> Result<(std::path::PathBuf, std::path::PathBuf), String> {
|
||||
match source {
|
||||
"global" => Ok(global_memory_paths(home_dir)),
|
||||
"project" => {
|
||||
let project = project_path
|
||||
.as_ref()
|
||||
.ok_or_else(|| "Project path is required for project memory".to_string())?;
|
||||
Ok(project_memory_paths(project))
|
||||
}
|
||||
_ => Err("Unsupported source for memory file".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn read_claude_memory() -> Result<MemoryFile, String> {
|
||||
let home_dir = home_dir()?;
|
||||
@@ -1612,16 +1718,187 @@ pub async fn read_claude_memory() -> Result<MemoryFile, String> {
|
||||
#[tauri::command]
|
||||
pub async fn write_claude_memory(content: String) -> Result<(), String> {
|
||||
let home_dir = home_dir()?;
|
||||
let claude_md_path = home_dir.join(".claude/CLAUDE.md");
|
||||
let (active_path, disabled_path) = global_memory_paths(&home_dir);
|
||||
|
||||
// Ensure .claude directory exists
|
||||
if let Some(parent) = claude_md_path.parent() {
|
||||
if let Some(parent) = active_path.parent() {
|
||||
ensure_dir(parent, ".claude directory")?;
|
||||
}
|
||||
|
||||
std::fs::write(&claude_md_path, content)
|
||||
// Always write enabled global memory for this legacy command
|
||||
std::fs::write(&active_path, content)
|
||||
.map_err(|e| format!("Failed to write CLAUDE.md file: {}", e))?;
|
||||
|
||||
// Remove disabled file if it exists to keep state consistent
|
||||
if disabled_path.exists() {
|
||||
std::fs::remove_file(&disabled_path)
|
||||
.map_err(|e| format!("Failed to remove disabled CLAUDE.md file: {}", e))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_claude_memory_files() -> Result<Vec<MemoryEntry>, String> {
|
||||
let home_dir = home_dir()?;
|
||||
let mut entries = Vec::new();
|
||||
|
||||
// Global memory – always include an entry to mirror previous behavior
|
||||
let (global_active, global_disabled) = global_memory_paths(&home_dir);
|
||||
let global_entry = read_memory_entry_from_paths(
|
||||
&global_active,
|
||||
&global_disabled,
|
||||
"global".to_string(),
|
||||
"global".to_string(),
|
||||
None,
|
||||
)?;
|
||||
entries.push(global_entry);
|
||||
|
||||
// Project memories – based on .claude.json projects keys
|
||||
let project_paths = get_project_paths_for_memory(&home_dir)?;
|
||||
for project_path in project_paths {
|
||||
let (active, disabled) = project_memory_paths(&project_path);
|
||||
|
||||
// Only include project entries if a file actually exists
|
||||
if !active.is_file() && !disabled.is_file() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let name = std::path::Path::new(&project_path)
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or(&project_path)
|
||||
.to_string();
|
||||
|
||||
let entry = read_memory_entry_from_paths(
|
||||
&active,
|
||||
&disabled,
|
||||
name,
|
||||
"project".to_string(),
|
||||
Some(project_path.clone()),
|
||||
)?;
|
||||
|
||||
entries.push(entry);
|
||||
}
|
||||
|
||||
// Sort: global first, then projects by name
|
||||
entries.sort_by(|a, b| {
|
||||
let order_a = if a.source == "global" { 0 } else { 1 };
|
||||
let order_b = if b.source == "global" { 0 } else { 1 };
|
||||
|
||||
if order_a != order_b {
|
||||
order_a.cmp(&order_b)
|
||||
} else {
|
||||
a.name.cmp(&b.name)
|
||||
}
|
||||
});
|
||||
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn write_claude_memory_file(
|
||||
source: String,
|
||||
project_path: Option<String>,
|
||||
content: String,
|
||||
disabled: bool,
|
||||
) -> Result<(), String> {
|
||||
let home_dir = home_dir()?;
|
||||
|
||||
let (active_path, disabled_path) =
|
||||
resolve_memory_paths(source.as_str(), &home_dir, &project_path)?;
|
||||
|
||||
// Ensure parent directory exists
|
||||
if let Some(parent) = active_path.parent() {
|
||||
ensure_dir(parent, "memory directory")?;
|
||||
}
|
||||
|
||||
if disabled {
|
||||
// Write to disabled path and remove active if it exists
|
||||
std::fs::write(&disabled_path, content)
|
||||
.map_err(|e| format!("Failed to write disabled memory file: {}", e))?;
|
||||
if active_path.exists() {
|
||||
std::fs::remove_file(&active_path)
|
||||
.map_err(|e| format!("Failed to remove active memory file: {}", e))?;
|
||||
}
|
||||
} else {
|
||||
// Write to active path and remove disabled if it exists
|
||||
std::fs::write(&active_path, content)
|
||||
.map_err(|e| format!("Failed to write memory file: {}", e))?;
|
||||
if disabled_path.exists() {
|
||||
std::fs::remove_file(&disabled_path)
|
||||
.map_err(|e| format!("Failed to remove disabled memory file: {}", e))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn toggle_claude_memory_file(
|
||||
source: String,
|
||||
project_path: Option<String>,
|
||||
disabled: bool,
|
||||
) -> Result<(), String> {
|
||||
let home_dir = home_dir()?;
|
||||
|
||||
let (active_path, disabled_path) =
|
||||
resolve_memory_paths(source.as_str(), &home_dir, &project_path)?;
|
||||
|
||||
let (from, to) = if disabled {
|
||||
// Disable: rename active -> disabled
|
||||
(&active_path, &disabled_path)
|
||||
} else {
|
||||
// Enable: rename disabled -> active
|
||||
(&disabled_path, &active_path)
|
||||
};
|
||||
|
||||
if !from.exists() {
|
||||
return Err(format!(
|
||||
"Memory file {} does not exist",
|
||||
from.display()
|
||||
));
|
||||
}
|
||||
|
||||
std::fs::rename(from, to)
|
||||
.map_err(|e| format!("Failed to toggle memory file: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn delete_claude_memory_file(
|
||||
source: String,
|
||||
project_path: Option<String>,
|
||||
) -> Result<(), String> {
|
||||
let home_dir = home_dir()?;
|
||||
|
||||
let (active_path, disabled_path) =
|
||||
resolve_memory_paths(source.as_str(), &home_dir, &project_path)?;
|
||||
|
||||
let mut removed_any = false;
|
||||
|
||||
if active_path.exists() {
|
||||
std::fs::remove_file(&active_path)
|
||||
.map_err(|e| format!("Failed to delete memory file {}: {}", active_path.display(), e))?;
|
||||
removed_any = true;
|
||||
}
|
||||
|
||||
if disabled_path.exists() {
|
||||
std::fs::remove_file(&disabled_path).map_err(|e| {
|
||||
format!(
|
||||
"Failed to delete disabled memory file {}: {}",
|
||||
disabled_path.display(),
|
||||
e
|
||||
)
|
||||
})?;
|
||||
removed_any = true;
|
||||
}
|
||||
|
||||
if !removed_any {
|
||||
return Err("No memory file found to delete".to_string());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2808,6 +3085,7 @@ pub struct AgentFile {
|
||||
pub name: String,
|
||||
pub content: String,
|
||||
pub exists: bool,
|
||||
pub disabled: bool,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
@@ -2834,28 +3112,56 @@ pub async fn read_claude_agents() -> Result<Vec<AgentFile>, String> {
|
||||
|
||||
let mut agent_files = Vec::new();
|
||||
|
||||
// Read all .md files in the agents directory
|
||||
// Read all .md and .md.disabled files in the agents directory
|
||||
let entries = std::fs::read_dir(&agents_dir)
|
||||
.map_err(|e| format!("Failed to read agents directory: {}", e))?;
|
||||
|
||||
for entry in entries {
|
||||
let entry = entry.map_err(|e| format!("Failed to read directory entry: {}", e))?;
|
||||
let entry =
|
||||
entry.map_err(|e| format!("Failed to read directory entry: {}", e))?;
|
||||
let path = entry.path();
|
||||
|
||||
if path.is_file() && path.extension().map(|ext| ext == "md").unwrap_or(false) {
|
||||
let file_name = path.file_stem()
|
||||
if path.is_file() {
|
||||
let file_name_str = path
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
.unwrap_or("");
|
||||
|
||||
let content = std::fs::read_to_string(&path)
|
||||
.map_err(|e| format!("Failed to read agent file {}: {}", path.display(), e))?;
|
||||
// Check if it's a .md or .md.disabled file
|
||||
let (is_agent_file, is_disabled) = if file_name_str.ends_with(".md.disabled")
|
||||
{
|
||||
(true, true)
|
||||
} else if file_name_str.ends_with(".md") {
|
||||
(true, false)
|
||||
} else {
|
||||
(false, false)
|
||||
};
|
||||
|
||||
agent_files.push(AgentFile {
|
||||
name: file_name,
|
||||
content,
|
||||
exists: true,
|
||||
});
|
||||
if is_agent_file {
|
||||
// Extract the agent name (without .md or .md.disabled)
|
||||
let agent_name = if is_disabled {
|
||||
file_name_str
|
||||
.trim_end_matches(".md.disabled")
|
||||
.to_string()
|
||||
} else {
|
||||
file_name_str.trim_end_matches(".md").to_string()
|
||||
};
|
||||
|
||||
let content = std::fs::read_to_string(&path).map_err(|e| {
|
||||
format!(
|
||||
"Failed to read agent file {}: {}",
|
||||
path.display(),
|
||||
e
|
||||
)
|
||||
})?;
|
||||
|
||||
agent_files.push(AgentFile {
|
||||
name: agent_name,
|
||||
content,
|
||||
exists: true,
|
||||
disabled: is_disabled,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2884,13 +3190,54 @@ pub async fn write_claude_agent(agent_name: String, content: String) -> Result<(
|
||||
pub async fn delete_claude_agent(agent_name: String) -> Result<(), String> {
|
||||
let home_dir = home_dir()?;
|
||||
let agents_dir = home_dir.join(".claude/agents");
|
||||
let agent_file_path = agents_dir.join(format!("{}.md", agent_name));
|
||||
let active_path = agents_dir.join(format!("{}.md", agent_name));
|
||||
let disabled_path = agents_dir.join(format!("{}.md.disabled", agent_name));
|
||||
|
||||
if agent_file_path.exists() {
|
||||
std::fs::remove_file(&agent_file_path)
|
||||
if active_path.exists() {
|
||||
std::fs::remove_file(&active_path)
|
||||
.map_err(|e| format!("Failed to delete agent file: {}", e))?;
|
||||
}
|
||||
|
||||
if disabled_path.exists() {
|
||||
std::fs::remove_file(&disabled_path)
|
||||
.map_err(|e| format!("Failed to delete disabled agent file: {}", e))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn toggle_claude_agent(
|
||||
agent_name: String,
|
||||
disabled: bool,
|
||||
) -> Result<(), String> {
|
||||
let home_dir = home_dir()?;
|
||||
let agents_dir = home_dir.join(".claude/agents");
|
||||
|
||||
let (source_path, target_path) = if disabled {
|
||||
// Disable: rename from .md to .md.disabled
|
||||
(
|
||||
agents_dir.join(format!("{}.md", agent_name)),
|
||||
agents_dir.join(format!("{}.md.disabled", agent_name)),
|
||||
)
|
||||
} else {
|
||||
// Enable: rename from .md.disabled to .md
|
||||
(
|
||||
agents_dir.join(format!("{}.md.disabled", agent_name)),
|
||||
agents_dir.join(format!("{}.md", agent_name)),
|
||||
)
|
||||
};
|
||||
|
||||
if !source_path.exists() {
|
||||
return Err(format!(
|
||||
"Agent file {} does not exist",
|
||||
source_path.display()
|
||||
));
|
||||
}
|
||||
|
||||
std::fs::rename(&source_path, &target_path)
|
||||
.map_err(|e| format!("Failed to toggle agent file: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -199,6 +199,10 @@ pub fn run() {
|
||||
read_project_usage_files,
|
||||
read_claude_memory,
|
||||
write_claude_memory,
|
||||
list_claude_memory_files,
|
||||
write_claude_memory_file,
|
||||
toggle_claude_memory_file,
|
||||
delete_claude_memory_file,
|
||||
track,
|
||||
get_notification_settings,
|
||||
update_notification_settings,
|
||||
@@ -212,6 +216,7 @@ pub fn run() {
|
||||
read_claude_agents,
|
||||
write_claude_agent,
|
||||
delete_claude_agent,
|
||||
toggle_claude_agent,
|
||||
read_installed_plugins,
|
||||
toggle_plugin,
|
||||
read_plugin_commands,
|
||||
|
||||
@@ -158,10 +158,24 @@
|
||||
"mcp.source": "Source",
|
||||
"mcp.title": "MCP",
|
||||
"mcp.toggleStatus": "Toggle Status",
|
||||
"memory.description": "Manage Claude Code global memory",
|
||||
"memory.description": "Manage Claude Code memory files for global and projects",
|
||||
"memory.title": "Memory",
|
||||
"memory.save": "Save",
|
||||
"memory.saving": "Saving...",
|
||||
"memory.title": "Memory",
|
||||
"memory.enabled": "Enabled",
|
||||
"memory.disabled": "Disabled",
|
||||
"memory.enable": "Enable",
|
||||
"memory.disable": "Disable",
|
||||
"memory.delete": "Delete",
|
||||
"memory.error": "Error loading memory: {{error}}",
|
||||
"memory.noMemories": "No memory files found yet.",
|
||||
"memory.globalLabel": "Global Memory",
|
||||
"memory.projectLabel": "Project Memory ({{path}})",
|
||||
"memory.sourceGlobal": "Global",
|
||||
"memory.sourceProject": "Project",
|
||||
"memory.contentPlaceholder": "CLAUDE.md content",
|
||||
"memory.deleteConfirm": "Delete {{label}}? This will remove the underlying CLAUDE.md file.",
|
||||
"memory.deleteTitle": "Delete memory",
|
||||
"minimax.apiKeyPlaceholder": "Enter your MiniMax API Key",
|
||||
"minimax.chinaMainland": "China Mainland (minimaxi.com)",
|
||||
"minimax.configInternational": "Configure MiniMax (International)",
|
||||
@@ -276,6 +290,12 @@
|
||||
"toast.storeSaveFailed": "Failed to save config: {{error}}",
|
||||
"toast.storeSaved": "Config \"{{title}}\" saved successfully",
|
||||
"toast.storeSavedAndActive": "Config \"{{title}}\" saved successfully. Config has applied, please restart the Claude Code session",
|
||||
"toast.memorySaved": "Memory saved successfully",
|
||||
"toast.memorySaveFailed": "Failed to save memory: {{error}}",
|
||||
"toast.memoryToggled": "Memory status updated successfully",
|
||||
"toast.memoryToggleFailed": "Failed to update memory status: {{error}}",
|
||||
"toast.memoryDeleted": "Memory deleted successfully",
|
||||
"toast.memoryDeleteFailed": "Failed to delete memory: {{error}}",
|
||||
"toast.updateInstallFailed": "Failed to install update: {{error}}",
|
||||
"toast.updateInstalled": "Update installed successfully. Restarting...",
|
||||
"updateButton.installing": "Installing...",
|
||||
|
||||
@@ -142,10 +142,24 @@
|
||||
"mcp.source": "Source",
|
||||
"mcp.title": "MCP",
|
||||
"mcp.toggleStatus": "Basculer le statut",
|
||||
"memory.description": "Gérer la mémoire globale de Claude Code",
|
||||
"memory.description": "Gérer les fichiers de mémoire Claude Code pour global et projets",
|
||||
"memory.title": "Mémoire",
|
||||
"memory.save": "Sauvegarder",
|
||||
"memory.saving": "Sauvegarde...",
|
||||
"memory.title": "Mémoire",
|
||||
"memory.enabled": "Activée",
|
||||
"memory.disabled": "Désactivée",
|
||||
"memory.enable": "Activer",
|
||||
"memory.disable": "Désactiver",
|
||||
"memory.delete": "Supprimer",
|
||||
"memory.error": "Erreur lors du chargement de la mémoire : {{error}}",
|
||||
"memory.noMemories": "Aucun fichier de mémoire trouvé.",
|
||||
"memory.globalLabel": "Mémoire globale",
|
||||
"memory.projectLabel": "Mémoire projet ({{path}})",
|
||||
"memory.sourceGlobal": "Global",
|
||||
"memory.sourceProject": "Projet",
|
||||
"memory.contentPlaceholder": "Contenu de CLAUDE.md",
|
||||
"memory.deleteConfirm": "Supprimer {{label}} ? Cela supprimera le fichier CLAUDE.md sous-jacent.",
|
||||
"memory.deleteTitle": "Supprimer la mémoire",
|
||||
"minimax.apiKeyPlaceholder": "Entrez votre clé API MiniMax",
|
||||
"minimax.chinaMainland": "Chine continentale (minimaxi.com)",
|
||||
"minimax.configInternational": "Configurer MiniMax (International)",
|
||||
@@ -258,6 +272,12 @@
|
||||
"toast.storeSaveFailed": "Échec de la sauvegarde de la configuration : {{error}}",
|
||||
"toast.storeSaved": "Configuration \"{{title}}\" sauvegardée avec succès",
|
||||
"toast.storeSavedAndActive": "Configuration \"{{title}}\" sauvegardée avec succès. La configuration a été appliquée, veuillez redémarrer la session Claude Code",
|
||||
"toast.memorySaved": "Mémoire sauvegardée avec succès",
|
||||
"toast.memorySaveFailed": "Échec de la sauvegarde de la mémoire : {{error}}",
|
||||
"toast.memoryToggled": "Statut de la mémoire mis à jour avec succès",
|
||||
"toast.memoryToggleFailed": "Échec de la mise à jour du statut de la mémoire : {{error}}",
|
||||
"toast.memoryDeleted": "Mémoire supprimée avec succès",
|
||||
"toast.memoryDeleteFailed": "Échec de la suppression de la mémoire : {{error}}",
|
||||
"toast.updateInstallFailed": "Échec de l'installation de la mise à jour : {{error}}",
|
||||
"toast.updateInstalled": "Mise à jour installée avec succès. Redémarrage...",
|
||||
"updateButton.installing": "Installation...",
|
||||
|
||||
@@ -142,10 +142,24 @@
|
||||
"mcp.source": "ソース",
|
||||
"mcp.title": "MCP",
|
||||
"mcp.toggleStatus": "ステータスを切り替え",
|
||||
"memory.description": "Claude Code グローバルメモリを管理",
|
||||
"memory.description": "Claude Code のグローバルおよびプロジェクト用メモリファイルを管理",
|
||||
"memory.title": "メモリ",
|
||||
"memory.save": "保存",
|
||||
"memory.saving": "保存中...",
|
||||
"memory.title": "メモリ",
|
||||
"memory.enabled": "有効",
|
||||
"memory.disabled": "無効",
|
||||
"memory.enable": "有効化",
|
||||
"memory.disable": "無効化",
|
||||
"memory.delete": "削除",
|
||||
"memory.error": "メモリの読み込みエラー:{{error}}",
|
||||
"memory.noMemories": "メモリファイルがまだありません。",
|
||||
"memory.globalLabel": "グローバルメモリ",
|
||||
"memory.projectLabel": "プロジェクトメモリ({{path}})",
|
||||
"memory.sourceGlobal": "グローバル",
|
||||
"memory.sourceProject": "プロジェクト",
|
||||
"memory.contentPlaceholder": "CLAUDE.md の内容",
|
||||
"memory.deleteConfirm": "{{label}} を削除しますか?対応する CLAUDE.md ファイルも削除されます。",
|
||||
"memory.deleteTitle": "メモリを削除",
|
||||
"minimax.apiKeyPlaceholder": "MiniMax API キーを入力してください",
|
||||
"minimax.chinaMainland": "中国大陸 (minimaxi.com)",
|
||||
"minimax.configInternational": "MiniMax (国際) を設定",
|
||||
@@ -258,6 +272,12 @@
|
||||
"toast.storeSaveFailed": "設定の保存に失敗しました:{{error}}",
|
||||
"toast.storeSaved": "設定 \"{{title}}\" が正常に保存されました",
|
||||
"toast.storeSavedAndActive": "設定 \"{{title}}\" が正常に保存されました。設定が適用されました。Claude Code セッションを再起動してください",
|
||||
"toast.memorySaved": "メモリが正常に保存されました",
|
||||
"toast.memorySaveFailed": "メモリの保存に失敗しました:{{error}}",
|
||||
"toast.memoryToggled": "メモリのステータスが正常に更新されました",
|
||||
"toast.memoryToggleFailed": "メモリのステータス更新に失敗しました:{{error}}",
|
||||
"toast.memoryDeleted": "メモリが正常に削除されました",
|
||||
"toast.memoryDeleteFailed": "メモリの削除に失敗しました:{{error}}",
|
||||
"toast.updateInstallFailed": "更新のインストールに失敗しました:{{error}}",
|
||||
"toast.updateInstalled": "更新が正常にインストールされました。再起動中...",
|
||||
"updateButton.installing": "インストール中...",
|
||||
|
||||
@@ -154,10 +154,24 @@
|
||||
"mcp.source": "源码",
|
||||
"mcp.title": "MCP",
|
||||
"mcp.toggleStatus": "切换状态",
|
||||
"memory.description": "管理 Claude Code 全局记忆",
|
||||
"memory.description": "管理 Claude Code 的全局和项目记忆文件",
|
||||
"memory.title": "记忆",
|
||||
"memory.save": "保存",
|
||||
"memory.saving": "保存中...",
|
||||
"memory.title": "记忆",
|
||||
"memory.enabled": "已启用",
|
||||
"memory.disabled": "已禁用",
|
||||
"memory.enable": "启用",
|
||||
"memory.disable": "禁用",
|
||||
"memory.delete": "删除",
|
||||
"memory.error": "加载记忆时出错:{{error}}",
|
||||
"memory.noMemories": "尚未找到记忆文件。",
|
||||
"memory.globalLabel": "全局记忆",
|
||||
"memory.projectLabel": "项目记忆({{path}})",
|
||||
"memory.sourceGlobal": "全局",
|
||||
"memory.sourceProject": "项目",
|
||||
"memory.contentPlaceholder": "CLAUDE.md 内容",
|
||||
"memory.deleteConfirm": "删除 {{label}}?这将删除底层的 CLAUDE.md 文件。",
|
||||
"memory.deleteTitle": "删除记忆",
|
||||
"minimax.apiKeyPlaceholder": "请输入您的 MiniMax API Key",
|
||||
"minimax.chinaMainland": "中国大陆 (minimaxi.com)",
|
||||
"minimax.configInternational": "配置 MiniMax (国际)",
|
||||
@@ -270,6 +284,12 @@
|
||||
"toast.storeSaveFailed": "保存配置失败:{{error}}",
|
||||
"toast.storeSaved": "配置 \"{{title}}\" 保存成功",
|
||||
"toast.storeSavedAndActive": "配置 \"{{title}}\" 保存成功。配置已应用,请重启 Claude Code 会话",
|
||||
"toast.memorySaved": "记忆保存成功",
|
||||
"toast.memorySaveFailed": "保存记忆失败:{{error}}",
|
||||
"toast.memoryToggled": "记忆状态更新成功",
|
||||
"toast.memoryToggleFailed": "更新记忆状态失败:{{error}}",
|
||||
"toast.memoryDeleted": "记忆删除成功",
|
||||
"toast.memoryDeleteFailed": "删除记忆失败:{{error}}",
|
||||
"toast.updateInstallFailed": "安装更新失败:{{error}}",
|
||||
"toast.updateInstalled": "更新安装成功,正在重启...",
|
||||
"updateButton.installing": "安装中...",
|
||||
|
||||
+145
-2
@@ -511,6 +511,16 @@ export interface MemoryFile {
|
||||
exists: boolean;
|
||||
}
|
||||
|
||||
export interface MemoryEntry {
|
||||
name: string;
|
||||
path: string;
|
||||
content: string;
|
||||
exists: boolean;
|
||||
source: "global" | "project";
|
||||
projectPath?: string;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
export const useClaudeMemory = () => {
|
||||
return useSuspenseQuery({
|
||||
queryKey: ["claude-memory"],
|
||||
@@ -525,13 +535,122 @@ export const useWriteClaudeMemory = () => {
|
||||
mutationFn: (content: string) =>
|
||||
invoke<void>("write_claude_memory", { content }),
|
||||
onSuccess: () => {
|
||||
toast.success("Memory saved successfully");
|
||||
toast.success(i18n.t("toast.memorySaved"));
|
||||
queryClient.invalidateQueries({ queryKey: ["claude-memory"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["claude-memory-files"] });
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
toast.error(
|
||||
i18n.t("toast.memorySaveFailed", { error: errorMessage }),
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useClaudeMemoryFiles = () => {
|
||||
return useQuery({
|
||||
queryKey: ["claude-memory-files"],
|
||||
queryFn: () => invoke<MemoryEntry[]>("list_claude_memory_files"),
|
||||
});
|
||||
};
|
||||
|
||||
export const useWriteClaudeMemoryFile = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
source,
|
||||
projectPath,
|
||||
content,
|
||||
disabled,
|
||||
}: {
|
||||
source: "global" | "project";
|
||||
projectPath?: string;
|
||||
content: string;
|
||||
disabled: boolean;
|
||||
}) =>
|
||||
invoke<void>("write_claude_memory_file", {
|
||||
source,
|
||||
projectPath,
|
||||
content,
|
||||
disabled,
|
||||
}),
|
||||
onSuccess: () => {
|
||||
toast.success(i18n.t("toast.memorySaved"));
|
||||
queryClient.invalidateQueries({ queryKey: ["claude-memory-files"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["claude-memory"] });
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
toast.error(`Failed to save memory: ${errorMessage}`);
|
||||
toast.error(
|
||||
i18n.t("toast.memorySaveFailed", { error: errorMessage }),
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useToggleClaudeMemoryFile = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
source,
|
||||
projectPath,
|
||||
disabled,
|
||||
}: {
|
||||
source: "global" | "project";
|
||||
projectPath?: string;
|
||||
disabled: boolean;
|
||||
}) =>
|
||||
invoke<void>("toggle_claude_memory_file", {
|
||||
source,
|
||||
projectPath,
|
||||
disabled,
|
||||
}),
|
||||
onSuccess: () => {
|
||||
toast.success(i18n.t("toast.memoryToggled"));
|
||||
queryClient.invalidateQueries({ queryKey: ["claude-memory-files"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["claude-memory"] });
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
toast.error(
|
||||
i18n.t("toast.memoryToggleFailed", { error: errorMessage }),
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteClaudeMemoryFile = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
source,
|
||||
projectPath,
|
||||
}: {
|
||||
source: "global" | "project";
|
||||
projectPath?: string;
|
||||
}) =>
|
||||
invoke<void>("delete_claude_memory_file", {
|
||||
source,
|
||||
projectPath,
|
||||
}),
|
||||
onSuccess: () => {
|
||||
toast.success(i18n.t("toast.memoryDeleted"));
|
||||
queryClient.invalidateQueries({ queryKey: ["claude-memory-files"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["claude-memory"] });
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
toast.error(
|
||||
i18n.t("toast.memoryDeleteFailed", { error: errorMessage }),
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -728,6 +847,30 @@ export const useWriteClaudeAgent = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useToggleClaudeAgent = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
agentName,
|
||||
disabled,
|
||||
}: {
|
||||
agentName: string;
|
||||
disabled: boolean;
|
||||
}) => invoke<void>("toggle_claude_agent", { agentName, disabled }),
|
||||
onSuccess: () => {
|
||||
toast.success("Agent toggled successfully");
|
||||
queryClient.invalidateQueries({ queryKey: ["claude-agents"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["plugin-agents"] });
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
toast.error(`Failed to toggle agent: ${errorMessage}`);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteClaudeAgent = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
|
||||
+111
-44
@@ -27,6 +27,7 @@ import {
|
||||
useClaudeAgents,
|
||||
useDeleteClaudeAgent,
|
||||
usePluginAgents,
|
||||
useToggleClaudeAgent,
|
||||
useWriteClaudeAgent,
|
||||
} from "@/lib/query";
|
||||
import { useCodeMirrorTheme } from "@/lib/use-codemirror-theme";
|
||||
@@ -35,6 +36,7 @@ type UnifiedAgent = {
|
||||
name: string;
|
||||
content: string;
|
||||
exists: boolean;
|
||||
disabled: boolean;
|
||||
source: "user" | "plugin";
|
||||
pluginName?: string;
|
||||
pluginScope?: string;
|
||||
@@ -47,6 +49,7 @@ function AgentsPageContent() {
|
||||
const { data: pluginAgents, isLoading: isLoadingPlugin, error: errorPlugin } = usePluginAgents();
|
||||
const writeAgent = useWriteClaudeAgent();
|
||||
const deleteAgent = useDeleteClaudeAgent();
|
||||
const toggleAgent = useToggleClaudeAgent();
|
||||
const [agentEdits, setAgentEdits] = useState<Record<string, string>>({});
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const codeMirrorTheme = useCodeMirrorTheme();
|
||||
@@ -54,24 +57,40 @@ function AgentsPageContent() {
|
||||
const isLoading = isLoadingUser || isLoadingPlugin;
|
||||
const error = errorUser || errorPlugin;
|
||||
|
||||
const sourceOrder: Record<UnifiedAgent["source"], number> = {
|
||||
user: 0,
|
||||
plugin: 1,
|
||||
};
|
||||
|
||||
const agents: UnifiedAgent[] = [
|
||||
...(userAgents || []).map((agent): UnifiedAgent => ({
|
||||
name: agent.name,
|
||||
content: agent.content,
|
||||
exists: agent.exists,
|
||||
disabled: agent.disabled,
|
||||
source: "user",
|
||||
sourcePath: `~/.claude/agents/${agent.name}.md`,
|
||||
sourcePath: `~/.claude/agents/${agent.name}${agent.disabled ? ".md.disabled" : ".md"}`,
|
||||
})),
|
||||
...(pluginAgents || []).map((agent): UnifiedAgent => ({
|
||||
name: agent.name,
|
||||
content: agent.content,
|
||||
exists: agent.exists,
|
||||
disabled: false,
|
||||
source: "plugin",
|
||||
pluginName: agent.pluginName,
|
||||
pluginScope: agent.pluginScope,
|
||||
sourcePath: agent.sourcePath,
|
||||
})),
|
||||
].sort((a, b) => a.name.localeCompare(b.name));
|
||||
].sort((a, b) => {
|
||||
const orderA = sourceOrder[a.source] ?? 99;
|
||||
const orderB = sourceOrder[b.source] ?? 99;
|
||||
|
||||
if (orderA !== orderB) {
|
||||
return orderA - orderB;
|
||||
}
|
||||
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -119,6 +138,17 @@ function AgentsPageContent() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleAgent = (agent: UnifiedAgent) => {
|
||||
if (agent.source !== "user") {
|
||||
return;
|
||||
}
|
||||
|
||||
toggleAgent.mutate({
|
||||
agentName: agent.name,
|
||||
disabled: !agent.disabled,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
@@ -167,26 +197,46 @@ function AgentsPageContent() {
|
||||
className="bg-card"
|
||||
>
|
||||
<AccordionTrigger className="hover:no-underline px-4 py-2 bg-card hover:bg-accent duration-150">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<BotIcon size={12} />
|
||||
<span className="font-medium">{agent.name}</span>
|
||||
{agent.source === "user" ? (
|
||||
<Badge variant="default">
|
||||
{t("agents.sourceUser")}
|
||||
<div className="flex items-center justify-between gap-2 w-full">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<BotIcon size={12} />
|
||||
<span className="font-medium">{agent.name}</span>
|
||||
<Badge
|
||||
variant={agent.disabled ? "destructive" : "success"}
|
||||
>
|
||||
{agent.disabled ? "Disabled" : "Enabled"}
|
||||
</Badge>
|
||||
) : (
|
||||
<>
|
||||
<Badge variant="outline">
|
||||
{agent.pluginName}
|
||||
<span className="text-xs text-muted-foreground font-mono truncate max-w-xs">
|
||||
{agent.sourcePath}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{agent.source === "user" && (
|
||||
<Badge variant="secondary">
|
||||
{t("agents.sourceUser")}
|
||||
</Badge>
|
||||
<Badge variant={agent.pluginScope === "user" ? "default" : "secondary"}>
|
||||
{agent.pluginScope === "user" ? t("plugins.scopeUser") : t("plugins.scopeLocal")}
|
||||
</Badge>
|
||||
</>
|
||||
)}
|
||||
<span className="text-sm text-muted-foreground font-normal">
|
||||
{agent.sourcePath}
|
||||
</span>
|
||||
)}
|
||||
{agent.source === "plugin" && agent.pluginName && (
|
||||
<>
|
||||
<Badge variant="secondary">
|
||||
{`Plugins (${agent.pluginName})`}
|
||||
</Badge>
|
||||
{agent.pluginScope && (
|
||||
<Badge
|
||||
variant={
|
||||
agent.pluginScope === "user"
|
||||
? "default"
|
||||
: "secondary"
|
||||
}
|
||||
>
|
||||
{agent.pluginScope === "user"
|
||||
? t("plugins.scopeUser")
|
||||
: t("plugins.scopeLocal")}
|
||||
</Badge>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="pb-3">
|
||||
@@ -204,30 +254,47 @@ function AgentsPageContent() {
|
||||
basicSetup={codeMirrorBasicSetup}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between bg-card">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => handleSaveAgent(agent.name)}
|
||||
disabled={
|
||||
writeAgent.isPending ||
|
||||
agentEdits[agent.name] === undefined
|
||||
}
|
||||
size="sm"
|
||||
>
|
||||
<SaveIcon size={14} />
|
||||
{writeAgent.isPending
|
||||
? t("agents.saving")
|
||||
: t("agents.save")}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteAgent(agent.name)}
|
||||
disabled={deleteAgent.isPending}
|
||||
>
|
||||
<TrashIcon size={14} />
|
||||
</Button>
|
||||
<div className="flex justify-between bg-card px-1 py-1">
|
||||
<div className="flex items-center text-xs text-muted-foreground font-mono">
|
||||
<span className="truncate max-w-xs">
|
||||
{agent.sourcePath}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleSaveAgent(agent.name)}
|
||||
disabled={
|
||||
writeAgent.isPending ||
|
||||
agentEdits[agent.name] === undefined
|
||||
}
|
||||
>
|
||||
<SaveIcon size={12} className="mr-1" />
|
||||
{writeAgent.isPending
|
||||
? t("agents.saving")
|
||||
: t("agents.save")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleToggleAgent(agent)}
|
||||
disabled={
|
||||
agent.source !== "user" || toggleAgent.isPending
|
||||
}
|
||||
>
|
||||
{agent.disabled ? "Enable" : "Disable"}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteAgent(agent.name)}
|
||||
disabled={deleteAgent.isPending}
|
||||
>
|
||||
<TrashIcon size={12} className="mr-1" />
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
|
||||
+64
-40
@@ -176,29 +176,45 @@ function CommandsPageContent() {
|
||||
className="bg-card"
|
||||
>
|
||||
<AccordionTrigger className="hover:no-underline px-4 py-2 bg-card hover:bg-accent duration-150">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<TerminalIcon size={12} />
|
||||
<span className="font-medium">{command.name}</span>
|
||||
<Badge variant={command.disabled ? "secondary" : "success"}>
|
||||
{command.disabled ? t("commands.disabled") : t("commands.enabled")}
|
||||
</Badge>
|
||||
{command.source === "user" ? (
|
||||
<Badge variant="default">
|
||||
{t("commands.sourceUser")}
|
||||
<div className="flex items-center justify-between gap-2 w-full">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<TerminalIcon size={12} />
|
||||
<span className="font-medium">{command.name}</span>
|
||||
<Badge
|
||||
variant={command.disabled ? "secondary" : "success"}
|
||||
>
|
||||
{command.disabled
|
||||
? t("commands.disabled")
|
||||
: t("commands.enabled")}
|
||||
</Badge>
|
||||
) : (
|
||||
<>
|
||||
<Badge variant="outline">
|
||||
{command.pluginName}
|
||||
<span className="text-xs text-muted-foreground font-mono truncate max-w-xs">
|
||||
{command.sourcePath}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{command.source === "user" ? (
|
||||
<Badge variant="default">
|
||||
{t("commands.sourceUser")}
|
||||
</Badge>
|
||||
<Badge variant={command.pluginScope === "user" ? "default" : "secondary"}>
|
||||
{command.pluginScope === "user" ? t("plugins.scopeUser") : t("plugins.scopeLocal")}
|
||||
</Badge>
|
||||
</>
|
||||
)}
|
||||
<span className="text-sm text-muted-foreground font-normal">
|
||||
{command.sourcePath}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<Badge variant="outline">
|
||||
{command.pluginName}
|
||||
</Badge>
|
||||
<Badge
|
||||
variant={
|
||||
command.pluginScope === "user"
|
||||
? "default"
|
||||
: "secondary"
|
||||
}
|
||||
>
|
||||
{command.pluginScope === "user"
|
||||
? t("plugins.scopeUser")
|
||||
: t("plugins.scopeLocal")}
|
||||
</Badge>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="pb-3">
|
||||
@@ -216,41 +232,49 @@ function CommandsPageContent() {
|
||||
basicSetup={codeMirrorBasicSetup}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between bg-card">
|
||||
<div className="flex justify-between bg-card px-1 py-1">
|
||||
<div className="flex items-center text-xs text-muted-foreground font-mono">
|
||||
<span className="truncate max-w-xs">
|
||||
{command.sourcePath}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleSaveCommand(command.name)}
|
||||
disabled={
|
||||
writeCommand.isPending ||
|
||||
commandEdits[command.name] === undefined
|
||||
}
|
||||
size="sm"
|
||||
>
|
||||
<SaveIcon size={14} />
|
||||
<SaveIcon size={12} className="mr-1" />
|
||||
{writeCommand.isPending
|
||||
? t("commands.saving")
|
||||
: t("commands.save")}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleToggleCommand(command.name, command.disabled)}
|
||||
onClick={() =>
|
||||
handleToggleCommand(command.name, command.disabled)
|
||||
}
|
||||
disabled={toggleCommand.isPending}
|
||||
>
|
||||
{command.disabled ? t("commands.enable") : t("commands.disable")}
|
||||
{command.disabled
|
||||
? t("commands.enable")
|
||||
: t("commands.disable")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteCommand(command.name)}
|
||||
disabled={deleteCommand.isPending}
|
||||
>
|
||||
<TrashIcon size={12} className="mr-1" />
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteCommand(command.name)}
|
||||
disabled={deleteCommand.isPending}
|
||||
>
|
||||
<TrashIcon size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
@@ -350,12 +374,12 @@ function CreateCommandPanel({ onClose }: CreateCommandPanelProps) {
|
||||
<div className="rounded-lg overflow-hidden border">
|
||||
<CodeMirror
|
||||
value={commandContent}
|
||||
onChange={(value) => setCommandContent(value)}
|
||||
height="200px"
|
||||
theme={codeMirrorTheme}
|
||||
placeholder={t("commands.contentPlaceholder")}
|
||||
extensions={markdownExtensions}
|
||||
basicSetup={codeMirrorBasicSetup}
|
||||
onChange={(value) => setCommandContent(value)}
|
||||
placeholder={t("commands.contentPlaceholder")}
|
||||
extensions={markdownExtensions}
|
||||
basicSetup={codeMirrorBasicSetup}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+255
-85
@@ -1,46 +1,44 @@
|
||||
import { markdown, markdownLanguage } from "@codemirror/lang-markdown";
|
||||
import { yamlFrontmatter } from "@codemirror/lang-yaml";
|
||||
import CodeMirror, { EditorView } from "@uiw/react-codemirror";
|
||||
import { SaveIcon } from "lucide-react";
|
||||
import { Suspense, useEffect, useState } from "react";
|
||||
import { ask } from "@tauri-apps/plugin-dialog";
|
||||
import CodeMirror from "@uiw/react-codemirror";
|
||||
import { SaveIcon, SparklesIcon, TrashIcon } from "lucide-react";
|
||||
import { Suspense, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { useClaudeMemory, useWriteClaudeMemory } from "@/lib/query";
|
||||
import {
|
||||
type MemoryEntry,
|
||||
useClaudeMemoryFiles,
|
||||
useDeleteClaudeMemoryFile,
|
||||
useToggleClaudeMemoryFile,
|
||||
useWriteClaudeMemoryFile,
|
||||
} from "@/lib/query";
|
||||
import { codeMirrorBasicSetup, markdownExtensions } from "@/lib/codemirror-config";
|
||||
import { useCodeMirrorTheme } from "@/lib/use-codemirror-theme";
|
||||
|
||||
function MemoryPageHeader({
|
||||
onSave,
|
||||
saving,
|
||||
}: {
|
||||
onSave: () => void;
|
||||
saving: boolean;
|
||||
}) {
|
||||
function MemoryPageHeader() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex items-center p-3 border-b px-3 justify-between sticky top-0 bg-background z-10"
|
||||
className="flex items-center justify-between sticky top-0 z-10 border-b p-3 bg-background"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<div data-tauri-drag-region>
|
||||
<h3 className="font-bold" data-tauri-drag-region>
|
||||
{t("memory.title")}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p className="text-sm text-muted-foreground" data-tauri-drag-region>
|
||||
{t("memory.description")}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={onSave}
|
||||
disabled={saving}
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<SaveIcon className="w-4 h-4" />
|
||||
{saving ? t("memory.saving") : t("memory.save")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -76,76 +74,248 @@ function MemoryPageSkeleton() {
|
||||
}
|
||||
|
||||
function MemoryPageContent() {
|
||||
const { data: memoryData } = useClaudeMemory();
|
||||
const { mutate: saveMemory, isPending: saving } = useWriteClaudeMemory();
|
||||
const [content, setContent] = useState<string>("");
|
||||
const { t } = useTranslation();
|
||||
const { data: memories, isLoading, error } = useClaudeMemoryFiles();
|
||||
const writeMemory = useWriteClaudeMemoryFile();
|
||||
const toggleMemory = useToggleClaudeMemoryFile();
|
||||
const deleteMemory = useDeleteClaudeMemoryFile();
|
||||
const [edits, setEdits] = useState<Record<string, string>>({});
|
||||
const codeMirrorTheme = useCodeMirrorTheme();
|
||||
|
||||
// Update local content when memory data loads
|
||||
useEffect(() => {
|
||||
if (memoryData?.content) {
|
||||
setContent(memoryData.content);
|
||||
}
|
||||
}, [memoryData]);
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="text-center">{t("loading")}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
saveMemory(content);
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="text-center text-red-500">
|
||||
{t("memory.error", { error: (error as Error).message })}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const memoryList = (memories ?? []).slice();
|
||||
|
||||
const getEntryKey = (entry: MemoryEntry) =>
|
||||
entry.source === "global" ? "global" : entry.projectPath ?? entry.name;
|
||||
|
||||
const getProjectDisplayName = (projectPath?: string, fallback?: string) => {
|
||||
if (!projectPath) return fallback ?? "";
|
||||
const parts = projectPath.split(/[/\\]/).filter(Boolean);
|
||||
return parts[parts.length - 1] ?? projectPath;
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
// Cmd+S or Ctrl+S to save
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === "s") {
|
||||
e.preventDefault();
|
||||
handleSave();
|
||||
}
|
||||
const handleSaveMemory = (entry: MemoryEntry) => {
|
||||
const key = getEntryKey(entry);
|
||||
const content = edits[key] ?? entry.content;
|
||||
|
||||
writeMemory.mutate({
|
||||
source: entry.source,
|
||||
projectPath: entry.projectPath,
|
||||
content,
|
||||
disabled: entry.disabled,
|
||||
});
|
||||
};
|
||||
|
||||
// Add keyboard event listener
|
||||
useEffect(() => {
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [content]);
|
||||
const handleToggleMemory = (entry: MemoryEntry) => {
|
||||
toggleMemory.mutate({
|
||||
source: entry.source,
|
||||
projectPath: entry.projectPath,
|
||||
disabled: !entry.disabled,
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteMemory = async (entry: MemoryEntry) => {
|
||||
const label =
|
||||
entry.source === "global"
|
||||
? t("memory.globalLabel")
|
||||
: t("memory.projectLabel", {
|
||||
path: getProjectDisplayName(entry.projectPath, entry.name),
|
||||
});
|
||||
|
||||
const confirmed = await ask(
|
||||
t("memory.deleteConfirm", { label }),
|
||||
{
|
||||
title: t("memory.deleteTitle"),
|
||||
kind: "warning",
|
||||
},
|
||||
);
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
deleteMemory.mutate({
|
||||
source: entry.source,
|
||||
projectPath: entry.projectPath,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-screen">
|
||||
<MemoryPageHeader onSave={handleSave} saving={saving} />
|
||||
<div>
|
||||
<MemoryPageHeader />
|
||||
<div>
|
||||
{memoryList.length === 0 ? (
|
||||
<div className="text-center text-muted-foreground py-8">
|
||||
{t("memory.noMemories")}
|
||||
</div>
|
||||
) : (
|
||||
<ScrollArea className="h-full">
|
||||
<div>
|
||||
<Accordion type="multiple">
|
||||
{memoryList.map((entry) => {
|
||||
const key = getEntryKey(entry);
|
||||
const label =
|
||||
entry.source === "global"
|
||||
? t("memory.globalLabel")
|
||||
: t("memory.projectLabel", {
|
||||
path: getProjectDisplayName(
|
||||
entry.projectPath,
|
||||
entry.name,
|
||||
),
|
||||
});
|
||||
|
||||
<div className="flex-1 p-4 overflow-hidden">
|
||||
<div className="rounded-lg overflow-hidden border h-full">
|
||||
<CodeMirror
|
||||
value={content}
|
||||
height="100%"
|
||||
extensions={[
|
||||
yamlFrontmatter({
|
||||
content: markdown({
|
||||
base: markdownLanguage,
|
||||
}),
|
||||
}),
|
||||
EditorView.lineWrapping,
|
||||
]}
|
||||
placeholder="~/.claude/CLAUDE.md"
|
||||
onChange={(value) => setContent(value)}
|
||||
theme={codeMirrorTheme}
|
||||
basicSetup={{
|
||||
lineNumbers: false,
|
||||
highlightActiveLineGutter: true,
|
||||
foldGutter: false,
|
||||
dropCursor: false,
|
||||
allowMultipleSelections: false,
|
||||
indentOnInput: true,
|
||||
bracketMatching: true,
|
||||
closeBrackets: true,
|
||||
autocompletion: true,
|
||||
highlightActiveLine: true,
|
||||
highlightSelectionMatches: true,
|
||||
searchKeymap: false,
|
||||
}}
|
||||
className="h-full"
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</div>
|
||||
return (
|
||||
<AccordionItem
|
||||
key={key}
|
||||
value={key}
|
||||
className="bg-card"
|
||||
>
|
||||
<AccordionTrigger className="hover:no-underline px-4 py-2 bg-card hover:bg-accent duration-150">
|
||||
<div className="flex items-center justify-between gap-2 w-full">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<SparklesIcon size={12} />
|
||||
<span className="font-medium">
|
||||
{label}
|
||||
</span>
|
||||
<Badge
|
||||
variant={
|
||||
entry.disabled
|
||||
? "destructive"
|
||||
: "success"
|
||||
}
|
||||
>
|
||||
{entry.disabled
|
||||
? t("memory.disabled")
|
||||
: t("memory.enabled")}
|
||||
</Badge>
|
||||
{entry.source === "project" &&
|
||||
entry.projectPath && (
|
||||
<span className="text-xs text-muted-foreground font-mono truncate max-w-xs">
|
||||
{entry.projectPath}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{entry.source === "global" && (
|
||||
<Badge variant="secondary">
|
||||
{t("memory.sourceGlobal")}
|
||||
</Badge>
|
||||
)}
|
||||
{entry.source === "project" && (
|
||||
<Badge variant="secondary">
|
||||
{t("memory.sourceProject")}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="pb-3">
|
||||
<div className="px-3 pt-3 space-y-3">
|
||||
<div className="rounded-lg overflow-hidden border">
|
||||
<CodeMirror
|
||||
value={
|
||||
edits[key] ?? entry.content
|
||||
}
|
||||
height="280px"
|
||||
theme={codeMirrorTheme}
|
||||
onChange={(value) =>
|
||||
setEdits((prev) => ({
|
||||
...prev,
|
||||
[key]: value,
|
||||
}))
|
||||
}
|
||||
placeholder={t(
|
||||
"memory.contentPlaceholder",
|
||||
)}
|
||||
extensions={markdownExtensions}
|
||||
basicSetup={codeMirrorBasicSetup}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between bg-card px-1 py-1">
|
||||
<div className="flex items-center text-xs text-muted-foreground font-mono">
|
||||
<span className="truncate max-w-xs">
|
||||
{entry.path}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
handleSaveMemory(entry)
|
||||
}
|
||||
disabled={
|
||||
writeMemory.isPending
|
||||
}
|
||||
>
|
||||
<SaveIcon
|
||||
size={12}
|
||||
className="mr-1"
|
||||
/>
|
||||
{writeMemory.isPending
|
||||
? t("memory.saving")
|
||||
: t("memory.save")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
handleToggleMemory(entry)
|
||||
}
|
||||
disabled={
|
||||
toggleMemory.isPending
|
||||
}
|
||||
>
|
||||
{entry.disabled
|
||||
? t("memory.enable")
|
||||
: t("memory.disable")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
void handleDeleteMemory(
|
||||
entry,
|
||||
);
|
||||
}}
|
||||
disabled={
|
||||
deleteMemory.isPending
|
||||
}
|
||||
>
|
||||
<TrashIcon
|
||||
size={12}
|
||||
className="mr-1"
|
||||
/>
|
||||
{t("memory.delete")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
);
|
||||
})}
|
||||
</Accordion>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user