From 66dce7ebaead9cba75ae54377d7b08e0d9ca1763 Mon Sep 17 00:00:00 2001 From: Hibryda Date: Wed, 25 Mar 2026 20:26:49 +0100 Subject: [PATCH] fix(electrobun): 7 bug fixes + 3 partial features completed Bugs fixed: - Agent retry: clear dead session on failed start (was non-recoverable) - seqId persist: save seq_id column in agent_messages, migrate existing DBs - Window frame save: wire resize event listener to saveWindowFrame() - Diagnostics counters: real RPC call counter via withRpcCounting() proxy Partial features completed: - Search auto-seed: rebuildIndex() on startup + indexMessage() on save - Notifications: removed 3 hardcoded demo entries, start empty - Agent cost streaming: emitCostIfChanged() on every message batch New: src/bun/rpc-stats.ts (RPC call + dropped event counters) --- .../src/bun/handlers/agent-handlers.ts | 57 +++++++++++++----- .../src/bun/handlers/misc-handlers.ts | 5 +- ui-electrobun/src/bun/index.ts | 60 ++++++++++++------- ui-electrobun/src/bun/rpc-stats.ts | 23 +++++++ ui-electrobun/src/bun/session-db.ts | 25 ++++++-- ui-electrobun/src/mainview/App.svelte | 5 +- .../src/mainview/agent-store.svelte.ts | 3 + .../mainview/notifications-store.svelte.ts | 8 +-- 8 files changed, 139 insertions(+), 47 deletions(-) create mode 100644 ui-electrobun/src/bun/rpc-stats.ts diff --git a/ui-electrobun/src/bun/handlers/agent-handlers.ts b/ui-electrobun/src/bun/handlers/agent-handlers.ts index c85954d..cff7d9f 100644 --- a/ui-electrobun/src/bun/handlers/agent-handlers.ts +++ b/ui-electrobun/src/bun/handlers/agent-handlers.ts @@ -4,11 +4,13 @@ import type { SidecarManager } from "../sidecar-manager.ts"; import type { SessionDb } from "../session-db.ts"; +import type { SearchDb } from "../search-db.ts"; export function createAgentHandlers( sidecarManager: SidecarManager, sessionDb: SessionDb, sendToWebview: { send: Record void> }, + searchDb?: SearchDb, ) { return { "agent.start": ({ sessionId, provider, prompt, cwd, model, systemPrompt, maxTurns, permissionMode, claudeConfigDir, extraEnv, additionalDirectories, worktreeName }: Record) => { @@ -21,12 +23,38 @@ export function createAgentHandlers( ); if (result.ok) { + // Partial 3: Track last-sent cost to emit live updates only on change + let lastCostUsd = 0; + let lastInputTokens = 0; + let lastOutputTokens = 0; + + /** Emit agent.cost if cost data changed since last emit. */ + function emitCostIfChanged(sid: string): void { + const sessions = sidecarManager.listSessions(); + const session = sessions.find((s: Record) => s.sessionId === sid); + if (!session) return; + const cost = (session.costUsd as number) ?? 0; + const inTok = (session.inputTokens as number) ?? 0; + const outTok = (session.outputTokens as number) ?? 0; + if (cost === lastCostUsd && inTok === lastInputTokens && outTok === lastOutputTokens) return; + lastCostUsd = cost; + lastInputTokens = inTok; + lastOutputTokens = outTok; + try { + (sendToWebview.send as Record)["agent.cost"]({ + sessionId: sid, costUsd: cost, inputTokens: inTok, outputTokens: outTok, + }); + } catch { /* ignore */ } + } + sidecarManager.onMessage(sessionId as string, (sid: string, messages: unknown) => { try { (sendToWebview.send as Record)["agent.message"]({ sessionId: sid, messages }); } catch (err) { console.error("[agent.message] forward error:", err); } + // Partial 3: Stream cost on every message batch + emitCostIfChanged(sid); }); sidecarManager.onStatus(sessionId as string, (sid: string, status: string, error?: string) => { @@ -35,19 +63,7 @@ export function createAgentHandlers( } catch (err) { console.error("[agent.status] forward error:", err); } - - const sessions = sidecarManager.listSessions(); - const session = sessions.find((s: Record) => s.sessionId === sid); - if (session) { - try { - (sendToWebview.send as Record)["agent.cost"]({ - sessionId: sid, - costUsd: session.costUsd, - inputTokens: session.inputTokens, - outputTokens: session.outputTokens, - }); - } catch { /* ignore */ } - } + emitCostIfChanged(sid); }); } @@ -122,9 +138,22 @@ export function createAgentHandlers( sessionDb.saveMessages(messages.map((m) => ({ sessionId: m.sessionId, msgId: m.msgId, role: m.role, content: m.content, toolName: m.toolName, toolInput: m.toolInput, - timestamp: m.timestamp, costUsd: (m.costUsd as number) ?? 0, + timestamp: m.timestamp, seqId: (m.seqId as number) ?? 0, + costUsd: (m.costUsd as number) ?? 0, inputTokens: (m.inputTokens as number) ?? 0, outputTokens: (m.outputTokens as number) ?? 0, }))); + // Partial 1: Index each new message in FTS5 search + if (searchDb) { + for (const m of messages) { + try { + searchDb.indexMessage( + String(m.sessionId ?? ""), + String(m.role ?? ""), + String(m.content ?? ""), + ); + } catch { /* non-critical — don't fail the save */ } + } + } return { ok: true }; } catch (err) { console.error("[session.messages.save]", err); diff --git a/ui-electrobun/src/bun/handlers/misc-handlers.ts b/ui-electrobun/src/bun/handlers/misc-handlers.ts index 073c6d1..9acc2fe 100644 --- a/ui-electrobun/src/bun/handlers/misc-handlers.ts +++ b/ui-electrobun/src/bun/handlers/misc-handlers.ts @@ -16,6 +16,7 @@ import type { RelayClient } from "../relay-client.ts"; import type { SidecarManager } from "../sidecar-manager.ts"; import { checkForUpdates, getLastCheckTimestamp } from "../updater.ts"; import type { TelemetryManager } from "../telemetry.ts"; +import { getRpcCallCount, getDroppedEvents } from "../rpc-stats.ts"; // ── Helpers ───────────────────────────────────────────────────────────────── @@ -199,8 +200,8 @@ export function createMiscHandlers(deps: MiscDeps) { ptyConnected: ptyClient.isConnected, relayConnections: relayClient.listMachines().filter((m) => m.status === "connected").length, activeSidecars: sidecarManager.listSessions().filter((s) => s.status === "running").length, - rpcCallCount: 0, - droppedEvents: 0, + rpcCallCount: getRpcCallCount(), + droppedEvents: getDroppedEvents(), }), // ── Telemetry ─────────────────────────────────────────────────────── diff --git a/ui-electrobun/src/bun/index.ts b/ui-electrobun/src/bun/index.ts index 08d7d2b..a68739d 100644 --- a/ui-electrobun/src/bun/index.ts +++ b/ui-electrobun/src/bun/index.ts @@ -29,6 +29,7 @@ import { createRemoteHandlers } from "./handlers/remote-handlers.ts"; import { createGitHandlers } from "./handlers/git-handlers.ts"; import { createProviderHandlers } from "./handlers/provider-handlers.ts"; import { createMiscHandlers } from "./handlers/misc-handlers.ts"; +import { incrementRpcCallCount, incrementDroppedEvents } from "./rpc-stats.ts"; /** Current app version — sourced from electrobun.config.ts at build time. */ const APP_VERSION = "0.0.1"; @@ -74,7 +75,7 @@ const rpcRef: { send: Record void> } = { send: { const ptyHandlers = createPtyHandlers(ptyClient); const filesHandlers = createFilesHandlers(); const settingsHandlers = createSettingsHandlers(settingsDb); -const agentHandlers = createAgentHandlers(sidecarManager, sessionDb, rpcRef); +const agentHandlers = createAgentHandlers(sidecarManager, sessionDb, rpcRef, searchDb); const btmsgHandlers = createBtmsgHandlers(btmsgDb, rpcRef); const bttaskHandlers = createBttaskHandlers(bttaskDb, rpcRef); const searchHandlers = createSearchHandlers(searchDb); @@ -89,24 +90,42 @@ const miscHandlers = createMiscHandlers({ // Window ref — handlers use closure; set after mainWindow creation let mainWindow: BrowserWindow; +// ── RPC counter wrapper ───────────────────────────────────────────────────── + +/** Wrap each handler to increment the RPC call counter on every invocation. */ +function withRpcCounting unknown>>(handlers: T): T { + const wrapped: Record unknown> = {}; + for (const [key, fn] of Object.entries(handlers)) { + wrapped[key] = (...args: unknown[]) => { + incrementRpcCallCount(); + return fn(...args); + }; + } + return wrapped as T; +} + // ── RPC definition ───────────────────────────────────────────────────────── +const allHandlers = withRpcCounting({ + ...ptyHandlers, + ...filesHandlers, + ...settingsHandlers, + ...agentHandlers, + ...btmsgHandlers, + ...bttaskHandlers, + ...searchHandlers, + ...pluginHandlers, + ...remoteHandlers, + ...gitHandlers, + ...providerHandlers, + ...miscHandlers, +}); + const rpc = BrowserView.defineRPC({ maxRequestTime: 120_000, handlers: { requests: { - ...ptyHandlers, - ...filesHandlers, - ...settingsHandlers, - ...agentHandlers, - ...btmsgHandlers, - ...bttaskHandlers, - ...searchHandlers, - ...pluginHandlers, - ...remoteHandlers, - ...gitHandlers, - ...providerHandlers, - ...miscHandlers, + ...allHandlers, // GTK native drag/resize — delegates to window manager (zero CPU) "window.beginResize": ({ edge }: { edge: string }) => { @@ -202,13 +221,6 @@ relayClient.onStatus((machineId, status, error) => { // ── App window ──────────────────────────────────────────────────────────── async function getMainViewUrl(): Promise { - // TEMPORARY: load resize test stub via Vite dev server (keeps RPC bridge) - const RESIZE_TEST = false; // DISABLED — real app mode - if (RESIZE_TEST) { - const testUrl = DEV_SERVER_URL + "/resize-test.html"; - console.log(`[RESIZE_TEST] Loading stub via Vite: ${testUrl}`); - return testUrl; - } const channel = await Updater.localInfo.channel(); if (channel === "dev") { try { @@ -224,6 +236,14 @@ async function getMainViewUrl(): Promise { connectToDaemon(); +// Partial 1: Seed FTS5 search index on startup +try { + searchDb.rebuildIndex(); + console.log("[search] FTS5 index seeded on startup"); +} catch (err) { + console.error("[search] Failed to seed FTS5 index:", err); +} + const url = await getMainViewUrl(); const savedX = Number(settingsDb.getSetting("win_x") ?? 100); diff --git a/ui-electrobun/src/bun/rpc-stats.ts b/ui-electrobun/src/bun/rpc-stats.ts new file mode 100644 index 0000000..5ea672d --- /dev/null +++ b/ui-electrobun/src/bun/rpc-stats.ts @@ -0,0 +1,23 @@ +/** + * Simple RPC call counter for diagnostics. + * Incremented on each RPC request, read by diagnostics.stats handler. + */ + +let _rpcCallCount = 0; +let _droppedEvents = 0; + +export function incrementRpcCallCount(): void { + _rpcCallCount++; +} + +export function incrementDroppedEvents(): void { + _droppedEvents++; +} + +export function getRpcCallCount(): number { + return _rpcCallCount; +} + +export function getDroppedEvents(): number { + return _droppedEvents; +} diff --git a/ui-electrobun/src/bun/session-db.ts b/ui-electrobun/src/bun/session-db.ts index e16f7bc..826c1ab 100644 --- a/ui-electrobun/src/bun/session-db.ts +++ b/ui-electrobun/src/bun/session-db.ts @@ -39,6 +39,7 @@ export interface StoredMessage { toolName?: string; toolInput?: string; timestamp: number; + seqId: number; costUsd: number; inputTokens: number; outputTokens: number; @@ -72,6 +73,7 @@ CREATE TABLE IF NOT EXISTS agent_messages ( tool_name TEXT, tool_input TEXT, timestamp INTEGER NOT NULL, + seq_id INTEGER NOT NULL DEFAULT 0, cost_usd REAL NOT NULL DEFAULT 0, input_tokens INTEGER NOT NULL DEFAULT 0, output_tokens INTEGER NOT NULL DEFAULT 0, @@ -91,6 +93,17 @@ export class SessionDb { constructor() { this.db = openDb(DB_PATH, { foreignKeys: true }); this.db.exec(SESSION_SCHEMA); + this.migrate(); + } + + /** Run schema migrations for existing DBs. */ + private migrate(): void { + // Add seq_id column if missing (for DBs created before this field existed) + try { + this.db.run("ALTER TABLE agent_messages ADD COLUMN seq_id INTEGER NOT NULL DEFAULT 0"); + } catch { + // Column already exists — expected + } } // ── Sessions ───────────────────────────────────────────────────────────── @@ -212,8 +225,8 @@ export class SessionDb { .query( `INSERT INTO agent_messages (session_id, msg_id, role, content, tool_name, tool_input, - timestamp, cost_usd, input_tokens, output_tokens) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10) + timestamp, seq_id, cost_usd, input_tokens, output_tokens) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11) ON CONFLICT(session_id, msg_id) DO NOTHING` ) .run( @@ -224,6 +237,7 @@ export class SessionDb { m.toolName ?? null, m.toolInput ?? null, m.timestamp, + m.seqId ?? 0, m.costUsd, m.inputTokens, m.outputTokens @@ -234,8 +248,8 @@ export class SessionDb { const stmt = this.db.prepare( `INSERT INTO agent_messages (session_id, msg_id, role, content, tool_name, tool_input, - timestamp, cost_usd, input_tokens, output_tokens) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10) + timestamp, seq_id, cost_usd, input_tokens, output_tokens) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11) ON CONFLICT(session_id, msg_id) DO NOTHING` ); @@ -249,6 +263,7 @@ export class SessionDb { m.toolName ?? null, m.toolInput ?? null, m.timestamp, + m.seqId ?? 0, m.costUsd, m.inputTokens, m.outputTokens @@ -269,6 +284,7 @@ export class SessionDb { tool_name: string | null; tool_input: string | null; timestamp: number; + seq_id: number; cost_usd: number; input_tokens: number; output_tokens: number; @@ -289,6 +305,7 @@ export class SessionDb { toolName: r.tool_name ?? undefined, toolInput: r.tool_input ?? undefined, timestamp: r.timestamp, + seqId: r.seq_id ?? 0, costUsd: r.cost_usd, inputTokens: r.input_tokens, outputTokens: r.output_tokens, diff --git a/ui-electrobun/src/mainview/App.svelte b/ui-electrobun/src/mainview/App.svelte index 5c3218d..e5537a3 100644 --- a/ui-electrobun/src/mainview/App.svelte +++ b/ui-electrobun/src/mainview/App.svelte @@ -258,6 +258,9 @@ keybindingStore.on("group4", () => setActiveGroup(getGroups()[3]?.id)); keybindingStore.on("minimize", () => handleMinimize()); + // Bug 3: Persist window frame on resize (debounced inside saveWindowFrame) + window.addEventListener("resize", saveWindowFrame); + function handleSearchShortcut(e: KeyboardEvent) { if (e.ctrlKey && e.shiftKey && e.key === "F") { e.preventDefault(); @@ -316,7 +319,7 @@ clearInterval(sessionId); document.removeEventListener("keydown", handleSearchShortcut); window.removeEventListener("palette-command", handlePaletteCommand); - // Native resize — no JS listeners to clean + window.removeEventListener("resize", saveWindowFrame); }; }); diff --git a/ui-electrobun/src/mainview/agent-store.svelte.ts b/ui-electrobun/src/mainview/agent-store.svelte.ts index 2cfb595..0e5ba80 100644 --- a/ui-electrobun/src/mainview/agent-store.svelte.ts +++ b/ui-electrobun/src/mainview/agent-store.svelte.ts @@ -549,6 +549,9 @@ async function _startAgentInner( if (!result.ok) { sessions[sessionId].status = 'error'; sessions[sessionId].error = result.error; + // Bug 1: Clear the dead session so the next send starts fresh + projectSessionMap.delete(projectId); + clearStallTimer(sessionId); } return result; diff --git a/ui-electrobun/src/mainview/notifications-store.svelte.ts b/ui-electrobun/src/mainview/notifications-store.svelte.ts index e49189a..08711e4 100644 --- a/ui-electrobun/src/mainview/notifications-store.svelte.ts +++ b/ui-electrobun/src/mainview/notifications-store.svelte.ts @@ -15,13 +15,9 @@ export interface Notification { // ── State ───────────────────────────────────────────────────────────────── -let notifications = $state([ - { id: 1, message: 'Agent completed: wake scheduler implemented', type: 'success', time: '2m ago' }, - { id: 2, message: 'Context pressure: 78% on agent-orchestrator', type: 'warning', time: '5m ago' }, - { id: 3, message: 'PTY daemon connected', type: 'info', time: '12m ago' }, -]); +let notifications = $state([]); -let nextId = $state(100); +let nextId = $state(1); // ── Public API ────────────────────────────────────────────────────────────