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>
This commit is contained in:
parent
120715982b
commit
79b8525ede
14 changed files with 502 additions and 148 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -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/
|
||||
|
|
|
|||
75
TEST-ROADMAP.md
Normal file
75
TEST-ROADMAP.md
Normal file
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -155,27 +155,57 @@ pub async fn xvfb_status() -> Result<XvfbStatus, String> {
|
|||
|
||||
#[tauri::command]
|
||||
pub async fn xvfb_screenshot(display_num: Option<u16>) -> Result<String, String> {
|
||||
// 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<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(),
|
||||
]),
|
||||
];
|
||||
|
||||
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 ============
|
||||
|
|
|
|||
|
|
@ -1,6 +1,22 @@
|
|||
<script lang="ts">
|
||||
import { agents, selectedAgentId, agentCount, agentTree, agentMode, type AgentTreeNode } from '$lib/stores/app';
|
||||
import type { Agent } from '$lib/stores/app';
|
||||
import { derived } from 'svelte/store';
|
||||
|
||||
let onlyActive = $state(false);
|
||||
|
||||
// Gefilterter Tree: wenn onlyActive, dann nur Agents mit status==='active'
|
||||
const filteredTree = derived([agentTree], ([$tree]) => {
|
||||
if (!onlyActive) return $tree;
|
||||
function prune(node: AgentTreeNode): AgentTreeNode | null {
|
||||
const kids = node.children.map(prune).filter((n): n is AgentTreeNode => n !== null);
|
||||
if (node.agent.status === 'active' || kids.length > 0) {
|
||||
return { agent: node.agent, children: kids };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return $tree.map(prune).filter((n): n is AgentTreeNode => n !== null);
|
||||
});
|
||||
|
||||
// Delegations-Badge-Text je nach Agent-Modus
|
||||
const delegationBadges: Record<string, { label: string; cssClass: string }> = {
|
||||
|
|
@ -115,7 +131,9 @@
|
|||
</div>
|
||||
<div class="agent-task">{agent.task}</div>
|
||||
<div class="agent-meta">
|
||||
<span class="agent-tools">🔧 {agent.toolCalls.length}</span>
|
||||
{#if agent.toolCalls.length > 0}
|
||||
<span class="agent-tools">🔧 {agent.toolCalls.length}</span>
|
||||
{/if}
|
||||
{#if depth > 0}
|
||||
<span class="agent-depth">Ebene {depth}</span>
|
||||
{#if $agentMode && $agentMode !== 'solo' && delegationBadges[$agentMode]}
|
||||
|
|
@ -149,6 +167,10 @@
|
|||
{$agentCount.subAgents} Sub |
|
||||
{$agentCount.active} aktiv
|
||||
</div>
|
||||
<label class="filter-toggle">
|
||||
<input type="checkbox" bind:checked={onlyActive} />
|
||||
Nur aktive
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{#if $agents.length === 0}
|
||||
|
|
@ -158,7 +180,7 @@
|
|||
</div>
|
||||
{:else}
|
||||
<div class="agent-tree">
|
||||
{#each $agentTree as rootNode}
|
||||
{#each $filteredTree as rootNode}
|
||||
{@render agentNode(rootNode, 0)}
|
||||
{/each}
|
||||
</div>
|
||||
|
|
@ -248,6 +270,20 @@
|
|||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.filter-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.filter-toggle:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
|||
|
|
@ -121,13 +121,17 @@
|
|||
}
|
||||
}
|
||||
|
||||
$: if ($messages.length) scrollToBottom();
|
||||
$effect(() => {
|
||||
if ($messages.length) scrollToBottom();
|
||||
});
|
||||
|
||||
// Bei Session-Wechsel: Compacting-Flag zurücksetzen
|
||||
$: if ($currentSessionId) {
|
||||
compactingWarningShown = false;
|
||||
showCompactingDialog = false;
|
||||
}
|
||||
$effect(() => {
|
||||
if ($currentSessionId) {
|
||||
compactingWarningShown = false;
|
||||
showCompactingDialog = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Token-Schätzung: ~4 Zeichen pro Token
|
||||
function estimateTokensForMessages(msgs: Message[]): number {
|
||||
|
|
@ -551,6 +555,20 @@
|
|||
{ id: 'ids', label: 'IDs/Referenzen' },
|
||||
];
|
||||
|
||||
let copyFeedback = $state<string | null>(null);
|
||||
|
||||
async function copyMessage(message: Message) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(message.content);
|
||||
copyFeedback = message.id;
|
||||
setTimeout(() => {
|
||||
if (copyFeedback === message.id) copyFeedback = null;
|
||||
}, 1500);
|
||||
} catch (err) {
|
||||
console.error('Kopieren fehlgeschlagen:', err);
|
||||
}
|
||||
}
|
||||
|
||||
function openRememberDialog(message: Message) {
|
||||
rememberContent = message.content;
|
||||
rememberEntry = {
|
||||
|
|
@ -638,6 +656,9 @@
|
|||
<button class="action-btn" onclick={() => regenerateResponse(index)} title="Antwort neu generieren">🔄</button>
|
||||
{/if}
|
||||
{#if message.content && message.role !== 'system'}
|
||||
<button class="action-btn" onclick={() => copyMessage(message)} title="Nachricht kopieren">
|
||||
{copyFeedback === message.id ? '✓' : '📋'}
|
||||
</button>
|
||||
<button class="action-btn" onclick={() => openRememberDialog(message)} title="Das merken">💡</button>
|
||||
{/if}
|
||||
<span class="message-time">
|
||||
|
|
@ -660,7 +681,15 @@
|
|||
</button>
|
||||
</div>
|
||||
{:else if message.role === 'assistant'}
|
||||
{@html renderMarkdown(message.content)}
|
||||
{#if message.content}
|
||||
{@html renderMarkdown(message.content)}
|
||||
{:else if $isProcessing}
|
||||
<span class="typing">
|
||||
<span class="dot"></span>
|
||||
<span class="dot"></span>
|
||||
<span class="dot"></span>
|
||||
</span>
|
||||
{/if}
|
||||
{:else}
|
||||
{message.content}
|
||||
{/if}
|
||||
|
|
@ -671,7 +700,8 @@
|
|||
|
||||
{#if $isProcessing}
|
||||
{@const lastMsg = $messages.at(-1)}
|
||||
{#if !lastMsg || lastMsg.role !== 'assistant' || lastMsg.content === ''}
|
||||
{#if !lastMsg || lastMsg.role !== 'assistant'}
|
||||
<!-- Nur zeigen wenn noch gar keine assistant-message da ist -->
|
||||
<div class="message assistant typing-msg">
|
||||
<div class="message-header">
|
||||
<span class="message-role">🤖 Claude</span>
|
||||
|
|
@ -696,7 +726,7 @@
|
|||
<textarea
|
||||
bind:this={inputTextarea}
|
||||
bind:value={$currentInput}
|
||||
on:keydown={handleKeydown}
|
||||
onkeydown={handleKeydown}
|
||||
placeholder="Nachricht eingeben... (Ctrl+K = Focus, Ctrl+Enter = Senden)"
|
||||
disabled={$isProcessing || isRecording}
|
||||
rows="3"
|
||||
|
|
@ -705,7 +735,7 @@
|
|||
<button
|
||||
class="mic-button"
|
||||
class:recording={isRecording}
|
||||
on:click={toggleRecording}
|
||||
onclick={toggleRecording}
|
||||
disabled={$isProcessing}
|
||||
title={isRecording ? 'Aufnahme stoppen' : 'Spracheingabe starten'}
|
||||
>
|
||||
|
|
@ -718,7 +748,7 @@
|
|||
</button>
|
||||
<button
|
||||
class="send-button"
|
||||
on:click={sendMessage}
|
||||
onclick={sendMessage}
|
||||
disabled={!$currentInput.trim() || $isProcessing || isRecording}
|
||||
>
|
||||
{#if $isProcessing}
|
||||
|
|
|
|||
|
|
@ -288,10 +288,22 @@
|
|||
.filter-select {
|
||||
padding: 4px 8px;
|
||||
font-size: 0.75rem;
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--bg-tertiary);
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.filter-select:hover,
|
||||
.filter-select:focus {
|
||||
border-color: var(--accent);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.filter-select option {
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.auto-scroll-toggle {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,17 @@
|
|||
let dbusServices = $state<string[]>([]);
|
||||
let dbusLoading = $state(false);
|
||||
let screenshot = $state<string | null>(null);
|
||||
let errorMsg = $state<string | null>(null);
|
||||
|
||||
async function copyError() {
|
||||
if (errorMsg) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(errorMsg);
|
||||
} catch (e) {
|
||||
console.error('Clipboard:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadXvfb() {
|
||||
try {
|
||||
|
|
@ -38,7 +49,7 @@
|
|||
resolution: xvfb.resolution
|
||||
});
|
||||
} catch (err) {
|
||||
alert(err);
|
||||
errorMsg = String(err);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -47,7 +58,7 @@
|
|||
xvfb = await invoke<XvfbStatus>('xvfb_stop');
|
||||
screenshot = null;
|
||||
} catch (err) {
|
||||
alert(err);
|
||||
errorMsg = String(err);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -56,7 +67,7 @@
|
|||
const b64 = await invoke<string>('xvfb_screenshot', { displayNum: xvfb.display_num });
|
||||
screenshot = `data:image/png;base64,${b64}`;
|
||||
} catch (err) {
|
||||
alert(err);
|
||||
errorMsg = String(err);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -72,7 +83,7 @@
|
|||
dbusServices = await invoke<string[]>('dbus_list_services', { session: true });
|
||||
} catch (err) {
|
||||
dbusServices = [];
|
||||
alert(err);
|
||||
errorMsg = String(err);
|
||||
} finally {
|
||||
dbusLoading = false;
|
||||
}
|
||||
|
|
@ -92,6 +103,19 @@
|
|||
<button class:active={section === 'xvfb'} onclick={() => (section = 'xvfb')}>🖥️ Xvfb</button>
|
||||
</div>
|
||||
|
||||
{#if errorMsg}
|
||||
<div class="error-banner">
|
||||
<div class="error-header">
|
||||
<span>⚠️ Fehler</span>
|
||||
<div class="error-actions">
|
||||
<button onclick={copyError} title="In Zwischenablage kopieren">📋 Kopieren</button>
|
||||
<button onclick={() => (errorMsg = null)} title="Schließen">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
<pre class="error-text">{errorMsg}</pre>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="section-body">
|
||||
{#if section === 'ide'}
|
||||
<IdePanel />
|
||||
|
|
@ -224,15 +248,36 @@
|
|||
}
|
||||
|
||||
.controls button {
|
||||
padding: 4px 10px;
|
||||
padding: 6px 14px;
|
||||
cursor: pointer;
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
transition: background 0.15s, border-color 0.15s;
|
||||
}
|
||||
|
||||
.controls button:hover:not(:disabled) {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.controls button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.controls button.primary {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--accent);
|
||||
}
|
||||
|
||||
.controls button.primary:hover:not(:disabled) {
|
||||
filter: brightness(1.15);
|
||||
}
|
||||
|
||||
.dbus-list {
|
||||
|
|
@ -275,4 +320,55 @@
|
|||
font-size: 0.8rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.error-banner {
|
||||
margin: var(--spacing-sm);
|
||||
background: rgba(248, 113, 113, 0.08);
|
||||
border: 1px solid rgba(248, 113, 113, 0.4);
|
||||
border-radius: var(--radius-sm);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.error-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 4px var(--spacing-sm);
|
||||
background: rgba(248, 113, 113, 0.15);
|
||||
color: #f87171;
|
||||
font-weight: 600;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.error-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.error-actions button {
|
||||
background: transparent;
|
||||
border: 1px solid rgba(248, 113, 113, 0.4);
|
||||
color: #f87171;
|
||||
padding: 2px 8px;
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.error-actions button:hover {
|
||||
background: rgba(248, 113, 113, 0.2);
|
||||
}
|
||||
|
||||
.error-text {
|
||||
margin: 0;
|
||||
padding: var(--spacing-sm);
|
||||
font-family: monospace;
|
||||
font-size: 0.78rem;
|
||||
color: var(--text-primary);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
user-select: text;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -166,7 +166,7 @@
|
|||
<button
|
||||
class="mode-card"
|
||||
class:selected={$agentMode === mode.id}
|
||||
on:click={() => $agentMode = mode.id}
|
||||
on:click={() => changeMode(mode.id)}
|
||||
>
|
||||
<div class="mode-header">
|
||||
<span class="mode-icon">{mode.icon}</span>
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ interface ResultEvent {
|
|||
};
|
||||
session_id?: string;
|
||||
model?: string;
|
||||
text?: string;
|
||||
}
|
||||
|
||||
interface MonitorEventPayload {
|
||||
|
|
@ -120,10 +121,11 @@ export async function initEventListeners(): Promise<void> {
|
|||
// Agent gestartet
|
||||
listeners.push(
|
||||
await listen<AgentEvent>('agent-started', (event) => {
|
||||
const { id, type, task } = event.payload;
|
||||
const { id, type, task, model } = event.payload;
|
||||
console.log('🤖 Agent gestartet:', id, type);
|
||||
|
||||
addAgent(mapAgentType(type || 'main'), task || 'Verarbeite...');
|
||||
// WICHTIG: id mitgeben, sonst stimmt parentAgentId der Sub-Agents nicht!
|
||||
addAgent(mapAgentType(type || 'main'), task || 'Verarbeite...', { id, model });
|
||||
isProcessing.set(true);
|
||||
|
||||
// Leere Streaming-Nachricht anlegen
|
||||
|
|
@ -264,7 +266,7 @@ export async function initEventListeners(): Promise<void> {
|
|||
// Ergebnis (Kosten, Token, Modell)
|
||||
listeners.push(
|
||||
await listen<ResultEvent>('claude-result', async (event) => {
|
||||
const { cost, tokens, session_id, model } = event.payload;
|
||||
const { cost, tokens, session_id, model, text } = event.payload;
|
||||
console.log('📊 Ergebnis:', { cost: cost ? `$${cost.toFixed(4)}` : '-', tokens, model, session_id });
|
||||
|
||||
// Modell an die Streaming-Nachricht anhängen und speichern
|
||||
|
|
@ -274,7 +276,9 @@ export async function initEventListeners(): Promise<void> {
|
|||
messages.update((msgs) => {
|
||||
return msgs.map((m) => {
|
||||
if (m.id === streamingMessageId) {
|
||||
finalMessage = { ...m, model: model || m.model };
|
||||
// Fallback: wenn kein Streaming-Text kam, result.text nutzen
|
||||
const content = m.content && m.content.trim() ? m.content : (text || '');
|
||||
finalMessage = { ...m, content, model: model || m.model };
|
||||
return finalMessage;
|
||||
}
|
||||
return m;
|
||||
|
|
|
|||
|
|
@ -45,6 +45,16 @@
|
|||
console.warn('Modell konnte nicht geladen werden:', err);
|
||||
}
|
||||
|
||||
// Agent-Modus aus Settings laden (sonst Badge nicht sofort sichtbar)
|
||||
try {
|
||||
const mode: string = await invoke('get_agent_mode');
|
||||
if (mode) {
|
||||
$agentMode = mode as AgentMode;
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Agent-Modus konnte nicht geladen werden:', err);
|
||||
}
|
||||
|
||||
// Sticky Context beim Start laden und an Bridge senden
|
||||
try {
|
||||
const ctx: StickyContextResponse = await invoke('init_sticky_context');
|
||||
|
|
|
|||
|
|
@ -7,7 +7,10 @@
|
|||
"rootDir": "src",
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"esModuleInterop": true
|
||||
"esModuleInterop": true,
|
||||
"types": ["node", "vscode", "ws"],
|
||||
"typeRoots": ["./node_modules/@types"]
|
||||
},
|
||||
"exclude": ["node_modules", ".vscode-test"]
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", ".vscode-test", "../node_modules"]
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue