- 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>
371 lines
9 KiB
TypeScript
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, '***@***.***');
|
|
}
|