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:
+161
-4
@@ -1,3 +1,4 @@
|
||||
mod mcp;
|
||||
mod terminal;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -57,6 +58,18 @@ impl Paths {
|
||||
fn pool_dir(&self, pool: &str) -> PathBuf {
|
||||
self.pools_dir().join(pool)
|
||||
}
|
||||
|
||||
/// Panel-Dateien pro Projekt: der Skill schreibt seinen Entwurf hier hinein,
|
||||
/// der Terminal-Prozess beobachtet die Datei und zeigt sie im Panel.
|
||||
fn panels_dir(&self) -> PathBuf {
|
||||
self.config_dir().join("panels")
|
||||
}
|
||||
}
|
||||
|
||||
/// Panel-Datei eines Projekts (Kanal Skill -> Panel). Der Pfad landet als
|
||||
/// AI_CONTROL_PANEL in der PTY-Umgebung.
|
||||
pub(crate) fn panel_file(project: &str) -> PathBuf {
|
||||
Paths::real().panels_dir().join(format!("{project}.md"))
|
||||
}
|
||||
|
||||
// ---------- Projekt-Registry ----------
|
||||
@@ -1194,6 +1207,7 @@ fn init_pool_config(
|
||||
let mut settings = serde_json::json!({
|
||||
"promptSuggestionEnabled": false,
|
||||
"awaySummaryEnabled": false,
|
||||
"permissions": { "allow": [PANEL_WRITE_PERMISSION] },
|
||||
});
|
||||
let base = settings.as_object_mut().unwrap();
|
||||
if let Some(obj) = extra.as_object() {
|
||||
@@ -1208,9 +1222,130 @@ fn init_pool_config(
|
||||
if !claude_md.is_file() {
|
||||
fs::write(&claude_md, "").map_err(|e| e.to_string())?;
|
||||
}
|
||||
install_panel_skill(dir);
|
||||
register_mcp_server(dir);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Skill, der das MCP-Tool `write_panel` für Entwürfe anweist. Liegt im
|
||||
/// Pool-Ordner (= CLAUDE_CONFIG_DIR), damit claude ihn als User-Skill findet.
|
||||
const PANEL_SKILL: &str = include_str!("../resources/panel-skill.md");
|
||||
|
||||
/// MCP-Server-Key — zugleich das Label, das claude im Tool-Call anzeigt
|
||||
/// („text panel"). Bestimmt den Tool-Namespace `mcp__<key>__<tool>`.
|
||||
const PANEL_MCP_SERVER: &str = "text-panel";
|
||||
|
||||
/// Freigabe für das MCP-Tool `write_panel` — damit der Aufruf ohne Rückfrage
|
||||
/// läuft. Namensschema: `mcp__<server>__<tool>`.
|
||||
const PANEL_WRITE_PERMISSION: &str = "mcp__text-panel__write_panel";
|
||||
|
||||
/// Schreibt/aktualisiert die Panel-Skill-Datei in einem Pool (überschreibt eine
|
||||
/// evtl. ältere, tee-basierte Fassung).
|
||||
fn install_panel_skill(pool_dir: &std::path::Path) {
|
||||
let skill_dir = pool_dir.join("skills").join("panel");
|
||||
if fs::create_dir_all(&skill_dir).is_ok() {
|
||||
let _ = fs::write(skill_dir.join("SKILL.md"), PANEL_SKILL);
|
||||
}
|
||||
}
|
||||
|
||||
/// Frühere Panel-Freigaben (Bash- und alter MCP-Key), die aus den Pool-Settings
|
||||
/// entfernt werden.
|
||||
const STALE_PANEL_PERMISSIONS: [&str; 3] =
|
||||
["Bash(tee:*)", "Bash(cat:*)", "mcp__aicontrol__write_panel"];
|
||||
|
||||
/// Trägt die MCP-Freigabe in die settings.json eines Pools ein und entfernt
|
||||
/// die alten Bash-Freigaben — idempotent, ohne sonstige Einträge zu verändern.
|
||||
fn ensure_panel_permission(pool_dir: &std::path::Path) {
|
||||
let sp = pool_dir.join("settings.json");
|
||||
let Ok(raw) = fs::read_to_string(&sp) else {
|
||||
return;
|
||||
};
|
||||
let Ok(mut v) = serde_json::from_str::<serde_json::Value>(&raw) else {
|
||||
return;
|
||||
};
|
||||
let Some(obj) = v.as_object_mut() else { return };
|
||||
let Some(perms) = obj
|
||||
.entry("permissions")
|
||||
.or_insert_with(|| serde_json::json!({}))
|
||||
.as_object_mut()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some(allow) = perms
|
||||
.entry("allow")
|
||||
.or_insert_with(|| serde_json::json!([]))
|
||||
.as_array_mut()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let before = allow.len();
|
||||
allow.retain(|e| !e.as_str().is_some_and(|s| STALE_PANEL_PERMISSIONS.contains(&s)));
|
||||
let has = allow.iter().any(|e| e.as_str() == Some(PANEL_WRITE_PERMISSION));
|
||||
if !has {
|
||||
allow.push(serde_json::json!(PANEL_WRITE_PERMISSION));
|
||||
}
|
||||
if has && allow.len() == before {
|
||||
return; // nichts geändert
|
||||
}
|
||||
if let Ok(out) = serde_json::to_string_pretty(&v) {
|
||||
let _ = fs::write(&sp, out + "\n");
|
||||
}
|
||||
}
|
||||
|
||||
/// Registriert den MCP-Server `aicontrol` (dieses Binary mit `--mcp-panel`) in
|
||||
/// der `.claude.json` des Pools (= CLAUDE_CONFIG_DIR, User-Scope für alle
|
||||
/// Projekte des Pools). Vorhandene .claude.json wird gemergt, nicht ersetzt.
|
||||
fn register_mcp_server(pool_dir: &std::path::Path) {
|
||||
let Ok(exe) = std::env::current_exe() else {
|
||||
return;
|
||||
};
|
||||
let cfg = pool_dir.join(".claude.json");
|
||||
let mut v = fs::read_to_string(&cfg)
|
||||
.ok()
|
||||
.and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
|
||||
.unwrap_or_else(|| serde_json::json!({}));
|
||||
let Some(obj) = v.as_object_mut() else { return };
|
||||
let Some(servers) = obj
|
||||
.entry("mcpServers")
|
||||
.or_insert_with(|| serde_json::json!({}))
|
||||
.as_object_mut()
|
||||
else {
|
||||
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,
|
||||
}),
|
||||
);
|
||||
if let Ok(out) = serde_json::to_string_pretty(&v) {
|
||||
let _ = fs::write(&cfg, out + "\n");
|
||||
}
|
||||
}
|
||||
|
||||
/// Panel-Skill, MCP-Server-Registrierung und Tool-Freigabe in einen Pool
|
||||
/// bringen. install_panel_skill überschreibt eine evtl. alte tee-Fassung.
|
||||
fn provision_pool(pool_dir: &std::path::Path) {
|
||||
install_panel_skill(pool_dir);
|
||||
register_mcp_server(pool_dir);
|
||||
ensure_panel_permission(pool_dir);
|
||||
}
|
||||
|
||||
/// Panel-MCP in alle vorhandenen Pools bringen (Migration beim App-Start).
|
||||
fn provision_pools_for_panel(paths: &Paths) {
|
||||
if let Ok(pools) = pool_names(paths) {
|
||||
for (id, _) in pools {
|
||||
provision_pool(&paths.pool_dir(&id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Runtime, die pro Pool ins synced Repo gelinkt wird: Transkripte, Todos,
|
||||
/// Prompt-Historie. (name, ist_ordner)
|
||||
const SYNCED_RUNTIME: [(&str, bool); 3] =
|
||||
@@ -1869,10 +2004,19 @@ fn link_pool_runtime(pool: String) -> Result<(), String> {
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
let mut args = std::env::args().skip(1);
|
||||
let first = args.next();
|
||||
|
||||
// `app --mcp-panel`: reiner MCP-stdio-Server (write_panel), kein Tauri/GTK.
|
||||
// Von claude als Tool-Server gestartet.
|
||||
if first.as_deref() == Some("--mcp-panel") {
|
||||
mcp::run_mcp_panel();
|
||||
return;
|
||||
}
|
||||
|
||||
// generate_context! darf pro Crate nur einmal expandieren (_EMBED_INFO_PLIST).
|
||||
let context = tauri::generate_context!();
|
||||
let mut args = std::env::args().skip(1);
|
||||
match args.next().as_deref() {
|
||||
match first.as_deref() {
|
||||
// `app --terminal <projekt>`: eigener Prozess pro Terminal-Fenster,
|
||||
// damit jedes Terminal ein eigenes Dock-Icon bekommt.
|
||||
Some("--terminal") => {
|
||||
@@ -2020,6 +2164,10 @@ fn main_builder() -> tauri::Builder<tauri::Wry> {
|
||||
// Session-Watcher: synct bei Session-Ende (Prozess verschwindet).
|
||||
spawn_session_watcher();
|
||||
|
||||
// Panel-MCP-Server + Tool-Freigabe in alle Pools legen (auch bestehende),
|
||||
// damit write_panel in jedem CLAUDE_CONFIG_DIR ohne Rückfrage bereitsteht.
|
||||
provision_pools_for_panel(&Paths::real());
|
||||
|
||||
// Alle pro-Terminal-.desktop-Dateien neu schreiben + verwaiste entfernen,
|
||||
// damit sie da sind, bevor ein Terminal startet.
|
||||
#[cfg(target_os = "linux")]
|
||||
@@ -2182,10 +2330,17 @@ fn terminal_builder(project: String) -> tauri::Builder<tauri::Wry> {
|
||||
terminal::build_window(app.handle(), &project, &cfg)?;
|
||||
Ok(())
|
||||
})
|
||||
// Fenster zu → PTY-Session abräumen; danach endet der Prozess.
|
||||
// Fenster zu → PTY-Session abräumen; danach endet der Prozess. Das
|
||||
// abgelöste Panel-Fenster hat keine PTY: sein Schließen dockt das Panel
|
||||
// wieder an (panel-attached), der Prozess läuft weiter.
|
||||
.on_window_event(|window, event| {
|
||||
if let tauri::WindowEvent::Destroyed = event {
|
||||
terminal::close(window);
|
||||
if window.label().starts_with("panel-") {
|
||||
use tauri::{Emitter, Manager};
|
||||
let _ = window.app_handle().emit("panel-attached", ());
|
||||
} else {
|
||||
terminal::close(window);
|
||||
}
|
||||
}
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
@@ -2193,6 +2348,8 @@ fn terminal_builder(project: String) -> tauri::Builder<tauri::Wry> {
|
||||
terminal::term_log,
|
||||
terminal::term_write,
|
||||
terminal::term_resize,
|
||||
terminal::panel_read,
|
||||
terminal::open_panel_window,
|
||||
// Header im Terminal-Prozess: Projektliste, Icon und Pool-Name.
|
||||
list_projects,
|
||||
project_icon,
|
||||
|
||||
Reference in New Issue
Block a user