claude-desktop/src-tauri/src/programs.rs
Eddy 79b8525ede Bugfixes: Resume, Tool-Whitelist, Sub-Agent-Tree, UI-Polish
Bridge (claude-bridge.js):
- Resume-Fix: queryOptions.resume statt .sessionId (SDK-API)
- tools-Whitelist statt disallowedTools (Blacklist vererbt sich auf Sub-Agents!)
  Handlanger: Main nur Task+TodoWrite, Sub-Agents bekommen volles Tool-Set
  Experten: Main nur Task+TodoWrite+Read+Grep+Glob
  Solo: preset claude_code
- handleToolUse/handleToolResult Helper, greifen auch in assistant.content-Bloecken
  (SDK liefert tool_use/tool_result nicht als standalone events)
- Dedup via handledTools Set
- Resume-Retry-Fallback bei ungueltiger Session-ID
- Custom agents-Option entfernt (SDK spawnt Sub-Agents ohne Tools → Halluzination)
- Orchestrator-Prompt: verweist auf general-purpose (vollstaendiges Tool-Set)

Backend (claude.rs):
- claude_session_id NUR beim 1. Mal setzen (sonst verliert man History)
- Generic event emit fuer alle Bridge-Events ans Frontend
- Mode-Persistenz bei Bridge-Start (agent_mode aus DB laden)

Knowledge (knowledge.rs):
- MYSQL_HOST: 192.168.155.1 → 192.168.155.11 (MariaDB-Server)
- MYSQL_PASS: claude → 8715
- category Option<&str> Typ-Annotation fuer exec_map

Programs (programs.rs):
- xvfb_screenshot: Fallback scrot → import (ImageMagick) → ffmpeg

Voice (voice.rs):
- Part::file (existiert nicht) → Part::bytes, keine Temp-Datei

Frontend:
- events.ts: mode-changed Listener, result.text Fallback,
  addAgent({id}) fuer korrekte Parent-Child-Verknuepfung
- ChatPanel: Copy-Button, Typing-Dots in Bubble (kein Doppel-Header),
  $effect statt $:, onkeydown statt on:keydown
- AgentView: "Nur aktive" Toggle, Delegations-Badge, Tool-Count hidden bei 0,
  agentMode Import
- ProgramsPanel: Button-Styling, Error-Banner mit Copy-Button,
  selectable Text
- MonitorPanel: Filter-Dropdown Styling (Hintergrund + Hover)
- SettingsPanel: changeMode() wird beim Klick aufgerufen (nicht nur Store)
- +layout.svelte: agent_mode beim App-Start laden, Mode-Badge im Footer,
  🎓-Button fuer Schulungsfenster
- +page.svelte: Programme-Tab + Hooks-Tab

Neue Dateien:
- TEST-ROADMAP.md — Status und naechste Schritte
- .gitignore erweitert (scheduled_tasks.lock, out/, node_modules)
- vscode-extension/tsconfig.json: include nur src/, exclude node_modules

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 21:24:51 +02:00

230 lines
7 KiB
Rust

