claude-desktop/src/lib/stores/app.ts
Eddy abaf4eb9bf Phase 6: Session-Management Verbesserungen
- Session Auto-Load bei App-Start (aktive Session + Nachrichten)
- agent_id Spalte in messages-Tabelle für Agent-Zuordnung
- DbMessage Interface erweitert (agent_id)
- Session-Compacting: compact_session() fasst alte Nachrichten zusammen
- Standard: 30 letzte Nachrichten behalten, Rest als Summary

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-14 13:05:16 +02:00

371 lines
9 KiB
TypeScript

// Claude Desktop — App-State
import { writable, derived } from 'svelte/store';
// 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;
}
export interface Message {
id: string;
role: 'user' | 'assistant' | 'system';
content: string;
timestamp: Date;
agentId?: string;
model?: string;
}
export interface Permission {
id: string;
pattern: string;
type: 'session' | 'permanent';
action: 'allow' | 'deny';
createdAt: Date;
}
// 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 currentInput = writable('');
export const selectedAgentId = writable<string | null>(null);
export const currentModel = writable('');
export const currentSessionId = writable<string | null>(null);
// Session-Statistiken (kumuliert)
export const sessionStats = writable({
totalTokensIn: 0,
totalTokensOut: 0,
totalCost: 0,
messageCount: 0,
});
// 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>): string {
const id = 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) =>
calls.map((c) =>
c.id === id
? {
...c,
status: failed ? 'failed' : 'completed',
completedAt: new Date(),
result
}
: c
)
);
}
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
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;
});
return event.id;
}
// Monitor leeren
export function clearMonitorEvents() {
monitorEvents.set([]);
}
// 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, '***@***.***');
}