From 79b8525ede02959ed6c9495612e987d310a69f8e Mon Sep 17 00:00:00 2001 From: Eddy Date: Tue, 14 Apr 2026 21:24:51 +0200 Subject: [PATCH] Bugfixes: Resume, Tool-Whitelist, Sub-Agent-Tree, UI-Polish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .gitignore | 3 + TEST-ROADMAP.md | 75 +++++++ scripts/claude-bridge.js | 255 ++++++++++++++---------- src-tauri/src/claude.rs | 6 +- src-tauri/src/knowledge.rs | 4 +- src-tauri/src/programs.rs | 60 ++++-- src/lib/components/AgentView.svelte | 40 +++- src/lib/components/ChatPanel.svelte | 50 ++++- src/lib/components/MonitorPanel.svelte | 16 +- src/lib/components/ProgramsPanel.svelte | 110 +++++++++- src/lib/components/SettingsPanel.svelte | 2 +- src/lib/stores/events.ts | 12 +- src/routes/+layout.svelte | 10 + vscode-extension/tsconfig.json | 7 +- 14 files changed, 502 insertions(+), 148 deletions(-) create mode 100644 TEST-ROADMAP.md diff --git a/.gitignore b/.gitignore index 9b7152f..4c0b3bc 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,6 @@ Thumbs.db # Package lock (use npm ci) package-lock.json +.claude/scheduled_tasks.lock +vscode-extension/out/ +vscode-extension/node_modules/ diff --git a/TEST-ROADMAP.md b/TEST-ROADMAP.md new file mode 100644 index 0000000..05ac863 --- /dev/null +++ b/TEST-ROADMAP.md @@ -0,0 +1,75 @@ +# Claude Desktop — Test-Roadmap (Fortsetzung) + +**Stand:** 14.04.2026 · Session-Ende nahe Token-Limit + +## Was bereits getestet & funktioniert +- ✅ Hooks-Panel: 5 Built-in Hooks sichtbar & toggelbar +- ✅ D-Bus: 80+ Services laden (Programme → D-Bus) +- ✅ Schulungs-Fenster öffnet via 🎓-Button +- ✅ Modus-Auswahl in Settings + Footer-Badge +- ✅ Chat funktioniert (nach Bridge-Fixes: `resume` statt `sessionId`, claude_session_id nur bei erstem Call setzen) +- ✅ Sub-Agent erscheint im Tree (nach `addAgent({id})` Fix) +- ✅ Sub-Agent "Nur aktive" Toggle +- ✅ Filter-Dropdown im Monitor-Panel sichtbar +- ✅ Copy-Button in Chat-Nachrichten +- ✅ Error-Banner mit kopierbarem Text im Programme-Panel + +## Offene Bugs (Reihenfolge der Priorität) + +### 1. Chat-Antwort bei komplexen Flows fehlt (HALB GEFIXT) +**Symptom:** Bei Handlanger-Chats mit Sub-Agent wird die finale Antwort nicht im Chat angezeigt. +**Ursache:** Streaming-Text-Events kommen nicht, nur `result.text` am Ende. +**Fix (drin):** Fallback in `events.ts` auf `result.text` wenn `content` leer. +**Zu verifizieren:** Nächster Chat im Handlanger-Modus — erscheint jetzt die Antwort? + +### 2. Date-Panic in Wissensbasis +**Symptom:** `Couldn't convert Row... Date(...) to String` bei jeder Wissens-Suche / Tool-Hints. +**Ursache:** MySQL liefert TIMESTAMP als `Value::Date`, Tupel erwartet `String`. +**Fix:** 7 SELECTs in [knowledge.rs](src-tauri/src/knowledge.rs) einzeln auf `chrono::NaiveDateTime` umstellen. +**Nicht:** `replace_all` auf "created_at, updated_at" — das zerstört Rust-Tupel-Identifier (schon 1× passiert). + +### 3. VSCodium-Extension nicht getestet +**Was zu tun:** +- `cd vscode-extension && npm run compile` (bereits OK) +- VSCodium öffnen, Extension via F5 in Dev-Host laden +- App: Programme → 🧩 VSCodium → Port 7890 → Verbinden +- Ping-Test, Datei öffnen + +### 4. Xvfb-Screenshot fehlt Tool +**Status:** Xvfb-Start funktioniert, Screenshot braucht `imagemagick` (scrot/ffmpeg-x11 fehlen in NixOS-Build). +**Fix:** `imagemagick` in `/etc/nixos/configuration.nix` → `nixos-rebuild switch`. + +### 5. Experten-Modus nicht getestet +**Analog zu Handlanger:** neue Session, Experten-Modus, Aufgabe mit Research/Implement-Charakter. + +### 6. Haiku-Kostenersparnis funktioniert nicht +**Status:** Sub-Agents laufen auf Opus (inherit vom Main). Custom `agents`-Option in SDK scheint ignoriert zu werden bzw. spawnt Agents ohne Tools (halluziniert). +**Nächster Ansatz:** Im Orchestrator-Prompt Claude explizit vorgeben `model: "haiku"` in Task-Calls zu setzen. Ob das SDK das respektiert, ist offen. + +## Uncommitted Changes (alles sinnvolle Fixes — lohnt sich zu committen) +- `scripts/claude-bridge.js` — resume-Fix, tools-Whitelist, handleToolUse/Result Helper, Dedup +- `src-tauri/src/claude.rs` — claude_session_id nur 1× setzen, generic event emit +- `src-tauri/src/knowledge.rs` — IP+PW korrekt (155.11/8715) +- `src/lib/stores/events.ts` — mode-changed Listener, result.text Fallback, addAgent({id}) +- `src/lib/components/ChatPanel.svelte` — Copy-Button, Typing-Dots in Bubble (kein Doppel-Header) +- `src/lib/components/AgentView.svelte` — Nur-aktive-Toggle, Delegations-Badge, Tool-Count hidden bei 0 +- `src/lib/components/ProgramsPanel.svelte` — Error-Banner mit Copy +- `src/lib/components/MonitorPanel.svelte` — Filter-Dropdown Styling +- `src/routes/+layout.svelte` — agent_mode beim Start laden +- `src/routes/+page.svelte` — Tabs Programme + Hooks + +## Schnellstart nach Neustart +```bash +cd "/mnt/17 - Entwicklungen/20 - Projekte/ClaudeDesktop" +CARGO_TARGET_DIR=/tmp/claude-desktop-target nix-shell --run "npx tauri dev" +# Dauert ~15s beim ersten Start nach Reboot wenn /tmp leer ist +``` + +## DB-Reset wenn Claude-Session-IDs veraltet +```bash +nix-shell -p sqlite --run 'sqlite3 "/home/data/.local/share/de.alles-watt-laeuft.claude-desktop/claude-desktop.db" "UPDATE sessions SET claude_session_id = NULL;"' +``` + +## Nächster Commit +Alles zusammen ein großer Bugfix-Commit mit Titel: +> Fix: Resume, tools-Whitelist, Sub-Agent-Tree, Date-Handling, UI-Polish diff --git a/scripts/claude-bridge.js b/scripts/claude-bridge.js index 8dc06ae..20652dd 100644 --- a/scripts/claude-bridge.js +++ b/scripts/claude-bridge.js @@ -29,26 +29,30 @@ let stickyContext = ''; const ORCHESTRATOR_PROMPTS = { handlanger: ` -Du bist der HAUPT-AGENT und arbeitest im HANDLANGER-MODUS. +Du bist der HAUPT-AGENT im HANDLANGER-MODUS. -WICHTIG: Du denkst und planst, aber Sub-Agents führen aus! +KRITISCH: Dir stehen NUR Task + TodoWrite zur Verfügung. +Du kannst NICHT direkt lesen, suchen oder ausführen — du MUSST delegieren! -Dir steht das Task-Tool zur Verfügung mit dem Sub-Agent-Typ "worker". -Der Worker läuft auf Haiku (günstig) und führt genau aus was du sagst. +Task-Tool mit den RICHTIGEN Sub-Agent-Typen: +- "general-purpose" — Standard-Agent mit VOLLEM Tool-Zugriff (Bash, Read, Write, Grep, Glob). + Nutze diesen für JEDE Aufgabe die Bash/Shell benötigt (ls, cat, find, grep auf System-Ebene). +- "Explore" — read-only Agent. NUR für reine Code-/Dateisuche innerhalb des Projekts. + Hat KEINEN Bash-Zugriff! Nicht für Systembefehle wie "ls /etc" verwenden. -Arbeitsweise: -1. ANALYSIERE die Aufgabe -2. Zerlege in EXAKTE Ausführungsschritte -3. Delegiere jeden Schritt per Task(subagent_type: "worker", prompt: "...") -4. Formuliere den Task-Prompt als exakte Anweisung, nicht als Frage -5. Sammle die Zusammenfassungen, entscheide den nächsten Schritt +Arbeitsweise (verbindlich): +1. Wähle den RICHTIGEN subagent_type basierend auf der Aufgabe. +2. Rufe das Task-Tool auf mit EXAKTER Anweisung. +3. Prüfe im Ergebnis das "tool_uses"-Feld. Wenn tool_uses:0 → Sub-Agent hat halluziniert! + In dem Fall: Neuer Task-Call mit "general-purpose" statt "Explore". +4. Verarbeite das Ergebnis und gib dem User die Zusammenfassung. -Beispiel-Delegationen: -- Task(subagent_type:"worker", prompt:"Lies src/lib.rs, gib mir Zeilen 10-50 zurück") -- Task(subagent_type:"worker", prompt:"Suche 'handleError' in src/ via Grep, liste die Dateien") -- Task(subagent_type:"worker", prompt:"Führe 'npm test' aus, berichte nur passed/failed + Fehlerzeile") +Halluziniere NIEMALS Dateilisten aus dem Gedächtnis — delegiere immer real. -Halte deinen Context klein — lass den Worker die Rohdaten bearbeiten! +Beispiele: +- "List /etc files" → Task(subagent_type:"general-purpose", prompt:"Run 'ls -1 /etc | sort' and return the output") +- "Find handleError in src/" → Task(subagent_type:"Explore", prompt:"Grep for 'handleError' in src/") +- "Read /etc/hosts" → Task(subagent_type:"general-purpose", prompt:"cat /etc/hosts and return output") `, experten: ` @@ -373,49 +377,150 @@ async function sendMessage(message, requestId, model = null, contextOverride = n abortController: activeAbort, }; - // Session-ID für Fortsetzung hinzufügen wenn vorhanden + // Session-ID für Fortsetzung — SDK erwartet `resume`, nicht `sessionId` if (resumeSessionId) { - queryOptions.sessionId = resumeSessionId; + queryOptions.resume = resumeSessionId; } - // Tool-Filterung + Custom Sub-Agents je nach effektivem Modus - // Handlanger: Main darf NUR delegieren (Task) und planen (TodoWrite) - // Sub-Agents laufen auf Haiku (siehe HANDLANGER_AGENTS) - // Experten: Main darf zusätzlich lesen/suchen, aber nicht schreiben - // Sub-Agents sind autonome Research/Implement/Test/Review + // Tool-Konfig je nach Modus. + // WICHTIG: disallowedTools vererbt sich auf Sub-Agents! + // Deshalb Whitelist via `tools` nutzen — die gilt nur fuer Main, + // Sub-Agents bekommen das volle Standard-Tool-Set. + queryOptions.allowedTools = ['Task', 'TodoWrite', 'Read', 'Grep', 'Glob', 'Write', 'Edit', 'Bash']; + if (effectiveMode === 'handlanger') { - queryOptions.allowedTools = ['Task', 'TodoWrite']; - queryOptions.agents = HANDLANGER_AGENTS; - sendMonitorEvent('agent', 'Handlanger-Modus: Main → Task+TodoWrite, Worker auf Haiku', { + queryOptions.tools = ['Task', 'TodoWrite']; + sendMonitorEvent('agent', 'Handlanger: Main nur Task+TodoWrite, Sub-Agents mit vollem Tool-Set', { mode: effectiveMode, - allowedTools: queryOptions.allowedTools, - agents: Object.keys(HANDLANGER_AGENTS), + mainTools: queryOptions.tools, }); } else if (effectiveMode === 'experten') { - queryOptions.allowedTools = ['Task', 'TodoWrite', 'Read', 'Grep', 'Glob']; - queryOptions.agents = EXPERTEN_AGENTS; - sendMonitorEvent('agent', 'Experten-Modus: 4 autonome Experten verfügbar', { + queryOptions.tools = ['Task', 'TodoWrite', 'Read', 'Grep', 'Glob']; + sendMonitorEvent('agent', 'Experten: Main lesen+delegieren, Sub-Agents voll', { mode: effectiveMode, - allowedTools: queryOptions.allowedTools, - agents: Object.keys(EXPERTEN_AGENTS), + mainTools: queryOptions.tools, }); + } else { + // solo: volles Preset + queryOptions.tools = { type: 'preset', preset: 'claude_code' }; } - // solo: keine Einschränkung - const conversation = query({ + let conversation = query({ prompt: fullPrompt, options: queryOptions, }); - for await (const event of conversation) { + // Dedupe: Manche Tool-Events kommen sowohl in assistant-Blocks + // als auch als standalone tool_use Event. Via toolUseId deduplizieren. + const handledTools = new Set(); + + // Tool-Use handhaben — funktioniert sowohl fuer standalone tool_use Events + // als auch fuer tool_use Bloecke innerhalb einer assistant-Nachricht + function handleToolUse(ev) { + const toolId = ev.tool_use_id || ev.id || randomUUID(); + if (handledTools.has(toolId)) return; + handledTools.add(toolId); + + const toolName = ev.name || 'unknown'; + const toolInput = ev.input || {}; + + if (SUBAGENT_TOOLS.includes(toolName)) { + const subagentId = randomUUID(); + const subagentType = getSubagentType(toolName, toolInput); + const subagentTask = toolInput.description || toolInput.prompt || toolInput.task || 'Subagent-Aufgabe'; + const subagentModel = toolInput.model || useModel; + const depth = 1; + + activeSubagents.set(toolId, { + agentId: subagentId, + parentId: currentAgentId, + type: subagentType, + task: subagentTask, + depth, + model: subagentModel, + }); + + sendEvent('subagent-started', { + id: subagentId, + parentAgentId: currentAgentId, + type: subagentType, + task: subagentTask.substring(0, 100), + depth, + model: subagentModel, + toolUseId: toolId, + }); + } + + sendEvent('tool-start', { + id: toolId, + tool: toolName, + input: toolInput, + agentId: currentAgentId, + }); + + const toolSummary = summarizeToolInput(toolName, toolInput); + sendMonitorEvent('tool', `${toolName} ${toolSummary}`, { + toolId, + tool: toolName, + input: toolInput, + }); + } + + // Tool-Result handhaben + function handleToolResult(ev) { + const toolId = ev.tool_use_id || ''; + + if (activeSubagents.has(toolId)) { + const subagent = activeSubagents.get(toolId); + sendEvent('subagent-stopped', { + id: subagent.agentId, + parentAgentId: subagent.parentId, + success: !ev.is_error, + toolUseId: toolId, + }); + activeSubagents.delete(toolId); + } + + sendEvent('tool-end', { + id: toolId, + success: !ev.is_error, + agentId: currentAgentId, + }); + } + + // Hilfsfunktion: Iteration mit Fallback bei ungueltiger Session-ID + async function* iterateWithRetry() { + try { + for await (const ev of conversation) yield ev; + } catch (err) { + // Wenn Resume-Session ungueltig → Retry ohne sessionId + if (queryOptions.sessionId) { + sendMonitorEvent('agent', 'Resume fehlgeschlagen, starte neue Session', { + reason: err.message || String(err), + oldSessionId: queryOptions.sessionId, + }); + delete queryOptions.sessionId; + conversation = query({ prompt: fullPrompt, options: queryOptions }); + for await (const ev of conversation) yield ev; + } else { + throw err; + } + } + } + + for await (const event of iterateWithRetry()) { switch (event.type) { case 'assistant': - // Text aus der Nachricht extrahieren + // Content-Bloecke durchgehen (Text, tool_use, thinking, ...) if (event.message?.content) { for (const block of event.message.content) { if (block.type === 'text' && block.text) { fullText += block.text; sendEvent('text', { text: block.text }); + } else if (block.type === 'tool_use') { + // Tool-Call von Main-Agent — manuell weiterreichen, damit + // der tool_use-Case weiter unten greift + handleToolUse(block); } } } @@ -425,78 +530,24 @@ async function sendMessage(message, requestId, model = null, contextOverride = n break; case 'tool_use': { - const toolId = event.tool_use_id || randomUUID(); - const toolName = event.name || 'unknown'; - const toolInput = event.input || {}; - - // Prüfen ob dieses Tool einen Subagent startet - if (SUBAGENT_TOOLS.includes(toolName)) { - const subagentId = randomUUID(); - const subagentType = getSubagentType(toolName, toolInput); - const subagentTask = toolInput.description || toolInput.prompt || toolInput.task || 'Subagent-Aufgabe'; - const subagentModel = toolInput.model || useModel; - - // Tiefe berechnen (Main = 0, erster Sub = 1, etc.) - // Für jetzt: immer depth 1 (direkter Subagent vom Main) - const depth = 1; - - activeSubagents.set(toolId, { - agentId: subagentId, - parentId: currentAgentId, - type: subagentType, - task: subagentTask, - depth, - model: subagentModel, - }); - - sendEvent('subagent-started', { - id: subagentId, - parentAgentId: currentAgentId, - type: subagentType, - task: subagentTask.substring(0, 100), - depth, - model: subagentModel, - toolUseId: toolId, - }); - } - - sendEvent('tool-start', { - id: toolId, - tool: toolName, - input: toolInput, - agentId: currentAgentId, - }); - - // Monitor: Tool gestartet - const toolSummary = summarizeToolInput(toolName, toolInput); - sendMonitorEvent('tool', `${toolName} ${toolSummary}`, { - toolId, - tool: toolName, - input: toolInput, - }); + handleToolUse(event); break; } case 'tool_result': { - const toolId = event.tool_use_id || ''; + handleToolResult(event); + break; + } - // Prüfen ob dieser Tool-Call ein Subagent war - if (activeSubagents.has(toolId)) { - const subagent = activeSubagents.get(toolId); - sendEvent('subagent-stopped', { - id: subagent.agentId, - parentAgentId: subagent.parentId, - success: !event.is_error, - toolUseId: toolId, - }); - activeSubagents.delete(toolId); + case 'user': { + // tool_result kommt vom SDK meist als Block innerhalb user-message + if (event.message?.content) { + for (const block of event.message.content) { + if (block.type === 'tool_result') { + handleToolResult(block); + } + } } - - sendEvent('tool-end', { - id: toolId, - success: !event.is_error, - agentId: currentAgentId, - }); break; } diff --git a/src-tauri/src/claude.rs b/src-tauri/src/claude.rs index bfadc10..b326dc8 100644 --- a/src-tauri/src/claude.rs +++ b/src-tauri/src/claude.rs @@ -228,7 +228,11 @@ fn handle_bridge_message(app: &AppHandle, msg: BridgeMessage) { if let Ok(Some(active_id)) = db_lock.get_setting("active_session_id") { if !active_id.is_empty() { if let Ok(Some(mut session)) = db_lock.get_session(&active_id) { - session.claude_session_id = Some(sid.to_string()); + // claude_session_id nur beim ersten Mal setzen — + // sonst verlieren Folge-Chats den Kontext der Anfangs-History + if session.claude_session_id.is_none() { + session.claude_session_id = Some(sid.to_string()); + } session.message_count += 1; if let Some(cost) = payload.get("cost").and_then(|v| v.as_f64()) { session.cost_usd += cost; diff --git a/src-tauri/src/knowledge.rs b/src-tauri/src/knowledge.rs index 48f5d4d..579eb14 100644 --- a/src-tauri/src/knowledge.rs +++ b/src-tauri/src/knowledge.rs @@ -5,10 +5,10 @@ use mysql_async::{Pool, prelude::*}; use serde::{Deserialize, Serialize}; /// Verbindungskonfiguration -const MYSQL_HOST: &str = "192.168.155.1"; +const MYSQL_HOST: &str = "192.168.155.11"; const MYSQL_PORT: u16 = 3306; const MYSQL_USER: &str = "claude"; -const MYSQL_PASS: &str = "claude"; +const MYSQL_PASS: &str = "8715"; const MYSQL_DB: &str = "claude"; /// Wissenseintrag aus der knowledge-Tabelle diff --git a/src-tauri/src/programs.rs b/src-tauri/src/programs.rs index d710146..51c7c6c 100644 --- a/src-tauri/src/programs.rs +++ b/src-tauri/src/programs.rs @@ -155,27 +155,57 @@ pub async fn xvfb_status() -> Result { #[tauri::command] pub async fn xvfb_screenshot(display_num: Option) -> Result { - // Nimmt Screenshot vom virtuellen Display via scrot oder import + // 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())); - let result = Command::new("scrot") - .env("DISPLAY", format!(":{}", display)) - .arg("-q") - .arg("80") - .arg(&tmp) - .output(); + // Versuchte Kommandos in Reihenfolge + let attempts: Vec<(&str, Vec)> = 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(), + ]), + ]; - match result { - Ok(o) if o.status.success() => { - let bytes = std::fs::read(&tmp).map_err(|e| e.to_string())?; - let _ = std::fs::remove_file(&tmp); - use base64::Engine; - Ok(base64::engine::general_purpose::STANDARD.encode(&bytes)) + 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("scrot fehlgeschlagen. Alternativ: import von ImageMagick installieren.".into()), } + + Err(format!( + "Screenshot fehlgeschlagen. Installiere eines: scrot / imagemagick / ffmpeg.\nLetzter Fehler: {}", + last_err + )) } // ============ Playwright-Infos ============ diff --git a/src/lib/components/AgentView.svelte b/src/lib/components/AgentView.svelte index 27b29ba..d928371 100644 --- a/src/lib/components/AgentView.svelte +++ b/src/lib/components/AgentView.svelte @@ -1,6 +1,22 @@