// Claude Desktop — Programm-Steuerung
// D-Bus: Linux-Apps via dbus-send/qdbus
// Xvfb: Virtuelles Display fuer Computer-Use (Scaffold)
// Playwright: Tool-Hinweise (eigentliche Steuerung laeuft ueber MCP-Server)
use serde::{Deserialize, Serialize};
use std::process::Command;
// ============ D-Bus ============
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DbusCallResult {
pub success: bool,
pub stdout: String,
pub stderr: String,
pub exit_code: i32,
}
#[tauri::command]
pub async fn dbus_call(
service: String,
path: String,
method: String,
args: Option<Vec<String>>,
session: Option<bool>,
) -> Result<DbusCallResult, String> {
let bus = if session.unwrap_or(true) { "--session" } else { "--system" };
let mut cmd = Command::new("dbus-send");
cmd.arg("--print-reply")
.arg(bus)
.arg(format!("--dest={}", service))
.arg(&path)
.arg(&method);
if let Some(args) = args {
for a in args {
cmd.arg(a);
}
}
let output = cmd.output().map_err(|e| format!("dbus-send Fehler: {}", e))?;
Ok(DbusCallResult {
success: output.status.success(),
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
exit_code: output.status.code().unwrap_or(-1),
})
}
#[tauri::command]
pub async fn dbus_list_services(session: Option<bool>) -> Result<Vec<String>, String> {
let bus = if session.unwrap_or(true) { "--session" } else { "--system" };
let output = Command::new("dbus-send")
.arg("--print-reply")
.arg(bus)
.arg("--dest=org.freedesktop.DBus")
.arg("/org/freedesktop/DBus")
.arg("org.freedesktop.DBus.ListNames")
.output()
.map_err(|e| format!("dbus-send ListNames: {}", e))?;
if !output.status.success() {
return Err(String::from_utf8_lossy(&output.stderr).to_string());
}
let text = String::from_utf8_lossy(&output.stdout);
let mut services: Vec<String> = text
.lines()
.filter_map(|l| {
let t = l.trim();
if t.starts_with("string \"") && t.ends_with('"') {
Some(t[8..t.len() - 1].to_string())
} else {
None
}
})
.filter(|s| !s.starts_with(':'))
.collect();
services.sort();
services.dedup();
Ok(services)
}
// ============ Xvfb (Virtuelles Display) ============
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct XvfbStatus {
pub running: bool,
pub display_num: u16,
pub pid: Option<u32>,
pub resolution: String,
}
static XVFB_STATE: std::sync::OnceLock<std::sync::Mutex<XvfbStatus>> = std::sync::OnceLock::new();
fn xvfb_state() -> &'static std::sync::Mutex<XvfbStatus> {
XVFB_STATE.get_or_init(|| {
std::sync::Mutex::new(XvfbStatus {
running: false,
display_num: 1,
pid: None,
resolution: "1920x1080x24".into(),
})
})
}
#[tauri::command]
pub async fn xvfb_start(display_num: Option<u16>, resolution: Option<String>) -> Result<XvfbStatus, String> {
let display = display_num.unwrap_or(1);
let res = resolution.unwrap_or_else(|| "1920x1080x24".into());
// Pruefen ob Xvfb verfuegbar
let check = Command::new("which").arg("Xvfb").output();
if check.map(|o| !o.status.success()).unwrap_or(true) {
return Err("Xvfb nicht installiert. Auf NixOS: nixpkgs.xorg.xvfb".into());
}
let child = Command::new("Xvfb")
.arg(format!(":{}", display))
.arg("-screen")
.arg("0")
.arg(&res)
.spawn()
.map_err(|e| format!("Xvfb-Start fehlgeschlagen: {}", e))?;
let status = XvfbStatus {
running: true,
display_num: display,
pid: Some(child.id()),
resolution: res,
};
*xvfb_state().lock().unwrap() = status.clone();
Ok(status)
}
#[tauri::command]
pub async fn xvfb_stop() -> Result<XvfbStatus, String> {
let mut state = xvfb_state().lock().unwrap();
if let Some(pid) = state.pid {
let _ = Command::new("kill").arg(pid.to_string()).output();
}
state.running = false;
state.pid = None;
Ok(state.clone())
}
#[tauri::command]
pub async fn xvfb_status() -> Result<XvfbStatus, String> {
Ok(xvfb_state().lock().unwrap().clone())
}
#[tauri::command]
pub async fn xvfb_screenshot(display_num: Option<u16>) -> Result<String, String> {
// Screenshot vom virtuellen Display — probiert mehrere Tools durch
let display = display_num.unwrap_or(1);
let display_env = format!(":{}", display);
let tmp = std::env::temp_dir().join(format!("claude-xvfb-{}.png", uuid::Uuid::new_v4()));
// Versuchte Kommandos in Reihenfolge
let attempts: Vec<(&str, Vec<String>)> = vec![
("scrot", vec!["-q".into(), "80".into(), tmp.to_string_lossy().into_owned()]),
("import", vec!["-window".into(), "root".into(), tmp.to_string_lossy().into_owned()]),
("ffmpeg", vec![
"-f".into(), "x11grab".into(),
"-i".into(), display_env.clone(),
"-frames:v".into(), "1".into(),
"-y".into(),
tmp.to_string_lossy().into_owned(),
]),
];
let mut last_err = String::new();
for (cmd, args) in &attempts {
// Tool vorhanden?
if Command::new("which").arg(cmd).output().map(|o| !o.status.success()).unwrap_or(true) {
last_err = format!("'{}' nicht installiert", cmd);
continue;
}
let result = Command::new(cmd)
.env("DISPLAY", &display_env)
.args(args)
.output();
match result {
Ok(o) if o.status.success() && tmp.exists() => {
let bytes = std::fs::read(&tmp).map_err(|e| e.to_string())?;
let _ = std::fs::remove_file(&tmp);
use base64::Engine;
return Ok(base64::engine::general_purpose::STANDARD.encode(&bytes));
}
Ok(o) => {
last_err = format!("{} Exit {}: {}", cmd, o.status, String::from_utf8_lossy(&o.stderr));
}
Err(e) => {
last_err = format!("{} Fehler: {}", cmd, e);
}
}
}
Err(format!(
"Screenshot fehlgeschlagen. Installiere eines: scrot / imagemagick / ffmpeg.\nLetzter Fehler: {}",
last_err
))
}
// ============ Playwright-Infos ============
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlaywrightInfo {
pub available: bool,
pub hint: String,
}
#[tauri::command]
pub async fn playwright_info() -> Result<PlaywrightInfo, String> {
// Playwright laeuft bei Eddy ueber MCP — hier nur Info fuer das UI
Ok(PlaywrightInfo {
available: true,
hint: "Playwright-Steuerung laeuft ueber den MCP-Server. Nutze die Tools \
mcp__plugin_playwright_playwright__browser_navigate, \
_click, _snapshot etc. im Chat. Für Dolibarr-Automation \
empfohlen: Session-Login via MCP-Playwright speichern."
.into(),
})
}