fix(electrobun): address all 20 Codex review findings

CRITICAL:
- PTY leak: Terminal.svelte now calls pty.close on destroy, not just unsubscribe
- Agent session cleanup: clearSession() removes done/error sessions, backend
  deletes after 60s grace period

HIGH:
- Clone branch passthrough: user's branch name flows through callback
- Circular imports: extracted rpc.ts singleton, broke main.ts ↔ App.svelte cycle
- Settings wired to runtime: Terminal reads cursor/scrollback from settings
- Security disclaimer: added "prototype — not system keyring" notice
- ThemeEditor: fixed basePalette → initialPalette reference

MEDIUM:
- Clone race: UUID suffix instead of count-based index
- Silent failures: structured error returns from PTY handlers
- WebKitGTK mount: only current + previous group mounted
- Debug listeners: gated behind DEBUG, cleanup on destroy
- NDJSON residual buffer parsed on process exit
- Codex adapter: deduplicated tool_call/tool_result
- extraEnv: rejects CLAUDE*/CODEX*/OLLAMA* keys
- settings-db: runMigrations() with version tracking
- active_group: persisted via settings.set

LOW:
- Removed dead demo code, unused variables
- color-mix() fallbacks added
This commit is contained in:
Hibryda 2026-03-22 01:20:23 +01:00
parent ef0183de7f
commit 29a3370e79
18 changed files with 331 additions and 114 deletions

View file

@ -41,11 +41,25 @@ interface ActiveSession {
onStatus: StatusCallback[];
}
// ── Environment stripping ────────────────────────────────────────────────────
// ── Environment stripping (Fix #14) ──────────────────────────────────────────
const STRIP_PREFIXES = ["CLAUDE", "CODEX", "OLLAMA", "ANTHROPIC_"];
const WHITELIST_PREFIXES = ["CLAUDE_CODE_EXPERIMENTAL_"];
function validateExtraEnv(extraEnv: Record<string, string> | undefined): Record<string, string> | undefined {
if (!extraEnv) return undefined;
const clean: Record<string, string> = {};
for (const [key, value] of Object.entries(extraEnv)) {
const blocked = STRIP_PREFIXES.some((p) => key.startsWith(p));
if (blocked) {
console.warn(`[sidecar] Rejected extraEnv key "${key}" — provider-prefixed keys not allowed`);
continue;
}
clean[key] = value;
}
return Object.keys(clean).length > 0 ? clean : undefined;
}
function buildCleanEnv(extraEnv?: Record<string, string>, claudeConfigDir?: string): Record<string, string> {
const clean: Record<string, string> = {};
@ -61,8 +75,10 @@ function buildCleanEnv(extraEnv?: Record<string, string>, claudeConfigDir?: stri
if (claudeConfigDir) {
clean["CLAUDE_CONFIG_DIR"] = claudeConfigDir;
}
if (extraEnv) {
Object.assign(clean, extraEnv);
// Apply validated extraEnv
const validated = validateExtraEnv(extraEnv);
if (validated) {
Object.assign(clean, validated);
}
return clean;
@ -93,13 +109,11 @@ function findClaudeCli(): string | undefined {
// ── Runner resolution ────────────────────────────────────────────────────────
function resolveRunnerPath(provider: ProviderId): string {
// Sidecar runners live in the repo's sidecar/dist/ directory
const repoRoot = join(import.meta.dir, "..", "..", "..");
return join(repoRoot, "sidecar", "dist", `${provider}-runner.mjs`);
}
function findNodeRuntime(): string {
// Prefer Deno, fallback to Node.js (matching Tauri sidecar behavior)
try {
const result = Bun.spawnSync(["which", "deno"]);
const path = new TextDecoder().decode(result.stdout).trim();
@ -115,10 +129,15 @@ function findNodeRuntime(): string {
return "node"; // last resort
}
// ── Cleanup grace period ─────────────────────────────────────────────────────
const CLEANUP_GRACE_MS = 60_000; // 60s after done/error before removing session
// ── SidecarManager ───────────────────────────────────────────────────────────
export class SidecarManager {
private sessions = new Map<string, ActiveSession>();
private cleanupTimers = new Map<string, ReturnType<typeof setTimeout>>();
private claudePath: string | undefined;
private nodeRuntime: string;
@ -197,6 +216,8 @@ export class SidecarManager {
if (s) {
s.state.status = exitCode === 0 ? "done" : "error";
this.emitStatus(sessionId, s.state.status, exitCode !== 0 ? `Exit code: ${exitCode}` : undefined);
// Schedule cleanup (Fix #2)
this.scheduleCleanup(sessionId);
}
});
@ -211,7 +232,7 @@ export class SidecarManager {
maxTurns: options.maxTurns,
permissionMode: options.permissionMode ?? "bypassPermissions",
claudeConfigDir: options.claudeConfigDir,
extraEnv: options.extraEnv,
extraEnv: validateExtraEnv(options.extraEnv),
};
this.writeToProcess(sessionId, queryMsg);
@ -236,7 +257,7 @@ export class SidecarManager {
s.controller.abort();
s.state.status = "done";
this.emitStatus(sessionId, "done");
this.sessions.delete(sessionId);
this.scheduleCleanup(sessionId);
}
}, 3000);
@ -290,6 +311,30 @@ export class SidecarManager {
}
this.sessions.delete(sessionId);
}
// Cancel any cleanup timer
const timer = this.cleanupTimers.get(sessionId);
if (timer) {
clearTimeout(timer);
this.cleanupTimers.delete(sessionId);
}
}
// ── Cleanup scheduling (Fix #2) ─────────────────────────────────────────
private scheduleCleanup(sessionId: string): void {
// Cancel any existing timer
const existing = this.cleanupTimers.get(sessionId);
if (existing) clearTimeout(existing);
const timer = setTimeout(() => {
this.cleanupTimers.delete(sessionId);
const session = this.sessions.get(sessionId);
if (session && (session.state.status === "done" || session.state.status === "error")) {
this.sessions.delete(sessionId);
}
}, CLEANUP_GRACE_MS);
this.cleanupTimers.set(sessionId, timer);
}
// ── Internal ───────────────────────────────────────────────────────────────
@ -324,6 +369,12 @@ export class SidecarManager {
this.handleNdjsonLine(sessionId, session, line);
}
}
// Fix #12: Parse any residual data left in the buffer after stream ends
const residual = buffer.trim();
if (residual) {
this.handleNdjsonLine(sessionId, session, residual);
}
} catch (err) {
// Stream closed — expected on process exit
if (!session.controller.signal.aborted) {
@ -339,7 +390,6 @@ export class SidecarManager {
try {
for await (const chunk of reader) {
const text = decoder.decode(chunk, { stream: true });
// Log sidecar stderr as debug output
for (const line of text.split("\n")) {
if (line.trim()) {
console.log(`[sidecar:${sessionId}] ${line.trim()}`);