fix(electrobun): address all 22 Codex review #2 findings

CRITICAL:
- DocsTab XSS: DOMPurify sanitization on all {@html} output
- File RPC path traversal: guardPath() validates against project CWDs

HIGH:
- SSH injection: spawn /usr/bin/ssh via PTY args, no shell string
- Search XSS: strip HTML, highlight matches client-side with <mark>
- Terminal listener leak: cleanup functions stored + called in onDestroy
- FileBrowser race: request token, discard stale responses
- SearchOverlay race: same request token pattern
- App startup ordering: groups.list chains into active_group restore
- PtyClient timeout: 5-second auth timeout on connect()
- Rule 55: 6 {#if} patterns converted to style:display toggle

MEDIUM:
- Agent persistence: only persist NEW messages (lastPersistedIndex)
- Search errors: typed error response, "Invalid query" UI
- Health store wired: agent events call recordActivity/setProjectStatus
- index.ts SRP: split into 8 domain handler modules (298 lines)
- App.svelte: extracted workspace-store.svelte.ts
- rpc.ts: typed AppRpcHandle, removed `any`

LOW:
- CommandPalette listener wired in App.svelte
- Dead code removed (removeGroup, onDragStart, plugin loaded)
This commit is contained in:
Hibryda 2026-03-22 02:30:09 +01:00
parent 8e756d3523
commit 1cd4558740
28 changed files with 1342 additions and 1164 deletions

View file

@ -6,6 +6,7 @@
*/
import { appRpc } from './rpc.ts';
import { recordActivity, recordToolDone, recordTokenSnapshot, setProjectStatus } from './health-store.svelte.ts';
// ── Types ────────────────────────────────────────────────────────────────────
@ -118,6 +119,8 @@ const cleanupTimers = new Map<string, ReturnType<typeof setTimeout>>();
// Debounce timer for message persistence
const msgPersistTimers = new Map<string, ReturnType<typeof setTimeout>>();
// Fix #12: Track last persisted index per session to avoid re-saving entire history
const lastPersistedIndex = new Map<string, number>();
// ── Session persistence helpers ─────────────────────────────────────────────
@ -146,7 +149,11 @@ function persistMessages(session: AgentSession): void {
const timer = setTimeout(() => {
msgPersistTimers.delete(session.sessionId);
const msgs = session.messages.map((m) => ({
// Fix #12: Only persist NEW messages (from lastPersistedIndex onward)
const startIdx = lastPersistedIndex.get(session.sessionId) ?? 0;
const newMsgs = session.messages.slice(startIdx);
if (newMsgs.length === 0) return;
const msgs = newMsgs.map((m) => ({
sessionId: session.sessionId,
msgId: m.id,
role: m.role,
@ -155,8 +162,9 @@ function persistMessages(session: AgentSession): void {
toolInput: m.toolInput,
timestamp: m.timestamp,
}));
if (msgs.length === 0) return;
appRpc.request['session.messages.save']({ messages: msgs }).catch((err: unknown) => {
appRpc.request['session.messages.save']({ messages: msgs }).then(() => {
lastPersistedIndex.set(session.sessionId, session.messages.length);
}).catch((err: unknown) => {
console.error('[session.messages.save] persist error:', err);
});
}, 2000);
@ -197,6 +205,16 @@ function ensureListeners() {
persistMessages(session);
// Reset stall timer on activity
resetStallTimer(payload.sessionId, session.projectId);
// Fix #14: Wire health store — record activity on every message batch
for (const msg of converted) {
if (msg.role === 'tool-call') {
recordActivity(session.projectId, msg.toolName);
} else if (msg.role === 'tool-result') {
recordToolDone(session.projectId);
} else {
recordActivity(session.projectId);
}
}
}
});
@ -212,6 +230,9 @@ function ensureListeners() {
session.status = normalizeStatus(payload.status);
if (payload.error) session.error = payload.error;
// Fix #14: Wire health store — update project status
setProjectStatus(session.projectId, session.status === 'done' ? 'done' : session.status === 'error' ? 'error' : session.status === 'running' ? 'running' : 'idle');
// Persist on every status change
persistSession(session);
@ -250,6 +271,8 @@ function ensureListeners() {
session.costUsd = payload.costUsd;
session.inputTokens = payload.inputTokens;
session.outputTokens = payload.outputTokens;
// Fix #14: Wire health store — record token/cost snapshot
recordTokenSnapshot(session.projectId, payload.inputTokens + payload.outputTokens, payload.costUsd);
});
}