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:
parent
29a3370e79
commit
252fca70df
22 changed files with 8116 additions and 227 deletions
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue