feat(electrobun): file management — CodeMirror editor, PDF viewer, CSV table, real file I/O

- CodeEditor: CodeMirror 6 with Catppuccin theme, 15+ languages, Ctrl+S save,
  dirty tracking, save-on-blur
- PdfViewer: pdfjs-dist canvas rendering, zoom 0.5-3x, HiDPI, lazy page load
- CsvTable: RFC 4180 parser, delimiter auto-detect, sortable columns, sticky header
- FileBrowser: real filesystem via files.list/read/write RPC, lazy dir loading,
  file type routing (code→editor, pdf→viewer, csv→table, images→display)
- 10MB size gate, binary detection, base64 encoding for non-text files
This commit is contained in:
Hibryda 2026-03-22 01:36:02 +01:00
parent 29a3370e79
commit 252fca70df
22 changed files with 8116 additions and 227 deletions

View file

@ -74,6 +74,54 @@ let sessions = $state<Record<string, AgentSession>>({});
// Grace period timers for cleanup after done/error
const cleanupTimers = new Map<string, ReturnType<typeof setTimeout>>();
// Debounce timer for message persistence
const msgPersistTimers = new Map<string, ReturnType<typeof setTimeout>>();
// ── Session persistence helpers ─────────────────────────────────────────────
function persistSession(session: AgentSession): void {
appRpc.request['session.save']({
projectId: session.projectId,
sessionId: session.sessionId,
provider: session.provider,
status: session.status,
costUsd: session.costUsd,
inputTokens: session.inputTokens,
outputTokens: session.outputTokens,
model: session.model,
error: session.error,
createdAt: session.messages[0]?.timestamp ?? Date.now(),
updatedAt: Date.now(),
}).catch((err: unknown) => {
console.error('[session.save] persist error:', err);
});
}
function persistMessages(session: AgentSession): void {
// Debounce: batch message saves every 2 seconds
const existing = msgPersistTimers.get(session.sessionId);
if (existing) clearTimeout(existing);
const timer = setTimeout(() => {
msgPersistTimers.delete(session.sessionId);
const msgs = session.messages.map((m) => ({
sessionId: session.sessionId,
msgId: m.id,
role: m.role,
content: m.content,
toolName: m.toolName,
toolInput: m.toolInput,
timestamp: m.timestamp,
}));
if (msgs.length === 0) return;
appRpc.request['session.messages.save']({ messages: msgs }).catch((err: unknown) => {
console.error('[session.messages.save] persist error:', err);
});
}, 2000);
msgPersistTimers.set(session.sessionId, timer);
}
// ── RPC event listeners (registered once) ────────────────────────────────────
let listenersRegistered = false;
@ -104,6 +152,7 @@ function ensureListeners() {
if (converted.length > 0) {
session.messages = [...session.messages, ...converted];
persistMessages(session);
}
});
@ -119,8 +168,18 @@ function ensureListeners() {
session.status = normalizeStatus(payload.status);
if (payload.error) session.error = payload.error;
// Persist on every status change
persistSession(session);
// Schedule cleanup after done/error (Fix #2)
if (session.status === 'done' || session.status === 'error') {
// Flush any pending message persistence immediately
const pendingTimer = msgPersistTimers.get(session.sessionId);
if (pendingTimer) {
clearTimeout(pendingTimer);
msgPersistTimers.delete(session.sessionId);
}
persistMessages(session);
scheduleCleanup(session.sessionId, session.projectId);
}
});
@ -427,5 +486,57 @@ export function clearSession(projectId: string): void {
}
}
/**
* Load the last session for a project from SQLite (for restart recovery).
* Restores session state + messages into the reactive store.
* Only restores done/error sessions (running sessions are gone after restart).
*/
export async function loadLastSession(projectId: string): Promise<boolean> {
ensureListeners();
try {
const { session } = await appRpc.request['session.load']({ projectId });
if (!session) return false;
// Only restore completed sessions (running sessions can't be resumed)
if (session.status !== 'done' && session.status !== 'error') return false;
// Load messages for this session
const { messages: storedMsgs } = await appRpc.request['session.messages.load']({
sessionId: session.sessionId,
});
const restoredMessages: AgentMessage[] = storedMsgs.map((m: {
msgId: string; role: string; content: string;
toolName?: string; toolInput?: string; timestamp: number;
}) => ({
id: m.msgId,
role: m.role as MsgRole,
content: m.content,
toolName: m.toolName,
toolInput: m.toolInput,
timestamp: m.timestamp,
}));
sessions[session.sessionId] = {
sessionId: session.sessionId,
projectId: session.projectId,
provider: session.provider,
status: normalizeStatus(session.status),
messages: restoredMessages,
costUsd: session.costUsd,
inputTokens: session.inputTokens,
outputTokens: session.outputTokens,
model: session.model,
error: session.error,
};
projectSessionMap.set(projectId, session.sessionId);
return true;
} catch (err) {
console.error('[loadLastSession] error:', err);
return false;
}
}
/** Initialize listeners on module load. */
ensureListeners();