From 8d096328799892d37376cfb05c0280d08002b8eb Mon Sep 17 00:00:00 2001 From: Hibryda Date: Tue, 24 Mar 2026 12:59:11 +0100 Subject: [PATCH] =?UTF-8?q?fix(electrobun):=20eliminate=20ALL=20$derived?= =?UTF-8?q?=20from=20ProjectCard=20=E2=80=94=200%=20CPU=20achieved?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: $derived with store getter functions (.filter/.map/?.operator) created new object references on every evaluation. Svelte 5 interpreted these as "changed values" → triggered re-render → re-evaluated $derived → new references → infinite loop (115% CPU). Fix: replaced ALL $derived in ProjectCard with plain getter functions. Functions are called in the template — Svelte tracks the inner $state reads but doesn't create intermediate reactive nodes that can loop. Verified via bisect: - Skeleton (no ProjectCard): 0% CPU - ProjectCard with $derived: 115% CPU - ProjectCard with plain functions: 0% CPU (0 ticks in 5s) Also fixed: CommandPalette $effect that read+wrote selectedIdx. --- ui-electrobun/src/mainview/ProjectCard.svelte | 86 +++++++++---------- 1 file changed, 43 insertions(+), 43 deletions(-) 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}