Entwurfs-Panel mit MCP-Tool write_panel

- Andockbares Panel im Terminal-Fenster: Markdown-Rendering, Copy-Button,
  normale Selektion; in eigenes Fenster ablösbar und wieder andockbar
  (Splitter, panel-detached/panel-attached).
- MCP-stdio-Server (app --mcp-panel) mit Tool write_panel; schreibt in die
  Datei aus AI_CONTROL_PANEL, Watcher zieht sie ins Panel.
- Provisionierung je Pool: Server-Registrierung in .claude.json (alwaysLoad),
  Freigabe mcp__text-panel__write_panel in settings.json, Panel-Skill; bei
  App-Start (alle Pools) und Pool-Anlage. Alte tee-Freigaben werden entfernt.
- Capability deckt panel-*-Fenster ab.
This commit is contained in:
marcus.hinz
2026-07-11 12:44:51 +02:00
parent c39330bffb
commit 8aa758ae64
13 changed files with 823 additions and 8 deletions
+66
View File
@@ -186,10 +186,20 @@ pub fn term_start(
.openpty(PtySize { rows, cols, pixel_width: 0, pixel_height: 0 })
.map_err(|e| e.to_string())?;
// Panel-Kanal: leere Datei anlegen (definierter Startzustand für den
// Watcher) und ihren Pfad als AI_CONTROL_PANEL in die PTY geben — der Skill
// schreibt seinen Entwurf dorthin.
let panel_path = crate::panel_file(&project);
if let Some(parent) = panel_path.parent() {
let _ = std::fs::create_dir_all(parent);
}
let _ = std::fs::write(&panel_path, "");
let mut cmd = CommandBuilder::new("/bin/zsh");
cmd.args(["-ic", &crate::claude_command(&paths)]);
cmd.cwd(&cwd);
cmd.env("TERM", "xterm-256color");
cmd.env("AI_CONTROL_PANEL", &panel_path);
if let Some(pool_dir) = crate::project_pool_dir(&project)? {
cmd.env("CLAUDE_CONFIG_DIR", pool_dir);
}
@@ -246,6 +256,8 @@ pub fn term_start(
let _ = app.emit_to(&label, "pty-exit", ());
});
spawn_panel_watcher(window.app_handle().clone(), panel_path);
terminals.0.lock().unwrap().insert(
window.label().to_string(),
Session { writer, master: pty.master, killer },
@@ -253,6 +265,60 @@ pub fn term_start(
Ok(())
}
/// Beobachtet die Panel-Datei des Projekts und schickt neuen Inhalt als
/// `panel-update` an alle Fenster dieses Terminal-Prozesses (angedocktes Panel
/// und ein evtl. abgelöstes Panel-Fenster). Pollt per mtime — kein notify-Crate,
/// die Datei ändert sich nur, wenn ein Entwurf geschrieben wird. Der Thread
/// endet mit dem Prozess (Fenster zu).
fn spawn_panel_watcher(app: AppHandle, path: std::path::PathBuf) {
std::thread::spawn(move || {
let mtime = || std::fs::metadata(&path).and_then(|m| m.modified()).ok();
let mut last = mtime();
loop {
std::thread::sleep(std::time::Duration::from_millis(200));
let now = mtime();
if now != last {
last = now;
if let Ok(content) = std::fs::read_to_string(&path) {
let _ = app.emit("panel-update", content);
}
}
}
});
}
/// Aktueller Panel-Inhalt (Erstbefüllung eines gerade geöffneten Panel-Fensters).
#[tauri::command]
pub fn panel_read(project: String) -> String {
std::fs::read_to_string(crate::panel_file(&project)).unwrap_or_default()
}
/// Löst das Panel in ein eigenes Fenster ab. Existiert es schon, kommt es nach
/// vorn. `panel-detached` blendet das angedockte Panel im Terminal-Fenster aus.
#[tauri::command]
pub fn open_panel_window(app: AppHandle, project: String) -> Result<(), String> {
let label = format!("panel-{project}");
if let Some(w) = app.get_webview_window(&label) {
let _ = w.set_focus();
return Ok(());
}
let cfg = crate::terminal_config(&project)?;
let (r, g, b) = theme_background(cfg.theme.as_deref().unwrap_or_default());
let title = cfg.title.as_deref().unwrap_or(project.as_str());
WebviewWindowBuilder::new(
&app,
&label,
WebviewUrl::App(format!("panel.html?project={project}").into()),
)
.title(format!("{title} — Entwurf"))
.inner_size(480.0, 640.0)
.background_color(tauri::window::Color(r, g, b, 0xff))
.build()
.map_err(|e| e.to_string())?;
app.emit("panel-detached", ()).map_err(|e| e.to_string())?;
Ok(())
}
/// Debug: JS-Fehler aus dem Terminal-Fenster ins Dev-Log.
#[tauri::command]
pub fn term_log(window: tauri::WebviewWindow, msg: String) {