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 (use npm ci)
|
||||||
package-lock.json
|
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 = {
|
const ORCHESTRATOR_PROMPTS = {
|
||||||
handlanger: `
|
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".
|
Task-Tool mit den RICHTIGEN Sub-Agent-Typen:
|
||||||
Der Worker läuft auf Haiku (günstig) und führt genau aus was du sagst.
|
- "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:
|
Arbeitsweise (verbindlich):
|
||||||
1. ANALYSIERE die Aufgabe
|
1. Wähle den RICHTIGEN subagent_type basierend auf der Aufgabe.
|
||||||
2. Zerlege in EXAKTE Ausführungsschritte
|
2. Rufe das Task-Tool auf mit EXAKTER Anweisung.
|
||||||
3. Delegiere jeden Schritt per Task(subagent_type: "worker", prompt: "...")
|
3. Prüfe im Ergebnis das "tool_uses"-Feld. Wenn tool_uses:0 → Sub-Agent hat halluziniert!
|
||||||
4. Formuliere den Task-Prompt als exakte Anweisung, nicht als Frage
|
In dem Fall: Neuer Task-Call mit "general-purpose" statt "Explore".
|
||||||
5. Sammle die Zusammenfassungen, entscheide den nächsten Schritt
|
4. Verarbeite das Ergebnis und gib dem User die Zusammenfassung.
|
||||||
|
|
||||||
Beispiel-Delegationen:
|
Halluziniere NIEMALS Dateilisten aus dem Gedächtnis — delegiere immer real.
|
||||||
- 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")
|
|
||||||
|
|
||||||
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: `
|
experten: `
|
||||||
|
|
@ -373,71 +377,58 @@ async function sendMessage(message, requestId, model = null, contextOverride = n
|
||||||
abortController: activeAbort,
|
abortController: activeAbort,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Session-ID für Fortsetzung hinzufügen wenn vorhanden
|
// Session-ID für Fortsetzung — SDK erwartet `resume`, nicht `sessionId`
|
||||||
if (resumeSessionId) {
|
if (resumeSessionId) {
|
||||||
queryOptions.sessionId = resumeSessionId;
|
queryOptions.resume = resumeSessionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tool-Filterung + Custom Sub-Agents je nach effektivem Modus
|
// Tool-Konfig je nach Modus.
|
||||||
// Handlanger: Main darf NUR delegieren (Task) und planen (TodoWrite)
|
// WICHTIG: disallowedTools vererbt sich auf Sub-Agents!
|
||||||
// Sub-Agents laufen auf Haiku (siehe HANDLANGER_AGENTS)
|
// Deshalb Whitelist via `tools` nutzen — die gilt nur fuer Main,
|
||||||
// Experten: Main darf zusätzlich lesen/suchen, aber nicht schreiben
|
// Sub-Agents bekommen das volle Standard-Tool-Set.
|
||||||
// Sub-Agents sind autonome Research/Implement/Test/Review
|
queryOptions.allowedTools = ['Task', 'TodoWrite', 'Read', 'Grep', 'Glob', 'Write', 'Edit', 'Bash'];
|
||||||
|
|
||||||
if (effectiveMode === 'handlanger') {
|
if (effectiveMode === 'handlanger') {
|
||||||
queryOptions.allowedTools = ['Task', 'TodoWrite'];
|
queryOptions.tools = ['Task', 'TodoWrite'];
|
||||||
queryOptions.agents = HANDLANGER_AGENTS;
|
sendMonitorEvent('agent', 'Handlanger: Main nur Task+TodoWrite, Sub-Agents mit vollem Tool-Set', {
|
||||||
sendMonitorEvent('agent', 'Handlanger-Modus: Main → Task+TodoWrite, Worker auf Haiku', {
|
|
||||||
mode: effectiveMode,
|
mode: effectiveMode,
|
||||||
allowedTools: queryOptions.allowedTools,
|
mainTools: queryOptions.tools,
|
||||||
agents: Object.keys(HANDLANGER_AGENTS),
|
|
||||||
});
|
});
|
||||||
} else if (effectiveMode === 'experten') {
|
} else if (effectiveMode === 'experten') {
|
||||||
queryOptions.allowedTools = ['Task', 'TodoWrite', 'Read', 'Grep', 'Glob'];
|
queryOptions.tools = ['Task', 'TodoWrite', 'Read', 'Grep', 'Glob'];
|
||||||
queryOptions.agents = EXPERTEN_AGENTS;
|
sendMonitorEvent('agent', 'Experten: Main lesen+delegieren, Sub-Agents voll', {
|
||||||
sendMonitorEvent('agent', 'Experten-Modus: 4 autonome Experten verfügbar', {
|
|
||||||
mode: effectiveMode,
|
mode: effectiveMode,
|
||||||
allowedTools: queryOptions.allowedTools,
|
mainTools: queryOptions.tools,
|
||||||
agents: Object.keys(EXPERTEN_AGENTS),
|
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
// solo: volles Preset
|
||||||
|
queryOptions.tools = { type: 'preset', preset: 'claude_code' };
|
||||||
}
|
}
|
||||||
// solo: keine Einschränkung
|
|
||||||
|
|
||||||
const conversation = query({
|
let conversation = query({
|
||||||
prompt: fullPrompt,
|
prompt: fullPrompt,
|
||||||
options: queryOptions,
|
options: queryOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
for await (const event of conversation) {
|
// Dedupe: Manche Tool-Events kommen sowohl in assistant-Blocks
|
||||||
switch (event.type) {
|
// als auch als standalone tool_use Event. Via toolUseId deduplizieren.
|
||||||
case 'assistant':
|
const handledTools = new Set();
|
||||||
// Text aus der Nachricht extrahieren
|
|
||||||
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 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (event.message?.model) {
|
|
||||||
usedModel = event.message.model;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'tool_use': {
|
// Tool-Use handhaben — funktioniert sowohl fuer standalone tool_use Events
|
||||||
const toolId = event.tool_use_id || randomUUID();
|
// als auch fuer tool_use Bloecke innerhalb einer assistant-Nachricht
|
||||||
const toolName = event.name || 'unknown';
|
function handleToolUse(ev) {
|
||||||
const toolInput = event.input || {};
|
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 || {};
|
||||||
|
|
||||||
// Prüfen ob dieses Tool einen Subagent startet
|
|
||||||
if (SUBAGENT_TOOLS.includes(toolName)) {
|
if (SUBAGENT_TOOLS.includes(toolName)) {
|
||||||
const subagentId = randomUUID();
|
const subagentId = randomUUID();
|
||||||
const subagentType = getSubagentType(toolName, toolInput);
|
const subagentType = getSubagentType(toolName, toolInput);
|
||||||
const subagentTask = toolInput.description || toolInput.prompt || toolInput.task || 'Subagent-Aufgabe';
|
const subagentTask = toolInput.description || toolInput.prompt || toolInput.task || 'Subagent-Aufgabe';
|
||||||
const subagentModel = toolInput.model || useModel;
|
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;
|
const depth = 1;
|
||||||
|
|
||||||
activeSubagents.set(toolId, {
|
activeSubagents.set(toolId, {
|
||||||
|
|
@ -467,26 +458,24 @@ async function sendMessage(message, requestId, model = null, contextOverride = n
|
||||||
agentId: currentAgentId,
|
agentId: currentAgentId,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Monitor: Tool gestartet
|
|
||||||
const toolSummary = summarizeToolInput(toolName, toolInput);
|
const toolSummary = summarizeToolInput(toolName, toolInput);
|
||||||
sendMonitorEvent('tool', `${toolName} ${toolSummary}`, {
|
sendMonitorEvent('tool', `${toolName} ${toolSummary}`, {
|
||||||
toolId,
|
toolId,
|
||||||
tool: toolName,
|
tool: toolName,
|
||||||
input: toolInput,
|
input: toolInput,
|
||||||
});
|
});
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'tool_result': {
|
// Tool-Result handhaben
|
||||||
const toolId = event.tool_use_id || '';
|
function handleToolResult(ev) {
|
||||||
|
const toolId = ev.tool_use_id || '';
|
||||||
|
|
||||||
// Prüfen ob dieser Tool-Call ein Subagent war
|
|
||||||
if (activeSubagents.has(toolId)) {
|
if (activeSubagents.has(toolId)) {
|
||||||
const subagent = activeSubagents.get(toolId);
|
const subagent = activeSubagents.get(toolId);
|
||||||
sendEvent('subagent-stopped', {
|
sendEvent('subagent-stopped', {
|
||||||
id: subagent.agentId,
|
id: subagent.agentId,
|
||||||
parentAgentId: subagent.parentId,
|
parentAgentId: subagent.parentId,
|
||||||
success: !event.is_error,
|
success: !ev.is_error,
|
||||||
toolUseId: toolId,
|
toolUseId: toolId,
|
||||||
});
|
});
|
||||||
activeSubagents.delete(toolId);
|
activeSubagents.delete(toolId);
|
||||||
|
|
@ -494,9 +483,71 @@ async function sendMessage(message, requestId, model = null, contextOverride = n
|
||||||
|
|
||||||
sendEvent('tool-end', {
|
sendEvent('tool-end', {
|
||||||
id: toolId,
|
id: toolId,
|
||||||
success: !event.is_error,
|
success: !ev.is_error,
|
||||||
agentId: currentAgentId,
|
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':
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (event.message?.model) {
|
||||||
|
usedModel = event.message.model;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'tool_use': {
|
||||||
|
handleToolUse(event);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'tool_result': {
|
||||||
|
handleToolResult(event);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
break;
|
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 let Ok(Some(active_id)) = db_lock.get_setting("active_session_id") {
|
||||||
if !active_id.is_empty() {
|
if !active_id.is_empty() {
|
||||||
if let Ok(Some(mut session)) = db_lock.get_session(&active_id) {
|
if let Ok(Some(mut session)) = db_lock.get_session(&active_id) {
|
||||||
|
// 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.claude_session_id = Some(sid.to_string());
|
||||||
|
}
|
||||||
session.message_count += 1;
|
session.message_count += 1;
|
||||||
if let Some(cost) = payload.get("cost").and_then(|v| v.as_f64()) {
|
if let Some(cost) = payload.get("cost").and_then(|v| v.as_f64()) {
|
||||||
session.cost_usd += cost;
|
session.cost_usd += cost;
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,10 @@ use mysql_async::{Pool, prelude::*};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
/// Verbindungskonfiguration
|
/// Verbindungskonfiguration
|
||||||
const MYSQL_HOST: &str = "192.168.155.1";
|
const MYSQL_HOST: &str = "192.168.155.11";
|
||||||
const MYSQL_PORT: u16 = 3306;
|
const MYSQL_PORT: u16 = 3306;
|
||||||
const MYSQL_USER: &str = "claude";
|
const MYSQL_USER: &str = "claude";
|
||||||
const MYSQL_PASS: &str = "claude";
|
const MYSQL_PASS: &str = "8715";
|
||||||
const MYSQL_DB: &str = "claude";
|
const MYSQL_DB: &str = "claude";
|
||||||
|
|
||||||
/// Wissenseintrag aus der knowledge-Tabelle
|
/// Wissenseintrag aus der knowledge-Tabelle
|
||||||
|
|
|
||||||
|
|
@ -155,27 +155,57 @@ pub async fn xvfb_status() -> Result<XvfbStatus, String> {
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn xvfb_screenshot(display_num: Option<u16>) -> Result<String, String> {
|
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 = 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 tmp = std::env::temp_dir().join(format!("claude-xvfb-{}.png", uuid::Uuid::new_v4()));
|
||||||
|
|
||||||
let result = Command::new("scrot")
|
// Versuchte Kommandos in Reihenfolge
|
||||||
.env("DISPLAY", format!(":{}", display))
|
let attempts: Vec<(&str, Vec<String>)> = vec![
|
||||||
.arg("-q")
|
("scrot", vec!["-q".into(), "80".into(), tmp.to_string_lossy().into_owned()]),
|
||||||
.arg("80")
|
("import", vec!["-window".into(), "root".into(), tmp.to_string_lossy().into_owned()]),
|
||||||
.arg(&tmp)
|
("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();
|
.output();
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(o) if o.status.success() => {
|
Ok(o) if o.status.success() && tmp.exists() => {
|
||||||
let bytes = std::fs::read(&tmp).map_err(|e| e.to_string())?;
|
let bytes = std::fs::read(&tmp).map_err(|e| e.to_string())?;
|
||||||
let _ = std::fs::remove_file(&tmp);
|
let _ = std::fs::remove_file(&tmp);
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
Ok(base64::engine::general_purpose::STANDARD.encode(&bytes))
|
return Ok(base64::engine::general_purpose::STANDARD.encode(&bytes));
|
||||||
}
|
}
|
||||||
_ => Err("scrot fehlgeschlagen. Alternativ: import von ImageMagick installieren.".into()),
|
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 ============
|
// ============ Playwright-Infos ============
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,22 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { agents, selectedAgentId, agentCount, agentTree, agentMode, type AgentTreeNode } from '$lib/stores/app';
|
import { agents, selectedAgentId, agentCount, agentTree, agentMode, type AgentTreeNode } from '$lib/stores/app';
|
||||||
import type { Agent } 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
|
// Delegations-Badge-Text je nach Agent-Modus
|
||||||
const delegationBadges: Record<string, { label: string; cssClass: string }> = {
|
const delegationBadges: Record<string, { label: string; cssClass: string }> = {
|
||||||
|
|
@ -115,7 +131,9 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="agent-task">{agent.task}</div>
|
<div class="agent-task">{agent.task}</div>
|
||||||
<div class="agent-meta">
|
<div class="agent-meta">
|
||||||
|
{#if agent.toolCalls.length > 0}
|
||||||
<span class="agent-tools">🔧 {agent.toolCalls.length}</span>
|
<span class="agent-tools">🔧 {agent.toolCalls.length}</span>
|
||||||
|
{/if}
|
||||||
{#if depth > 0}
|
{#if depth > 0}
|
||||||
<span class="agent-depth">Ebene {depth}</span>
|
<span class="agent-depth">Ebene {depth}</span>
|
||||||
{#if $agentMode && $agentMode !== 'solo' && delegationBadges[$agentMode]}
|
{#if $agentMode && $agentMode !== 'solo' && delegationBadges[$agentMode]}
|
||||||
|
|
@ -149,6 +167,10 @@
|
||||||
{$agentCount.subAgents} Sub |
|
{$agentCount.subAgents} Sub |
|
||||||
{$agentCount.active} aktiv
|
{$agentCount.active} aktiv
|
||||||
</div>
|
</div>
|
||||||
|
<label class="filter-toggle">
|
||||||
|
<input type="checkbox" bind:checked={onlyActive} />
|
||||||
|
Nur aktive
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if $agents.length === 0}
|
{#if $agents.length === 0}
|
||||||
|
|
@ -158,7 +180,7 @@
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="agent-tree">
|
<div class="agent-tree">
|
||||||
{#each $agentTree as rootNode}
|
{#each $filteredTree as rootNode}
|
||||||
{@render agentNode(rootNode, 0)}
|
{@render agentNode(rootNode, 0)}
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -248,6 +270,20 @@
|
||||||
color: var(--text-secondary);
|
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 {
|
.empty-state {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
||||||
|
|
@ -121,13 +121,17 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if ($messages.length) scrollToBottom();
|
$effect(() => {
|
||||||
|
if ($messages.length) scrollToBottom();
|
||||||
|
});
|
||||||
|
|
||||||
// Bei Session-Wechsel: Compacting-Flag zurücksetzen
|
// Bei Session-Wechsel: Compacting-Flag zurücksetzen
|
||||||
$: if ($currentSessionId) {
|
$effect(() => {
|
||||||
|
if ($currentSessionId) {
|
||||||
compactingWarningShown = false;
|
compactingWarningShown = false;
|
||||||
showCompactingDialog = false;
|
showCompactingDialog = false;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Token-Schätzung: ~4 Zeichen pro Token
|
// Token-Schätzung: ~4 Zeichen pro Token
|
||||||
function estimateTokensForMessages(msgs: Message[]): number {
|
function estimateTokensForMessages(msgs: Message[]): number {
|
||||||
|
|
@ -551,6 +555,20 @@
|
||||||
{ id: 'ids', label: 'IDs/Referenzen' },
|
{ 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) {
|
function openRememberDialog(message: Message) {
|
||||||
rememberContent = message.content;
|
rememberContent = message.content;
|
||||||
rememberEntry = {
|
rememberEntry = {
|
||||||
|
|
@ -638,6 +656,9 @@
|
||||||
<button class="action-btn" onclick={() => regenerateResponse(index)} title="Antwort neu generieren">🔄</button>
|
<button class="action-btn" onclick={() => regenerateResponse(index)} title="Antwort neu generieren">🔄</button>
|
||||||
{/if}
|
{/if}
|
||||||
{#if message.content && message.role !== 'system'}
|
{#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>
|
<button class="action-btn" onclick={() => openRememberDialog(message)} title="Das merken">💡</button>
|
||||||
{/if}
|
{/if}
|
||||||
<span class="message-time">
|
<span class="message-time">
|
||||||
|
|
@ -660,7 +681,15 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{:else if message.role === 'assistant'}
|
{:else if message.role === 'assistant'}
|
||||||
|
{#if message.content}
|
||||||
{@html renderMarkdown(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}
|
{:else}
|
||||||
{message.content}
|
{message.content}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -671,7 +700,8 @@
|
||||||
|
|
||||||
{#if $isProcessing}
|
{#if $isProcessing}
|
||||||
{@const lastMsg = $messages.at(-1)}
|
{@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 assistant typing-msg">
|
||||||
<div class="message-header">
|
<div class="message-header">
|
||||||
<span class="message-role">🤖 Claude</span>
|
<span class="message-role">🤖 Claude</span>
|
||||||
|
|
@ -696,7 +726,7 @@
|
||||||
<textarea
|
<textarea
|
||||||
bind:this={inputTextarea}
|
bind:this={inputTextarea}
|
||||||
bind:value={$currentInput}
|
bind:value={$currentInput}
|
||||||
on:keydown={handleKeydown}
|
onkeydown={handleKeydown}
|
||||||
placeholder="Nachricht eingeben... (Ctrl+K = Focus, Ctrl+Enter = Senden)"
|
placeholder="Nachricht eingeben... (Ctrl+K = Focus, Ctrl+Enter = Senden)"
|
||||||
disabled={$isProcessing || isRecording}
|
disabled={$isProcessing || isRecording}
|
||||||
rows="3"
|
rows="3"
|
||||||
|
|
@ -705,7 +735,7 @@
|
||||||
<button
|
<button
|
||||||
class="mic-button"
|
class="mic-button"
|
||||||
class:recording={isRecording}
|
class:recording={isRecording}
|
||||||
on:click={toggleRecording}
|
onclick={toggleRecording}
|
||||||
disabled={$isProcessing}
|
disabled={$isProcessing}
|
||||||
title={isRecording ? 'Aufnahme stoppen' : 'Spracheingabe starten'}
|
title={isRecording ? 'Aufnahme stoppen' : 'Spracheingabe starten'}
|
||||||
>
|
>
|
||||||
|
|
@ -718,7 +748,7 @@
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="send-button"
|
class="send-button"
|
||||||
on:click={sendMessage}
|
onclick={sendMessage}
|
||||||
disabled={!$currentInput.trim() || $isProcessing || isRecording}
|
disabled={!$currentInput.trim() || $isProcessing || isRecording}
|
||||||
>
|
>
|
||||||
{#if $isProcessing}
|
{#if $isProcessing}
|
||||||
|
|
|
||||||
|
|
@ -288,10 +288,22 @@
|
||||||
.filter-select {
|
.filter-select {
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
background: var(--bg-tertiary);
|
background: var(--bg-secondary);
|
||||||
border: 1px solid var(--bg-tertiary);
|
border: 1px solid var(--border);
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
color: var(--text-primary);
|
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 {
|
.auto-scroll-toggle {
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,17 @@
|
||||||
let dbusServices = $state<string[]>([]);
|
let dbusServices = $state<string[]>([]);
|
||||||
let dbusLoading = $state(false);
|
let dbusLoading = $state(false);
|
||||||
let screenshot = $state<string | null>(null);
|
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() {
|
async function loadXvfb() {
|
||||||
try {
|
try {
|
||||||
|
|
@ -38,7 +49,7 @@
|
||||||
resolution: xvfb.resolution
|
resolution: xvfb.resolution
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(err);
|
errorMsg = String(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -47,7 +58,7 @@
|
||||||
xvfb = await invoke<XvfbStatus>('xvfb_stop');
|
xvfb = await invoke<XvfbStatus>('xvfb_stop');
|
||||||
screenshot = null;
|
screenshot = null;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(err);
|
errorMsg = String(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -56,7 +67,7 @@
|
||||||
const b64 = await invoke<string>('xvfb_screenshot', { displayNum: xvfb.display_num });
|
const b64 = await invoke<string>('xvfb_screenshot', { displayNum: xvfb.display_num });
|
||||||
screenshot = `data:image/png;base64,${b64}`;
|
screenshot = `data:image/png;base64,${b64}`;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(err);
|
errorMsg = String(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -72,7 +83,7 @@
|
||||||
dbusServices = await invoke<string[]>('dbus_list_services', { session: true });
|
dbusServices = await invoke<string[]>('dbus_list_services', { session: true });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
dbusServices = [];
|
dbusServices = [];
|
||||||
alert(err);
|
errorMsg = String(err);
|
||||||
} finally {
|
} finally {
|
||||||
dbusLoading = false;
|
dbusLoading = false;
|
||||||
}
|
}
|
||||||
|
|
@ -92,6 +103,19 @@
|
||||||
<button class:active={section === 'xvfb'} onclick={() => (section = 'xvfb')}>🖥️ Xvfb</button>
|
<button class:active={section === 'xvfb'} onclick={() => (section = 'xvfb')}>🖥️ Xvfb</button>
|
||||||
</div>
|
</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">
|
<div class="section-body">
|
||||||
{#if section === 'ide'}
|
{#if section === 'ide'}
|
||||||
<IdePanel />
|
<IdePanel />
|
||||||
|
|
@ -224,15 +248,36 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.controls button {
|
.controls button {
|
||||||
padding: 4px 10px;
|
padding: 6px 14px;
|
||||||
cursor: pointer;
|
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 {
|
.controls button.primary {
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: 1px solid var(--accent);
|
||||||
border-radius: var(--radius-sm);
|
}
|
||||||
|
|
||||||
|
.controls button.primary:hover:not(:disabled) {
|
||||||
|
filter: brightness(1.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dbus-list {
|
.dbus-list {
|
||||||
|
|
@ -275,4 +320,55 @@
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
font-style: italic;
|
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>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -166,7 +166,7 @@
|
||||||
<button
|
<button
|
||||||
class="mode-card"
|
class="mode-card"
|
||||||
class:selected={$agentMode === mode.id}
|
class:selected={$agentMode === mode.id}
|
||||||
on:click={() => $agentMode = mode.id}
|
on:click={() => changeMode(mode.id)}
|
||||||
>
|
>
|
||||||
<div class="mode-header">
|
<div class="mode-header">
|
||||||
<span class="mode-icon">{mode.icon}</span>
|
<span class="mode-icon">{mode.icon}</span>
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,7 @@ interface ResultEvent {
|
||||||
};
|
};
|
||||||
session_id?: string;
|
session_id?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
|
text?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MonitorEventPayload {
|
interface MonitorEventPayload {
|
||||||
|
|
@ -120,10 +121,11 @@ export async function initEventListeners(): Promise<void> {
|
||||||
// Agent gestartet
|
// Agent gestartet
|
||||||
listeners.push(
|
listeners.push(
|
||||||
await listen<AgentEvent>('agent-started', (event) => {
|
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);
|
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);
|
isProcessing.set(true);
|
||||||
|
|
||||||
// Leere Streaming-Nachricht anlegen
|
// Leere Streaming-Nachricht anlegen
|
||||||
|
|
@ -264,7 +266,7 @@ export async function initEventListeners(): Promise<void> {
|
||||||
// Ergebnis (Kosten, Token, Modell)
|
// Ergebnis (Kosten, Token, Modell)
|
||||||
listeners.push(
|
listeners.push(
|
||||||
await listen<ResultEvent>('claude-result', async (event) => {
|
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 });
|
console.log('📊 Ergebnis:', { cost: cost ? `$${cost.toFixed(4)}` : '-', tokens, model, session_id });
|
||||||
|
|
||||||
// Modell an die Streaming-Nachricht anhängen und speichern
|
// Modell an die Streaming-Nachricht anhängen und speichern
|
||||||
|
|
@ -274,7 +276,9 @@ export async function initEventListeners(): Promise<void> {
|
||||||
messages.update((msgs) => {
|
messages.update((msgs) => {
|
||||||
return msgs.map((m) => {
|
return msgs.map((m) => {
|
||||||
if (m.id === streamingMessageId) {
|
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 finalMessage;
|
||||||
}
|
}
|
||||||
return m;
|
return m;
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,16 @@
|
||||||
console.warn('Modell konnte nicht geladen werden:', err);
|
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
|
// Sticky Context beim Start laden und an Bridge senden
|
||||||
try {
|
try {
|
||||||
const ctx: StickyContextResponse = await invoke('init_sticky_context');
|
const ctx: StickyContextResponse = await invoke('init_sticky_context');
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,10 @@
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"strict": 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