Panel: Clobber-Schutz, Content-Editor, Archiv-Reveal/Guard, Rechtschreib-Sprache

- register_mcp_server schreibt .claude.json nur bei fehlendem/abweichendem
  Eintrag (claudes Live-State nicht unnötig ueberschreiben).
- Content-Editor im Panel (Rohtext-Textarea): Cmd/Ctrl+Enter speichert via
  panel_set, Esc verwirft; eingehende Updates werden waehrend der Bearbeitung
  gepuffert (Button "geaendert"), Speichern gewinnt.
- Archivieren zeigt die Datei danach im Dateimanager (reveal_path_cmd, cfg pro
  OS). Breadth-Guard: Archiv-Ordner darf kein Home-Vorfahre sein, muss absolut
  sein; expand_home loest bare ~ auf.
- Rechtschreibpruefung: Default in settings.json (spellcheckLang "de"), pro
  Text per Selector ueberschreibbar.
This commit is contained in:
marcus.hinz
2026-07-11 14:24:16 +02:00
parent e2e9992b1c
commit 165501c8f9
6 changed files with 336 additions and 56 deletions
+68 -14
View File
@@ -88,6 +88,17 @@ pub(crate) fn project_archive_dir(project: &str) -> Option<PathBuf> {
pub(crate) fn set_project_archive_dir(project: &str, dir: &str) -> Result<(), String> {
let paths = Paths::real();
let expanded = expand_home(&paths, dir);
if !expanded.is_absolute() {
return Err(format!("Archiv-Ordner muss ein absoluter Pfad sein: {}", expanded.display()));
}
// Zu weit gefasst? Ein Vorfahre des Home (inkl. Home selbst und Root) würde
// claude über additionalDirectories weiten Zugriff geben — das lehnen wir ab.
if paths.home.starts_with(&expanded) {
return Err(format!(
"Archiv-Ordner zu weit gefasst: {}. Bitte einen spezifischen Unterordner wählen.",
expanded.display()
));
}
fs::create_dir_all(&expanded).map_err(|e| format!("{}: {e}", expanded.display()))?;
let contracted = contract_home(&paths, &expanded);
let mut cfg = read_project_config_in(&paths, project)?;
@@ -638,8 +649,11 @@ fn set_todo_in(paths: &Paths, name: &str, enabled: bool) -> Result<(), String> {
fs::write(&sp, raw + "\n").map_err(|e| format!("{}: {e}", sp.display()))
}
/// "~/x" relativ zum Home auflösen; alles andere unverändert.
/// "~" bzw. "~/x" relativ zum Home auflösen; alles andere unverändert.
fn expand_home(paths: &Paths, p: &str) -> PathBuf {
if p == "~" {
return paths.home.clone();
}
match p.strip_prefix("~/") {
Some(rest) => paths.home.join(rest),
None => PathBuf::from(p),
@@ -1460,6 +1474,14 @@ fn register_mcp_server(pool_dir: &std::path::Path) {
let Ok(exe) = std::env::current_exe() else {
return;
};
// Tool nicht deferren, sonst sieht das Modell write_panel nicht und schreibt
// den Entwurf in den Chat statt ins Panel.
let desired = serde_json::json!({
"type": "stdio",
"command": exe.to_string_lossy(),
"args": ["--mcp-panel"],
"alwaysLoad": true,
});
let cfg = pool_dir.join(".claude.json");
let mut v = fs::read_to_string(&cfg)
.ok()
@@ -1473,18 +1495,13 @@ fn register_mcp_server(pool_dir: &std::path::Path) {
else {
return;
};
// Schon korrekt und kein Altkey -> nichts schreiben. `.claude.json` ist
// claudes Live-State; wir fassen sie nur an, wenn der Eintrag fehlt/abweicht.
if servers.get("aicontrol").is_none() && servers.get(PANEL_MCP_SERVER) == Some(&desired) {
return;
}
servers.remove("aicontrol"); // alter Key vor Umbenennung
servers.insert(
PANEL_MCP_SERVER.into(),
serde_json::json!({
"type": "stdio",
"command": exe.to_string_lossy(),
"args": ["--mcp-panel"],
// Tool nicht deferren, sonst sieht das Modell write_panel nicht und
// schreibt den Entwurf in den Chat statt ins Panel.
"alwaysLoad": true,
}),
);
servers.insert(PANEL_MCP_SERVER.into(), desired);
if let Ok(out) = serde_json::to_string_pretty(&v) {
let _ = fs::write(&cfg, out + "\n");
}
@@ -2137,6 +2154,17 @@ fn terminal_font_size() -> u32 {
.unwrap_or(13)
}
/// Standard-Sprache der Rechtschreibprüfung (settings.json: spellcheckLang),
/// Default „de". Pro Text im Panel überschreibbar.
#[tauri::command]
fn spellcheck_lang() -> String {
fs::read_to_string(Paths::real().config_dir().join(APP_SETTINGS_FILE))
.ok()
.and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
.and_then(|v| v["spellcheckLang"].as_str().map(str::to_string))
.unwrap_or_else(|| "de".to_string())
}
/// Setzt die Schriftgröße; erhält übrige App-settings.
#[tauri::command]
fn set_terminal_font_size(size: u32) -> Result<(), String> {
@@ -2269,6 +2297,30 @@ fn panel_archive_cmd(project: String, dir: Option<String>) -> Result<String, Str
archive_panel_content(&project, dir.as_deref()).map(|p| p.display().to_string())
}
/// Zeigt einen Pfad im Dateimanager (nach dem Archivieren „im Finder zeigen").
#[tauri::command]
fn reveal_path_cmd(path: String) {
reveal_path(std::path::Path::new(&path));
}
#[cfg(target_os = "macos")]
fn reveal_path(path: &std::path::Path) {
let _ = Command::new("open").arg("-R").arg(path).spawn();
}
#[cfg(target_os = "linux")]
fn reveal_path(path: &std::path::Path) {
let target = path.parent().unwrap_or(path);
let _ = Command::new("xdg-open").arg(target).spawn();
}
#[cfg(target_os = "windows")]
fn reveal_path(path: &std::path::Path) {
let _ = Command::new("explorer")
.arg(format!("/select,{}", path.display()))
.spawn();
}
/// D-Bus-Relay für die GNOME-Extension: Show()/Hide() steuern das Popup-Fenster.
/// Die Extension ruft Show() beim Panel-Klick und positioniert das Fenster selbst.
#[cfg(target_os = "linux")]
@@ -2513,7 +2565,7 @@ fn terminal_builder(project: String) -> tauri::Builder<tauri::Wry> {
if let tauri::WindowEvent::Destroyed = event {
if window.label().starts_with("panel-") {
use tauri::{Emitter, Manager};
let _ = window.app_handle().emit("panel-attached", ());
let _ = window.app_handle().emit("panel-window-closed", ());
} else {
terminal::close(window);
}
@@ -2529,12 +2581,14 @@ fn terminal_builder(project: String) -> tauri::Builder<tauri::Wry> {
terminal::open_panel_window,
panel_archive_dir_cmd,
panel_archive_cmd,
reveal_path_cmd,
// Header im Terminal-Prozess: Projektliste, Icon und Pool-Name.
list_projects,
project_icon,
pool_label,
terminal_font_size,
set_terminal_font_size
set_terminal_font_size,
spellcheck_lang
])
}