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:
parent
da6d7272ee
commit
5ca035d438
4 changed files with 104 additions and 11 deletions
|
|
@ -159,6 +159,12 @@ impl SidecarManager {
|
|||
self.send_message(&msg)
|
||||
}
|
||||
|
||||
pub fn restart(&self, app: &AppHandle) -> Result<(), String> {
|
||||
log::info!("Restarting sidecar");
|
||||
let _ = self.shutdown();
|
||||
self.start(app)
|
||||
}
|
||||
|
||||
pub fn shutdown(&self) -> Result<(), String> {
|
||||
let mut child_lock = self.child.lock().unwrap();
|
||||
if let Some(ref mut child) = *child_lock {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,10 @@ export async function isAgentReady(): Promise<boolean> {
|
|||
return invoke<boolean>('agent_ready');
|
||||
}
|
||||
|
||||
export async function restartAgent(): Promise<void> {
|
||||
return invoke('agent_restart');
|
||||
}
|
||||
|
||||
export interface SidecarMessage {
|
||||
type: string;
|
||||
sessionId?: string;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { queryAgent, stopAgent, isAgentReady } from '../../adapters/agent-bridge';
|
||||
import { queryAgent, stopAgent, isAgentReady, restartAgent } from '../../adapters/agent-bridge';
|
||||
import {
|
||||
getAgentSession,
|
||||
createAgentSession,
|
||||
removeAgentSession,
|
||||
type AgentSession,
|
||||
} from '../../stores/agents.svelte';
|
||||
import { isSidecarAlive, setSidecarAlive } from '../../agent-dispatcher';
|
||||
import type {
|
||||
AgentMessage,
|
||||
TextContent,
|
||||
|
|
@ -30,6 +31,7 @@
|
|||
let inputPrompt = $state(initialPrompt);
|
||||
let scrollContainer: HTMLDivElement | undefined = $state();
|
||||
let autoScroll = $state(true);
|
||||
let restarting = $state(false);
|
||||
|
||||
onMount(async () => {
|
||||
if (initialPrompt) {
|
||||
|
|
@ -73,12 +75,31 @@
|
|||
stopAgent(sessionId).catch(() => {});
|
||||
}
|
||||
|
||||
async function handleRestart() {
|
||||
restarting = true;
|
||||
try {
|
||||
await restartAgent();
|
||||
setSidecarAlive(true);
|
||||
} catch {
|
||||
// Still dead
|
||||
} finally {
|
||||
restarting = false;
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToBottom() {
|
||||
if (autoScroll && scrollContainer) {
|
||||
scrollContainer.scrollTop = scrollContainer.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
function handleScroll() {
|
||||
if (!scrollContainer) return;
|
||||
const { scrollTop, scrollHeight, clientHeight } = scrollContainer;
|
||||
// Lock auto-scroll if user scrolled up more than 50px from bottom
|
||||
autoScroll = scrollHeight - scrollTop - clientHeight < 50;
|
||||
}
|
||||
|
||||
// Auto-scroll when new messages arrive
|
||||
$effect(() => {
|
||||
if (session?.messages.length) {
|
||||
|
|
@ -121,7 +142,7 @@
|
|||
</form>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="messages" bind:this={scrollContainer}>
|
||||
<div class="messages" bind:this={scrollContainer} onscroll={handleScroll}>
|
||||
{#each session.messages as msg (msg.id)}
|
||||
<div class="message msg-{msg.type}">
|
||||
{#if msg.type === 'init'}
|
||||
|
|
@ -173,6 +194,9 @@
|
|||
<div class="running-indicator">
|
||||
<span class="pulse"></span>
|
||||
<span>Running...</span>
|
||||
{#if !autoScroll}
|
||||
<button class="scroll-btn" onclick={() => { autoScroll = true; scrollToBottom(); }}>Scroll to bottom</button>
|
||||
{/if}
|
||||
<button class="stop-btn" onclick={handleStop}>Stop</button>
|
||||
</div>
|
||||
{:else if session.status === 'done'}
|
||||
|
|
@ -184,6 +208,11 @@
|
|||
{:else if session.status === 'error'}
|
||||
<div class="error-bar">
|
||||
<span>Error: {session.error ?? 'Unknown'}</span>
|
||||
{#if session.error?.includes('Sidecar') || session.error?.includes('crashed')}
|
||||
<button class="restart-btn" onclick={handleRestart} disabled={restarting}>
|
||||
{restarting ? 'Restarting...' : 'Restart Sidecar'}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
@ -407,11 +436,38 @@
|
|||
|
||||
.stop-btn:hover { opacity: 0.9; }
|
||||
|
||||
.scroll-btn {
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-secondary);
|
||||
border-radius: 3px;
|
||||
padding: 2px 8px;
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.scroll-btn:hover { color: var(--text-primary); }
|
||||
|
||||
.restart-btn {
|
||||
margin-left: auto;
|
||||
background: var(--ctp-peach);
|
||||
color: var(--ctp-crust);
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
padding: 2px 10px;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.restart-btn:hover { opacity: 0.9; }
|
||||
.restart-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||
|
||||
.done-bar, .error-bar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
font-size: 11px;
|
||||
font-family: var(--font-mono);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.done-bar { color: var(--ctp-green); }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue