feat(electrobun): agent execution layer — sidecar manager + message adapters + store

- SidecarManager: spawns claude/codex/ollama runners via Bun.spawn(),
  NDJSON stdio protocol, Claude CLI auto-detection, env stripping,
  AbortController stop, Deno/Node runtime detection
- MessageAdapter: parses Claude stream-json, Codex ThreadEvent, Ollama
  chunks into common AgentMessage format
- agent-store.svelte.ts: per-project reactive session state, RPC event
  listeners for agent.message/status/cost
- AgentPane: wired to real sessions (start/stop/prompt), stop button,
  thinking/system message rendering
- ProjectCard: status dot from real agent status, cost/tokens from store
- 5 new RPC types (agent.start/stop/prompt/list + events)
This commit is contained in:
Hibryda 2026-03-22 01:03:05 +01:00
parent 95f1f8208f
commit ef0183de7f
8 changed files with 1566 additions and 61 deletions

View file

@ -2,6 +2,7 @@ import path from "path";
import { BrowserWindow, BrowserView, Updater } from "electrobun/bun";
import { PtyClient } from "./pty-client.ts";
import { settingsDb } from "./settings-db.ts";
import { SidecarManager } from "./sidecar-manager.ts";
import type { PtyRPCSchema } from "../shared/pty-rpc-schema.ts";
import { randomUUID } from "crypto";
@ -11,6 +12,7 @@ const DEV_SERVER_URL = `http://localhost:${DEV_SERVER_PORT}`;
// ── PTY daemon client ────────────────────────────────────────────────────────
const ptyClient = new PtyClient();
const sidecarManager = new SidecarManager();
async function connectToDaemon(retries = 5, delayMs = 500): Promise<boolean> {
for (let attempt = 1; attempt <= retries; attempt++) {
@ -347,6 +349,90 @@ const rpc = BrowserView.defineRPC<PtyRPCSchema>({
return { ok: false };
}
},
// ── Agent handlers ──────────────────────────────────────────────────
"agent.start": ({ sessionId, provider, prompt, cwd, model, systemPrompt, maxTurns, permissionMode, claudeConfigDir, extraEnv }) => {
try {
const result = sidecarManager.startSession(sessionId, provider, prompt, {
cwd,
model,
systemPrompt,
maxTurns,
permissionMode,
claudeConfigDir,
extraEnv,
});
if (result.ok) {
// Forward sidecar messages to webview
sidecarManager.onMessage(sessionId, (sid, messages) => {
try {
rpc.send["agent.message"]({ sessionId: sid, messages });
} catch (err) {
console.error("[agent.message] forward error:", err);
}
});
sidecarManager.onStatus(sessionId, (sid, status, error) => {
try {
rpc.send["agent.status"]({ sessionId: sid, status, error });
} catch (err) {
console.error("[agent.status] forward error:", err);
}
// Send cost update on status change
const sessions = sidecarManager.listSessions();
const session = sessions.find((s) => s.sessionId === sid);
if (session) {
try {
rpc.send["agent.cost"]({
sessionId: sid,
costUsd: session.costUsd,
inputTokens: session.inputTokens,
outputTokens: session.outputTokens,
});
} catch { /* ignore */ }
}
});
}
return result;
} catch (err) {
const error = err instanceof Error ? err.message : String(err);
console.error("[agent.start]", err);
return { ok: false, error };
}
},
"agent.stop": ({ sessionId }) => {
try {
return sidecarManager.stopSession(sessionId);
} catch (err) {
const error = err instanceof Error ? err.message : String(err);
console.error("[agent.stop]", err);
return { ok: false, error };
}
},
"agent.prompt": ({ sessionId, prompt }) => {
try {
return sidecarManager.writePrompt(sessionId, prompt);
} catch (err) {
const error = err instanceof Error ? err.message : String(err);
console.error("[agent.prompt]", err);
return { ok: false, error };
}
},
"agent.list": () => {
try {
return { sessions: sidecarManager.listSessions() };
} catch (err) {
console.error("[agent.list]", err);
return { sessions: [] };
}
},
},
messages: {},