diff --git a/ui-electrobun/src/mainview/ProjectCard.svelte b/ui-electrobun/src/mainview/ProjectCard.svelte index fae8916..6c5caac 100644 --- a/ui-electrobun/src/mainview/ProjectCard.svelte +++ b/ui-electrobun/src/mainview/ProjectCard.svelte @@ -60,18 +60,17 @@ }: Props = $props(); // ── Agent session (reactive from store) ────────────────────────── - // CRITICAL: Use shared constants for fallback values. Creating new [] or {} - // inside $derived causes infinite loops because each evaluation returns a - // new reference → Svelte thinks the value changed → re-renders → new ref → loop. const EMPTY_MESSAGES: AgentMessage[] = []; - let session = $derived(getSession(id)); - let agentStatus: AgentStatus = $derived(session?.status ?? 'idle'); - let agentMessages: AgentMessage[] = $derived(session?.messages ?? EMPTY_MESSAGES); - let agentCost = $derived(session?.costUsd ?? 0); - let agentTokens = $derived((session?.inputTokens ?? 0) + (session?.outputTokens ?? 0)); - let agentModel = $derived(session?.model ?? model); - let agentInputTokens = $derived(session?.inputTokens ?? 0); - let agentOutputTokens = $derived(session?.outputTokens ?? 0); + // ALL agent session state as plain getter functions — NO $derived. + // $derived + store getters that return new refs = infinite loops in Svelte 5. + function getAgentSession() { return getSession(id); } + function getAgentStatus(): AgentStatus { return getAgentSession()?.status ?? 'idle'; } + function getAgentMessages(): AgentMessage[] { return getAgentSession()?.messages ?? []; } + function getAgentCost(): number { return getAgentSession()?.costUsd ?? 0; } + function getAgentTokens(): number { return (getAgentSession()?.inputTokens ?? 0) + (getAgentSession()?.outputTokens ?? 0); } + function getAgentModel(): string { return getAgentSession()?.model ?? model; } + function getAgentInputTokens(): number { return getAgentSession()?.inputTokens ?? 0; } + function getAgentOutputTokens(): number { return getAgentSession()?.outputTokens ?? 0; } // Context limit per model (approximate) const MODEL_LIMITS: Record = { @@ -81,17 +80,18 @@ 'gpt-5.4': 128000, 'qwen3:8b': 32000, }; - let contextLimit = $derived(MODEL_LIMITS[agentModel] ?? 200000); - let computedContextPct = $derived( - agentInputTokens > 0 ? Math.min(100, Math.round((agentInputTokens / contextLimit) * 100)) : (contextPct ?? 0) - ); + function getContextLimit(): number { return MODEL_LIMITS[getAgentModel()] ?? 200000; } + function getComputedContextPct(): number { + const inp = getAgentInputTokens(); + return inp > 0 ? Math.min(100, Math.round((inp / getContextLimit()) * 100)) : (contextPct ?? 0); + } // File references from tool_call messages // NOTE: Do NOT use $derived with .filter()/.map() — creates new arrays every // evaluation, which Svelte interprets as "changed" → re-render → new arrays → loop. // Instead, compute these as plain functions called in template (no caching). function getFileRefs(): string[] { - return agentMessages + return getAgentMessages() .filter((m) => m.role === 'tool-call' && m.toolPath) .map((m) => m.toolPath!) .filter((p, i, arr) => arr.indexOf(p) === i) @@ -99,12 +99,12 @@ } function getTurnCount(): number { - return agentMessages.filter((m) => m.role === 'user' || m.role === 'assistant').length; + return getAgentMessages().filter((m) => m.role === 'user' || m.role === 'assistant').length; } // ── Display messages (no $derived — avoids new ref on re-render) ── function getDisplayMessages(): AgentMessage[] { - return agentMessages.length > 0 ? agentMessages : EMPTY_MESSAGES; + return getAgentMessages().length > 0 ? getAgentMessages() : EMPTY_MESSAGES; } // ── Clone dialog state ────────────────────────────────────────── @@ -130,7 +130,7 @@ } // Derived from project-tabs-store for reactive reads - let activeTab = $derived(getActiveTab(id)); + function getCurrentTab() { return getActiveTab(id); } // ── Load last session on mount (once, not reactive) ───────────────── import { onMount } from 'svelte'; @@ -169,8 +169,8 @@ >
-
- +
+
{name} @@ -255,9 +255,9 @@ {#each ALL_TABS as tab}