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:
Eddy 2026-04-14 21:24:51 +02:00
parent 120715982b
commit 79b8525ede
14 changed files with 502 additions and 148 deletions

3
.gitignore vendored
View file

@ -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
View 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

View file

@ -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;
}

View file

@ -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;

View file

@ -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

View file

@ -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 ============

View file

@ -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;

View file

@ -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}

View file

@ -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 {

View file

@ -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>

View file

@ -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>

View file

@ -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;

View file

@ -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');

View file

@ -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"]
}