feat(electrobun): final 5% — full integration, real data, polish

1. Claude CLI: additionalDirectories + worktreeName passthrough
2. Agent-store: reads settings (default_cwd, provider model, permission)
3. Project hydration: SQLite replaces hardcoded PROJECTS, add/remove UI
4. Group hydration: SQLite groups, add/delete in sidebar
5. Terminal auto-spawn: reads default_cwd from settings
6. Context tab: real tokens from agent-store, file refs, turn count
7. Memory tab: Memora DB integration (read-only, graceful if missing)
8. Docs tab: markdown viewer (files.list + files.read + inline renderer)
9. SSH tab: CRUD connections, spawn PTY with ssh command
10. Error handling: global unhandledrejection → toast notifications
11. Notifications: agent done/error/stall → toasts, 15min stall timer
12. Command palette: all 18 commands (was 10)

+1,198 lines, 13 files. Electrobun now 100% feature-complete vs Tauri v3.
This commit is contained in:
Hibryda 2026-03-22 02:02:54 +01:00
parent 4826b9dffa
commit 8e756d3523
13 changed files with 1199 additions and 239 deletions

View file

@ -43,6 +43,48 @@ interface StartOptions {
permissionMode?: string;
claudeConfigDir?: string;
extraEnv?: Record<string, string>;
additionalDirectories?: string[];
worktreeName?: string;
}
// ── Toast callback (set by App.svelte) ────────────────────────────────────────
type ToastFn = (message: string, variant: 'success' | 'warning' | 'error' | 'info') => void;
let _toastFn: ToastFn | null = null;
/** Register a toast callback for agent notifications. */
export function setAgentToastFn(fn: ToastFn): void { _toastFn = fn; }
function emitToast(message: string, variant: 'success' | 'warning' | 'error' | 'info') {
_toastFn?.(message, variant);
}
// ── Stall detection ───────────────────────────────────────────────────────────
const stallTimers = new Map<string, ReturnType<typeof setTimeout>>();
const DEFAULT_STALL_MS = 15 * 60 * 1000; // 15 minutes
function resetStallTimer(sessionId: string, projectId: string): void {
const existing = stallTimers.get(sessionId);
if (existing) clearTimeout(existing);
const timer = setTimeout(() => {
stallTimers.delete(sessionId);
const session = sessions[sessionId];
if (session && session.status === 'running') {
emitToast(`Agent stalled on ${projectId} (no activity for 15 min)`, 'warning');
}
}, DEFAULT_STALL_MS);
stallTimers.set(sessionId, timer);
}
function clearStallTimer(sessionId: string): void {
const timer = stallTimers.get(sessionId);
if (timer) {
clearTimeout(timer);
stallTimers.delete(sessionId);
}
}
// ── Env var validation (Fix #14) ─────────────────────────────────────────────
@ -153,6 +195,8 @@ function ensureListeners() {
if (converted.length > 0) {
session.messages = [...session.messages, ...converted];
persistMessages(session);
// Reset stall timer on activity
resetStallTimer(payload.sessionId, session.projectId);
}
});
@ -171,6 +215,15 @@ function ensureListeners() {
// Persist on every status change
persistSession(session);
// Emit toast notification on completion
if (session.status === 'done') {
clearStallTimer(payload.sessionId);
emitToast(`Agent completed on ${session.projectId}`, 'success');
} else if (session.status === 'error') {
clearStallTimer(payload.sessionId);
emitToast(`Agent error on ${session.projectId}: ${payload.error ?? 'unknown'}`, 'error');
}
// Schedule cleanup after done/error (Fix #2)
if (session.status === 'done' || session.status === 'error') {
// Flush any pending message persistence immediately
@ -366,6 +419,8 @@ export async function startAgent(
// Read settings defaults if not explicitly provided (Fix #5)
let permissionMode = options.permissionMode;
let systemPrompt = options.systemPrompt;
let defaultModel = options.model;
let cwd = options.cwd;
try {
const { settings } = await appRpc.request['settings.getAll']({});
if (!permissionMode && settings['permission_mode']) {
@ -374,6 +429,17 @@ export async function startAgent(
if (!systemPrompt && settings['system_prompt_template']) {
systemPrompt = settings['system_prompt_template'];
}
if (!cwd && settings['default_cwd']) {
cwd = settings['default_cwd'];
}
// Read default model from provider_settings if not specified
if (!defaultModel && settings['provider_settings']) {
try {
const providerSettings = JSON.parse(settings['provider_settings']);
const provConfig = providerSettings[provider];
if (provConfig?.defaultModel) defaultModel = provConfig.defaultModel;
} catch { /* ignore parse errors */ }
}
} catch { /* use provided or defaults */ }
// Create reactive session state
@ -391,17 +457,18 @@ export async function startAgent(
costUsd: 0,
inputTokens: 0,
outputTokens: 0,
model: options.model ?? 'claude-opus-4-5',
model: defaultModel ?? 'claude-opus-4-5',
};
projectSessionMap.set(projectId, sessionId);
resetStallTimer(sessionId, projectId);
const result = await appRpc.request['agent.start']({
sessionId,
provider: provider as 'claude' | 'codex' | 'ollama',
prompt,
cwd: options.cwd,
model: options.model,
cwd,
model: defaultModel,
systemPrompt: systemPrompt,
maxTurns: options.maxTurns,
permissionMode: permissionMode,