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:
parent
ef0183de7f
commit
29a3370e79
18 changed files with 331 additions and 114 deletions
|
|
@ -5,7 +5,7 @@
|
|||
* Exposes reactive Svelte 5 rune state per project.
|
||||
*/
|
||||
|
||||
import { electrobun, appRpc } from './main.ts';
|
||||
import { appRpc } from './rpc.ts';
|
||||
|
||||
// ── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -45,6 +45,24 @@ interface StartOptions {
|
|||
extraEnv?: Record<string, string>;
|
||||
}
|
||||
|
||||
// ── Env var validation (Fix #14) ─────────────────────────────────────────────
|
||||
|
||||
const BLOCKED_ENV_PREFIXES = ['CLAUDE', 'CODEX', 'OLLAMA', 'ANTHROPIC_'];
|
||||
|
||||
function validateExtraEnv(env: Record<string, string> | undefined): Record<string, string> | undefined {
|
||||
if (!env) return undefined;
|
||||
const clean: Record<string, string> = {};
|
||||
for (const [key, value] of Object.entries(env)) {
|
||||
const blocked = BLOCKED_ENV_PREFIXES.some(p => key.startsWith(p));
|
||||
if (blocked) {
|
||||
console.warn(`[agent-store] Rejected extraEnv key "${key}" — provider-prefixed keys are not allowed`);
|
||||
continue;
|
||||
}
|
||||
clean[key] = value;
|
||||
}
|
||||
return Object.keys(clean).length > 0 ? clean : undefined;
|
||||
}
|
||||
|
||||
// ── Internal state ───────────────────────────────────────────────────────────
|
||||
|
||||
// Map projectId -> sessionId for lookup
|
||||
|
|
@ -53,6 +71,9 @@ const projectSessionMap = new Map<string, string>();
|
|||
// Map sessionId -> reactive session state
|
||||
let sessions = $state<Record<string, AgentSession>>({});
|
||||
|
||||
// Grace period timers for cleanup after done/error
|
||||
const cleanupTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
||||
|
||||
// ── RPC event listeners (registered once) ────────────────────────────────────
|
||||
|
||||
let listenersRegistered = false;
|
||||
|
|
@ -62,7 +83,7 @@ function ensureListeners() {
|
|||
listenersRegistered = true;
|
||||
|
||||
// agent.message — raw messages from sidecar, converted to display format
|
||||
electrobun.rpc?.addMessageListener('agent.message', (payload: {
|
||||
appRpc.addMessageListener('agent.message', (payload: {
|
||||
sessionId: string;
|
||||
messages: Array<{
|
||||
id: string;
|
||||
|
|
@ -87,7 +108,7 @@ function ensureListeners() {
|
|||
});
|
||||
|
||||
// agent.status — session status changes
|
||||
electrobun.rpc?.addMessageListener('agent.status', (payload: {
|
||||
appRpc.addMessageListener('agent.status', (payload: {
|
||||
sessionId: string;
|
||||
status: string;
|
||||
error?: string;
|
||||
|
|
@ -97,10 +118,15 @@ function ensureListeners() {
|
|||
|
||||
session.status = normalizeStatus(payload.status);
|
||||
if (payload.error) session.error = payload.error;
|
||||
|
||||
// Schedule cleanup after done/error (Fix #2)
|
||||
if (session.status === 'done' || session.status === 'error') {
|
||||
scheduleCleanup(session.sessionId, session.projectId);
|
||||
}
|
||||
});
|
||||
|
||||
// agent.cost — token/cost updates
|
||||
electrobun.rpc?.addMessageListener('agent.cost', (payload: {
|
||||
appRpc.addMessageListener('agent.cost', (payload: {
|
||||
sessionId: string;
|
||||
costUsd: number;
|
||||
inputTokens: number;
|
||||
|
|
@ -115,6 +141,32 @@ function ensureListeners() {
|
|||
});
|
||||
}
|
||||
|
||||
// ── Cleanup scheduling (Fix #2) ──────────────────────────────────────────────
|
||||
|
||||
const CLEANUP_GRACE_MS = 60_000; // 60 seconds after done/error
|
||||
|
||||
function scheduleCleanup(sessionId: string, projectId: string) {
|
||||
// Cancel any existing timer for this session
|
||||
const existing = cleanupTimers.get(sessionId);
|
||||
if (existing) clearTimeout(existing);
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
cleanupTimers.delete(sessionId);
|
||||
// Only clean up if session is still in done/error state
|
||||
const session = sessions[sessionId];
|
||||
if (session && (session.status === 'done' || session.status === 'error')) {
|
||||
// Keep session data (messages, cost) but remove from projectSessionMap
|
||||
// so starting a new session on this project works cleanly
|
||||
const currentMapped = projectSessionMap.get(projectId);
|
||||
if (currentMapped === sessionId) {
|
||||
projectSessionMap.delete(projectId);
|
||||
}
|
||||
}
|
||||
}, CLEANUP_GRACE_MS);
|
||||
|
||||
cleanupTimers.set(sessionId, timer);
|
||||
}
|
||||
|
||||
// ── Message conversion ───────────────────────────────────────────────────────
|
||||
|
||||
function convertRawMessage(raw: {
|
||||
|
|
@ -146,7 +198,6 @@ function convertRawMessage(raw: {
|
|||
case 'tool_call': {
|
||||
const name = String(c?.name ?? 'Tool');
|
||||
const input = c?.input as Record<string, unknown> | undefined;
|
||||
// Extract file path from common tool input patterns
|
||||
const path = extractToolPath(name, input);
|
||||
return {
|
||||
id: raw.id,
|
||||
|
|
@ -174,7 +225,6 @@ function convertRawMessage(raw: {
|
|||
|
||||
case 'init': {
|
||||
const model = String(c?.model ?? '');
|
||||
// Update session model from init message
|
||||
const sid = String(c?.sessionId ?? '');
|
||||
for (const s of Object.values(sessions)) {
|
||||
if (s.sessionId === raw.id || (sid && s.sessionId.includes(sid.slice(0, 8)))) {
|
||||
|
|
@ -210,7 +260,6 @@ function convertRawMessage(raw: {
|
|||
|
||||
function extractToolPath(name: string, input: Record<string, unknown> | undefined): string | undefined {
|
||||
if (!input) return undefined;
|
||||
// Common patterns: file_path, path, command (for Bash)
|
||||
if (typeof input.file_path === 'string') return input.file_path;
|
||||
if (typeof input.path === 'string') return input.path;
|
||||
if (name === 'Bash' && typeof input.command === 'string') {
|
||||
|
|
@ -241,7 +290,7 @@ function normalizeStatus(status: string): AgentStatus {
|
|||
|
||||
// ── Public API ───────────────────────────────────────────────────────────────
|
||||
|
||||
/** Start an agent session for a project. */
|
||||
/** Start an agent session for a project (Fix #5: reads permission_mode + system_prompt from settings). */
|
||||
export async function startAgent(
|
||||
projectId: string,
|
||||
provider: string,
|
||||
|
|
@ -250,8 +299,24 @@ export async function startAgent(
|
|||
): Promise<{ ok: boolean; error?: string }> {
|
||||
ensureListeners();
|
||||
|
||||
// If there's an existing done/error session for this project, clear it first
|
||||
clearSession(projectId);
|
||||
|
||||
const sessionId = `${projectId}-${Date.now()}`;
|
||||
|
||||
// Read settings defaults if not explicitly provided (Fix #5)
|
||||
let permissionMode = options.permissionMode;
|
||||
let systemPrompt = options.systemPrompt;
|
||||
try {
|
||||
const { settings } = await appRpc.request['settings.getAll']({});
|
||||
if (!permissionMode && settings['permission_mode']) {
|
||||
permissionMode = settings['permission_mode'];
|
||||
}
|
||||
if (!systemPrompt && settings['system_prompt_template']) {
|
||||
systemPrompt = settings['system_prompt_template'];
|
||||
}
|
||||
} catch { /* use provided or defaults */ }
|
||||
|
||||
// Create reactive session state
|
||||
sessions[sessionId] = {
|
||||
sessionId,
|
||||
|
|
@ -278,11 +343,11 @@ export async function startAgent(
|
|||
prompt,
|
||||
cwd: options.cwd,
|
||||
model: options.model,
|
||||
systemPrompt: options.systemPrompt,
|
||||
systemPrompt: systemPrompt,
|
||||
maxTurns: options.maxTurns,
|
||||
permissionMode: options.permissionMode,
|
||||
permissionMode: permissionMode,
|
||||
claudeConfigDir: options.claudeConfigDir,
|
||||
extraEnv: options.extraEnv,
|
||||
extraEnv: validateExtraEnv(options.extraEnv),
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
|
|
@ -341,5 +406,26 @@ export function hasSession(projectId: string): boolean {
|
|||
return projectSessionMap.has(projectId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear a done/error session for a project (Fix #2).
|
||||
* Removes from projectSessionMap so a new session can start.
|
||||
* Keeps session data in sessions map for history access.
|
||||
*/
|
||||
export function clearSession(projectId: string): void {
|
||||
const sessionId = projectSessionMap.get(projectId);
|
||||
if (!sessionId) return;
|
||||
|
||||
const session = sessions[sessionId];
|
||||
if (session && (session.status === 'done' || session.status === 'error')) {
|
||||
projectSessionMap.delete(projectId);
|
||||
// Cancel any pending cleanup timer
|
||||
const timer = cleanupTimers.get(sessionId);
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
cleanupTimers.delete(sessionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Initialize listeners on module load. */
|
||||
ensureListeners();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue