feat(v2): add sidecar crash detection, restart UI, and auto-scroll lock

Phase 3 polish: dispatcher listens for sidecar-exited events and marks
running sessions as error. AgentPane shows "Restart Sidecar" button on
error. Auto-scroll disables when user scrolls >50px from bottom with
"Scroll to bottom" button. Added agent_restart Tauri command.
This commit is contained in:
Hibryda 2026-03-06 12:19:35 +01:00
parent da6d7272ee
commit 5ca035d438
4 changed files with 104 additions and 11 deletions

View file

@ -1,7 +1,7 @@
// Agent Dispatcher — connects sidecar bridge events to agent store
// Single listener that routes sidecar messages to the correct agent session
import { onSidecarMessage, type SidecarMessage } from './adapters/agent-bridge';
import { onSidecarMessage, onSidecarExited, type SidecarMessage } from './adapters/agent-bridge';
import { adaptSDKMessage } from './adapters/sdk-messages';
import type { InitContent, CostContent } from './adapters/sdk-messages';
import {
@ -10,14 +10,29 @@ import {
setAgentModel,
appendAgentMessages,
updateAgentCost,
getAgentSessions,
} from './stores/agents.svelte';
let unlistenFn: (() => void) | null = null;
let unlistenMsg: (() => void) | null = null;
let unlistenExit: (() => void) | null = null;
// Sidecar liveness — checked by UI components
let sidecarAlive = true;
export function isSidecarAlive(): boolean {
return sidecarAlive;
}
export function setSidecarAlive(alive: boolean): void {
sidecarAlive = alive;
}
export async function startAgentDispatcher(): Promise<void> {
if (unlistenFn) return;
if (unlistenMsg) return;
sidecarAlive = true;
unlistenMsg = await onSidecarMessage((msg: SidecarMessage) => {
sidecarAlive = true;
unlistenFn = await onSidecarMessage((msg: SidecarMessage) => {
const sessionId = msg.sessionId;
if (!sessionId) return;
@ -39,10 +54,19 @@ export async function startAgentDispatcher(): Promise<void> {
break;
case 'agent_log':
// Debug logging — could route to a log panel later
break;
}
});
unlistenExit = await onSidecarExited(() => {
sidecarAlive = false;
// Mark all running sessions as errored
for (const session of getAgentSessions()) {
if (session.status === 'running' || session.status === 'starting') {
updateAgentStatus(session.id, 'error', 'Sidecar crashed');
}
}
});
}
function handleAgentEvent(sessionId: string, event: Record<string, unknown>): void {
@ -76,15 +100,18 @@ function handleAgentEvent(sessionId: string, event: Record<string, unknown>): vo
}
}
// Append all messages to the session history
if (messages.length > 0) {
appendAgentMessages(sessionId, messages);
}
}
export function stopAgentDispatcher(): void {
if (unlistenFn) {
unlistenFn();
unlistenFn = null;
if (unlistenMsg) {
unlistenMsg();
unlistenMsg = null;
}
if (unlistenExit) {
unlistenExit();
unlistenExit = null;
}
}