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)
This commit is contained in:
parent
0dd402e282
commit
66dce7ebae
8 changed files with 139 additions and 47 deletions
|
|
@ -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<string, (...args: unknown[]) => void> },
|
||||
searchDb?: SearchDb,
|
||||
) {
|
||||
return {
|
||||
"agent.start": ({ sessionId, provider, prompt, cwd, model, systemPrompt, maxTurns, permissionMode, claudeConfigDir, extraEnv, additionalDirectories, worktreeName }: Record<string, unknown>) => {
|
||||
|
|
@ -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<string, unknown>) => 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<string, Function>)["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<string, Function>)["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<string, unknown>) => s.sessionId === sid);
|
||||
if (session) {
|
||||
try {
|
||||
(sendToWebview.send as Record<string, Function>)["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);
|
||||
|
|
|
|||
|
|
@ -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 ───────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -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<string, (...args: unknown[]) => 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,12 +90,23 @@ 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<T extends Record<string, (...args: unknown[]) => unknown>>(handlers: T): T {
|
||||
const wrapped: Record<string, (...args: unknown[]) => unknown> = {};
|
||||
for (const [key, fn] of Object.entries(handlers)) {
|
||||
wrapped[key] = (...args: unknown[]) => {
|
||||
incrementRpcCallCount();
|
||||
return fn(...args);
|
||||
};
|
||||
}
|
||||
return wrapped as T;
|
||||
}
|
||||
|
||||
// ── RPC definition ─────────────────────────────────────────────────────────
|
||||
|
||||
const rpc = BrowserView.defineRPC<PtyRPCSchema>({
|
||||
maxRequestTime: 120_000,
|
||||
handlers: {
|
||||
requests: {
|
||||
const allHandlers = withRpcCounting({
|
||||
...ptyHandlers,
|
||||
...filesHandlers,
|
||||
...settingsHandlers,
|
||||
|
|
@ -107,6 +119,13 @@ const rpc = BrowserView.defineRPC<PtyRPCSchema>({
|
|||
...gitHandlers,
|
||||
...providerHandlers,
|
||||
...miscHandlers,
|
||||
});
|
||||
|
||||
const rpc = BrowserView.defineRPC<PtyRPCSchema>({
|
||||
maxRequestTime: 120_000,
|
||||
handlers: {
|
||||
requests: {
|
||||
...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<string> {
|
||||
// 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<string> {
|
|||
|
||||
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);
|
||||
|
|
|
|||
23
ui-electrobun/src/bun/rpc-stats.ts
Normal file
23
ui-electrobun/src/bun/rpc-stats.ts
Normal file
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -15,13 +15,9 @@ export interface Notification {
|
|||
|
||||
// ── State ─────────────────────────────────────────────────────────────────
|
||||
|
||||
let notifications = $state<Notification[]>([
|
||||
{ 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<Notification[]>([]);
|
||||
|
||||
let nextId = $state(100);
|
||||
let nextId = $state(1);
|
||||
|
||||
// ── Public API ────────────────────────────────────────────────────────────
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue