All checks were successful
Build AppImage / build (push) Successful in 8m8s
- Block A: KB-Hint-Pillen im Chat (💡) über Tool-Cards, Klick öffnet KB-Browser - Block B: KB-Usage-Tracking (usage_count/last_used), Sortier-Boost für bewährte Einträge - Block C: Cross-Session-Recall per SQLite-FTS5 (🕒 Pille "Schon mal beantwortet") - Block D: Voice-Konversationsmodus (Langes Halten = Loop mit Barge-In-Unterbrechung) - Block F: Select-Button im Audit-Log (appearance:none + SVG-Chevron, WebKitGTK-Fix) - Block G: Chat-Darstellungseinstellungen (Schriftart, -größe, Zeilenhöhe, Code-Größe) - WorkingIndicator: Deutsche Animationstexte beim Verarbeiten Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
541 lines
15 KiB
TypeScript
541 lines
15 KiB
TypeScript
// Claude Desktop — App-State
|
|
|
|
import { writable, derived } from 'svelte/store';
|
|
import { invoke } from '@tauri-apps/api/core';
|
|
|
|
// Typen
|
|
export interface Agent {
|
|
id: string;
|
|
type: 'main' | 'explore' | 'plan' | 'bash' | 'code' | 'test' | 'review';
|
|
status: 'active' | 'waiting' | 'idle' | 'stopped';
|
|
task: string;
|
|
startedAt: Date;
|
|
toolCalls: ToolCall[];
|
|
// Subagent-Hierarchie
|
|
parentAgentId?: string; // undefined = Main Agent
|
|
depth: number; // 0 = Main, 1 = direkter Subagent, etc.
|
|
model?: string; // Welches Modell nutzt dieser Agent
|
|
}
|
|
|
|
export interface ToolCall {
|
|
id: string;
|
|
agentId: string;
|
|
tool: string;
|
|
args: Record<string, unknown>;
|
|
status: 'running' | 'completed' | 'failed';
|
|
startedAt: Date;
|
|
completedAt?: Date;
|
|
result?: unknown;
|
|
}
|
|
|
|
// Inline Tool-Call der einer Message angehaengt ist (fuer Inline-Karten im Chat)
|
|
export interface InlineToolCall {
|
|
id: string; // toolId aus dem Backend
|
|
tool: string; // Read, Edit, Write, Bash, Grep, Glob, WebFetch, Task, MCP, ...
|
|
input: Record<string, unknown>;
|
|
status: 'running' | 'done' | 'error';
|
|
result?: string; // Stringifizierte Tool-Ausgabe (optional, wird beim tool-end gesetzt)
|
|
startedAt: Date;
|
|
completedAt?: Date;
|
|
}
|
|
|
|
export interface Message {
|
|
id: string;
|
|
role: 'user' | 'assistant' | 'system';
|
|
content: string;
|
|
timestamp: Date;
|
|
agentId?: string;
|
|
model?: string;
|
|
queued?: boolean; // Nachricht wartet in der Queue auf Dispatch
|
|
toolCalls?: InlineToolCall[]; // Inline gerenderte Tool-Karten (Phase 8)
|
|
knowledgeHints?: KnowledgeHint[]; // KB-Treffer die fuer diese Antwort herangezogen wurden
|
|
}
|
|
|
|
export interface Permission {
|
|
id: string;
|
|
pattern: string;
|
|
type: 'session' | 'permanent';
|
|
action: 'allow' | 'deny';
|
|
createdAt: Date;
|
|
}
|
|
|
|
// Quick-Actions Typ (fuer QuickActions.svelte + ChatPanel.svelte)
|
|
export interface QuickAction {
|
|
id: string;
|
|
label: string;
|
|
description: string;
|
|
icon: string;
|
|
category: 'build' | 'git' | 'session' | 'navigation' | 'voice' | 'tools';
|
|
shortcut?: string;
|
|
command?: string; // Wird als Message an Claude gesendet
|
|
invoke?: string; // Tauri-Command direkt aufrufen
|
|
invokeArgs?: Record<string, unknown>;
|
|
}
|
|
|
|
// Pending File-Changes (Accept/Reject DiffView)
|
|
export interface FileChange {
|
|
toolId: string;
|
|
tool: string;
|
|
filePath: string;
|
|
contentBefore: string;
|
|
contentAfter: string;
|
|
timestamp: Date;
|
|
}
|
|
|
|
export const pendingChanges = writable<FileChange[]>([]);
|
|
|
|
// Stores
|
|
export const agents = writable<Agent[]>([]);
|
|
export const toolCalls = writable<ToolCall[]>([]);
|
|
export const messages = writable<Message[]>([]);
|
|
export const permissions = writable<Permission[]>([]);
|
|
|
|
// UI-State
|
|
export const isProcessing = writable(false);
|
|
export const chatDetached = writable(false);
|
|
export const currentInput = writable('');
|
|
export const selectedAgentId = writable<string | null>(null);
|
|
export const currentModel = writable('');
|
|
export const currentSessionId = writable<string | null>(null);
|
|
|
|
// Message-Queue: Nachrichten die waehrend der Verarbeitung eingehen, werden hier
|
|
// gesammelt und nach Ende der aktuellen Antwort FIFO abgearbeitet.
|
|
// Jede Nachricht erscheint sofort im Chat als User-Message.
|
|
export const messageQueue = writable<string[]>([]);
|
|
|
|
// Abwaertskompatibel: queuedMessage zeigt die naechste wartende Nachricht
|
|
// (Legacy — wird in ChatPanel noch referenziert)
|
|
export const queuedMessage = writable<string | null>(null);
|
|
|
|
// Agent-Modus für Multi-Agent-Architektur
|
|
export type AgentMode = 'solo' | 'handlanger' | 'experten' | 'auto';
|
|
export const agentMode = writable<AgentMode>('solo');
|
|
|
|
// Session-Statistiken (kumuliert)
|
|
export const sessionStats = writable({
|
|
totalTokensIn: 0,
|
|
totalTokensOut: 0,
|
|
totalCost: 0,
|
|
messageCount: 0,
|
|
});
|
|
|
|
// Kontext-Auslastung (aktueller API-Call)
|
|
// inputTokens = was Claude bei diesem Request "gelesen" hat (System + Konversation)
|
|
export const contextUsage = writable({
|
|
inputTokens: 0, // Aktuelle Kontext-Tokens
|
|
outputTokens: 0, // Tokens der letzten Antwort
|
|
contextLimit: 200000, // Claude 3.5/Opus Context Window
|
|
});
|
|
|
|
// Abgeleitet: Prozent der Kontext-Auslastung
|
|
export const contextPercent = derived(contextUsage, ($ctx) =>
|
|
Math.round(($ctx.inputTokens / $ctx.contextLimit) * 100)
|
|
);
|
|
|
|
// Sticky Context Status (beim App-Start geladen)
|
|
export interface StickyContextInfo {
|
|
loaded: boolean;
|
|
entries: number;
|
|
estimatedTokens: number;
|
|
hasUserInfo: boolean;
|
|
hasProject: boolean;
|
|
credentialsCount: number;
|
|
rulesCount: number;
|
|
}
|
|
|
|
export const stickyContextInfo = writable<StickyContextInfo | null>(null);
|
|
|
|
// Wissens-Hints (aus claude-db)
|
|
export interface KnowledgeHint {
|
|
id: number;
|
|
category: string;
|
|
title: string;
|
|
content: string;
|
|
tags?: string;
|
|
priority: number;
|
|
}
|
|
|
|
export const activeKnowledgeHints = writable<KnowledgeHint[]>([]);
|
|
|
|
// Abgeleitete Stores
|
|
export const activeAgents = derived(agents, ($agents) =>
|
|
$agents.filter((a) => a.status === 'active')
|
|
);
|
|
|
|
export const recentToolCalls = derived(toolCalls, ($toolCalls) =>
|
|
$toolCalls.slice(-50).reverse()
|
|
);
|
|
|
|
export const agentCount = derived(agents, ($agents) => ({
|
|
total: $agents.length,
|
|
active: $agents.filter((a) => a.status === 'active').length,
|
|
waiting: $agents.filter((a) => a.status === 'waiting').length,
|
|
idle: $agents.filter((a) => a.status === 'idle').length,
|
|
mainAgents: $agents.filter((a) => !a.parentAgentId).length,
|
|
subAgents: $agents.filter((a) => a.parentAgentId).length,
|
|
}));
|
|
|
|
// Agent-Baum Typen und Builder (muss vor agentTree Store sein)
|
|
export interface AgentTreeNode {
|
|
agent: Agent;
|
|
children: AgentTreeNode[];
|
|
}
|
|
|
|
export function buildAgentTree(agentsList: Agent[]): AgentTreeNode[] {
|
|
// Nur Root-Agents (ohne Parent)
|
|
const roots = agentsList.filter((a) => !a.parentAgentId);
|
|
|
|
function buildNode(agent: Agent): AgentTreeNode {
|
|
const children = agentsList
|
|
.filter((a) => a.parentAgentId === agent.id)
|
|
.map(buildNode);
|
|
return { agent, children };
|
|
}
|
|
|
|
return roots.map(buildNode);
|
|
}
|
|
|
|
// Agent-Baum als reaktiver Store
|
|
export const agentTree = derived(agents, ($agents) => buildAgentTree($agents));
|
|
|
|
// Aktionen
|
|
export function addMessage(role: Message['role'], content: string, agentId?: string) {
|
|
messages.update((msgs) => [
|
|
...msgs,
|
|
{
|
|
id: crypto.randomUUID(),
|
|
role,
|
|
content,
|
|
timestamp: new Date(),
|
|
agentId
|
|
}
|
|
]);
|
|
}
|
|
|
|
export interface AddAgentOptions {
|
|
id?: string;
|
|
parentAgentId?: string;
|
|
model?: string;
|
|
}
|
|
|
|
export function addAgent(type: Agent['type'], task: string, options?: AddAgentOptions): string {
|
|
const id = options?.id || crypto.randomUUID();
|
|
const parentAgentId = options?.parentAgentId;
|
|
|
|
// Tiefe berechnen: Parent-Tiefe + 1 (oder 0 wenn kein Parent)
|
|
let depth = 0;
|
|
if (parentAgentId) {
|
|
agents.subscribe((ags) => {
|
|
const parent = ags.find((a) => a.id === parentAgentId);
|
|
if (parent) {
|
|
depth = parent.depth + 1;
|
|
}
|
|
})();
|
|
}
|
|
|
|
agents.update((ags) => [
|
|
...ags,
|
|
{
|
|
id,
|
|
type,
|
|
status: 'active',
|
|
task,
|
|
startedAt: new Date(),
|
|
toolCalls: [],
|
|
parentAgentId,
|
|
depth,
|
|
model: options?.model,
|
|
}
|
|
]);
|
|
return id;
|
|
}
|
|
|
|
// Subagent hinzufügen (Kurzform)
|
|
export function addSubAgent(
|
|
parentId: string,
|
|
type: Agent['type'],
|
|
task: string,
|
|
options?: Omit<AddAgentOptions, 'parentAgentId'>
|
|
): string {
|
|
return addAgent(type, task, { ...options, parentAgentId: parentId });
|
|
}
|
|
|
|
// Alle Kinder eines Agents finden
|
|
export function getChildAgents(parentId: string, agentsList: Agent[]): Agent[] {
|
|
return agentsList.filter((a) => a.parentAgentId === parentId);
|
|
}
|
|
|
|
export function updateAgentStatus(id: string, status: Agent['status']) {
|
|
agents.update((ags) =>
|
|
ags.map((a) => (a.id === id ? { ...a, status } : a))
|
|
);
|
|
}
|
|
|
|
export function addToolCall(agentId: string, tool: string, args: Record<string, unknown>, fixedId?: string): string {
|
|
const id = fixedId || crypto.randomUUID();
|
|
const call: ToolCall = {
|
|
id,
|
|
agentId,
|
|
tool,
|
|
args,
|
|
status: 'running',
|
|
startedAt: new Date()
|
|
};
|
|
|
|
toolCalls.update((calls) => [...calls, call]);
|
|
|
|
// Auch im Agent speichern
|
|
agents.update((ags) =>
|
|
ags.map((a) =>
|
|
a.id === agentId ? { ...a, toolCalls: [...a.toolCalls, call] } : a
|
|
)
|
|
);
|
|
|
|
return id;
|
|
}
|
|
|
|
export function completeToolCall(id: string, result: unknown, failed = false) {
|
|
toolCalls.update((calls) => {
|
|
// Exakte ID-Suche
|
|
let found = calls.some((c) => c.id === id);
|
|
|
|
if (found) {
|
|
return calls.map((c) =>
|
|
c.id === id
|
|
? { ...c, status: (failed ? 'failed' : 'completed') as ToolCall['status'], completedAt: new Date(), result }
|
|
: c
|
|
);
|
|
}
|
|
|
|
// Fallback: Letzten laufenden Tool-Call abschließen (für Events ohne passende ID)
|
|
const lastRunning = [...calls].reverse().find((c) => c.status === 'running');
|
|
if (lastRunning) {
|
|
return calls.map((c) =>
|
|
c.id === lastRunning.id
|
|
? { ...c, status: (failed ? 'failed' : 'completed') as ToolCall['status'], completedAt: new Date(), result }
|
|
: c
|
|
);
|
|
}
|
|
|
|
return calls;
|
|
});
|
|
}
|
|
|
|
export function clearAll() {
|
|
agents.set([]);
|
|
toolCalls.set([]);
|
|
messages.set([]);
|
|
isProcessing.set(false);
|
|
}
|
|
|
|
// DB-Nachricht Format (für Tauri)
|
|
export interface DbMessage {
|
|
id: string;
|
|
session_id: string;
|
|
role: string;
|
|
content: string;
|
|
model: string | null;
|
|
agent_id: string | null; // Agent der die Nachricht erzeugt hat
|
|
timestamp: string;
|
|
}
|
|
|
|
// Konvertierung: Store → DB
|
|
export function messageToDb(msg: Message, sessionId: string): DbMessage {
|
|
return {
|
|
id: msg.id,
|
|
session_id: sessionId,
|
|
role: msg.role,
|
|
content: msg.content,
|
|
model: msg.model || null,
|
|
agent_id: msg.agentId || null,
|
|
timestamp: msg.timestamp.toISOString(),
|
|
};
|
|
}
|
|
|
|
// Konvertierung: DB → Store
|
|
export function dbToMessage(db: DbMessage): Message {
|
|
return {
|
|
id: db.id,
|
|
role: db.role as Message['role'],
|
|
content: db.content,
|
|
model: db.model || undefined,
|
|
agentId: db.agent_id || undefined,
|
|
timestamp: new Date(db.timestamp),
|
|
};
|
|
}
|
|
|
|
// Nachrichten aus DB in Store laden
|
|
export function setMessagesFromDb(dbMessages: DbMessage[]) {
|
|
messages.set(dbMessages.map(dbToMessage));
|
|
}
|
|
|
|
// ============ System-Monitor ============
|
|
|
|
export type MonitorEventType = 'api' | 'hook' | 'tool' | 'mcp' | 'agent' | 'error' | 'debug';
|
|
|
|
export interface MonitorEvent {
|
|
id: string;
|
|
timestamp: Date;
|
|
type: MonitorEventType;
|
|
summary: string; // Einzeiler für Kompakt-Ansicht
|
|
details: Record<string, unknown>; // Vollständige Daten
|
|
sessionId?: string;
|
|
agentId?: string;
|
|
durationMs?: number;
|
|
error?: string;
|
|
}
|
|
|
|
// Farbcodierung für Event-Typen
|
|
export const monitorEventColors: Record<MonitorEventType, string> = {
|
|
api: '🔵',
|
|
hook: '🟢',
|
|
tool: '🟡',
|
|
mcp: '🟣',
|
|
agent: '🟠',
|
|
error: '🔴',
|
|
debug: '⚪',
|
|
};
|
|
|
|
// Monitor Store — Ringbuffer mit max 1000 Events
|
|
const MAX_MONITOR_EVENTS = 1000;
|
|
export const monitorEvents = writable<MonitorEvent[]>([]);
|
|
|
|
// Filter für Monitor-Ansicht
|
|
export const monitorFilter = writable<MonitorEventType | 'all'>('all');
|
|
export const monitorAutoScroll = writable(true);
|
|
export const selectedMonitorEventId = writable<string | null>(null);
|
|
|
|
// Gefilterte Events
|
|
export const filteredMonitorEvents = derived(
|
|
[monitorEvents, monitorFilter],
|
|
([$events, $filter]) => {
|
|
if ($filter === 'all') return $events;
|
|
return $events.filter((e) => e.type === $filter);
|
|
}
|
|
);
|
|
|
|
// Monitor-Statistiken
|
|
export const monitorStats = derived(monitorEvents, ($events) => {
|
|
const last100 = $events.slice(-100);
|
|
const apiEvents = last100.filter((e) => e.type === 'api');
|
|
const errorEvents = last100.filter((e) => e.type === 'error');
|
|
const avgLatency = apiEvents.length > 0
|
|
? apiEvents.reduce((sum, e) => sum + (e.durationMs || 0), 0) / apiEvents.length
|
|
: 0;
|
|
|
|
return {
|
|
totalEvents: $events.length,
|
|
apiCalls: apiEvents.length,
|
|
errors: errorEvents.length,
|
|
avgLatencyMs: Math.round(avgLatency),
|
|
};
|
|
});
|
|
|
|
// Monitor-Event hinzufügen (mit Persistierung)
|
|
export function addMonitorEvent(
|
|
type: MonitorEventType,
|
|
summary: string,
|
|
details: Record<string, unknown> = {},
|
|
options?: Partial<Omit<MonitorEvent, 'id' | 'timestamp' | 'type' | 'summary' | 'details'>>
|
|
) {
|
|
const event: MonitorEvent = {
|
|
id: crypto.randomUUID(),
|
|
timestamp: new Date(),
|
|
type,
|
|
summary,
|
|
details,
|
|
...options,
|
|
};
|
|
|
|
monitorEvents.update((events) => {
|
|
const updated = [...events, event];
|
|
// Ringbuffer: Alte Events entfernen wenn zu viele
|
|
if (updated.length > MAX_MONITOR_EVENTS) {
|
|
return updated.slice(-MAX_MONITOR_EVENTS);
|
|
}
|
|
return updated;
|
|
});
|
|
|
|
// Asynchron in DB speichern (fire-and-forget)
|
|
saveMonitorEventToDb(event).catch((err) => {
|
|
console.warn('Monitor-Event konnte nicht gespeichert werden:', err);
|
|
});
|
|
|
|
return event.id;
|
|
}
|
|
|
|
// Monitor-Event in DB speichern
|
|
async function saveMonitorEventToDb(event: MonitorEvent): Promise<void> {
|
|
// Für DB-Speicherung: Timestamp als ISO-String, Details als JSON-String
|
|
const dbEvent = {
|
|
id: event.id,
|
|
timestamp: event.timestamp.toISOString(),
|
|
event_type: event.type,
|
|
summary: event.summary,
|
|
details: JSON.stringify(event.details),
|
|
agent_id: event.agentId ?? null,
|
|
session_id: null, // TODO: Aktuelle Session-ID übergeben
|
|
duration_ms: event.durationMs ?? null,
|
|
error: event.error ?? null,
|
|
};
|
|
await invoke('save_monitor_event', { event: dbEvent });
|
|
}
|
|
|
|
// Monitor-Events aus DB laden
|
|
export async function loadMonitorEventsFromDb(limit = 500): Promise<void> {
|
|
try {
|
|
interface DbMonitorEvent {
|
|
id: string;
|
|
timestamp: string;
|
|
event_type: string;
|
|
summary: string;
|
|
details: string | null;
|
|
agent_id: string | null;
|
|
session_id: string | null;
|
|
duration_ms: number | null;
|
|
error: string | null;
|
|
}
|
|
|
|
const dbEvents = await invoke<DbMonitorEvent[]>('load_monitor_events', { limit });
|
|
|
|
// DB-Events in Frontend-Format umwandeln (neueste zuerst → umkehren für chronologische Reihenfolge)
|
|
const events: MonitorEvent[] = dbEvents.reverse().map((e) => ({
|
|
id: e.id,
|
|
timestamp: new Date(e.timestamp),
|
|
type: e.event_type as MonitorEventType,
|
|
summary: e.summary,
|
|
details: e.details ? JSON.parse(e.details) : {},
|
|
agentId: e.agent_id ?? undefined,
|
|
durationMs: e.duration_ms ?? undefined,
|
|
error: e.error ?? undefined,
|
|
}));
|
|
|
|
monitorEvents.set(events);
|
|
console.log(`📊 ${events.length} Monitor-Events aus DB geladen`);
|
|
} catch (err) {
|
|
console.error('Fehler beim Laden der Monitor-Events:', err);
|
|
}
|
|
}
|
|
|
|
// Monitor leeren (auch in DB)
|
|
export async function clearMonitorEvents(): Promise<void> {
|
|
monitorEvents.set([]);
|
|
selectedMonitorEventId.set(null);
|
|
|
|
try {
|
|
const count = await invoke<number>('clear_all_monitor_events');
|
|
console.log(`🗑️ ${count} Monitor-Events aus DB gelöscht`);
|
|
} catch (err) {
|
|
console.warn('Monitor-Events konnten nicht aus DB gelöscht werden:', err);
|
|
}
|
|
}
|
|
|
|
// Sensitive Daten maskieren
|
|
export function maskSensitive(data: string): string {
|
|
return data
|
|
.replace(/password[=:]\s*\S+/gi, 'password=***')
|
|
.replace(/api[_-]?key[=:]\s*\S+/gi, 'api_key=***')
|
|
.replace(/bearer\s+\S+/gi, 'Bearer ***')
|
|
.replace(/sk-[a-zA-Z0-9]+/g, 'sk-***')
|
|
.replace(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, '***@***.***');
|
|
}
|