Status indicator (StatusDot): drops the flat amber pulse for a richer set of states — orbiting amber for streaming, spinning sky ring for tool_running, static violet for waiting_for_input, plus the existing idle/error. Backend chat_status frame widens from 'working|idle|error' to discriminate streaming vs tool execution vs paused for user input. Workspace pane sync: pane layout moves from per-device localStorage to server-side sessions.workspace_panes jsonb. PATCH /api/sessions/:id/workspace broadcasts session_workspace_updated on the user channel for cross-device live sync. Echo dedup via JSON comparison so the round-trip frame doesn't loop. Legacy localStorage seeds the server on first hydrate, then is deleted. Deprecated session_panes table dropped. Resilience: startup sweep marks any stale 'streaming' message older than 5 minutes as 'failed' so v1.12.0-style hung rows clear on container restart. useWorkspacePanes gains validatePanes() to prune dead chatId references from saved pane state when the chat list lands. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
75 lines
2.0 KiB
TypeScript
75 lines
2.0 KiB
TypeScript
import { useChatStatus, type DerivedStatus } from '@/hooks/useChatStatus';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
interface Props {
|
|
chatId: string | null | undefined;
|
|
className?: string;
|
|
}
|
|
|
|
const STATUS_LABEL: Record<DerivedStatus, string> = {
|
|
streaming: 'streaming',
|
|
tool_running: 'running tool',
|
|
waiting_for_input: 'waiting for input',
|
|
idle_warm: 'idle',
|
|
idle_cold: 'idle',
|
|
error: 'error',
|
|
};
|
|
|
|
export function StatusDot({ chatId, className }: Props) {
|
|
const status = useChatStatus(chatId);
|
|
|
|
if (status === 'streaming') {
|
|
return (
|
|
<span
|
|
aria-label="Status: streaming"
|
|
title="streaming"
|
|
className={cn('inline-block relative w-3 h-3 shrink-0', className)}
|
|
>
|
|
<span className="absolute inset-0 animate-spin-slow">
|
|
<span className="absolute top-0 left-1/2 -translate-x-1/2 w-1 h-1 rounded-full bg-amber-500" />
|
|
<span className="absolute bottom-0 left-1/2 -translate-x-1/2 w-1 h-1 rounded-full bg-amber-500/60" />
|
|
</span>
|
|
</span>
|
|
);
|
|
}
|
|
|
|
if (status === 'tool_running') {
|
|
return (
|
|
<span
|
|
aria-label="Status: running tool"
|
|
title="running tool"
|
|
className={cn(
|
|
'inline-block w-3 h-3 rounded-full border-2 border-sky-500 border-t-transparent animate-spin shrink-0',
|
|
className,
|
|
)}
|
|
/>
|
|
);
|
|
}
|
|
|
|
if (status === 'waiting_for_input') {
|
|
return (
|
|
<span
|
|
aria-label="Status: waiting for input"
|
|
title="waiting for input"
|
|
className={cn(
|
|
'inline-block w-1.5 h-1.5 rounded-full shrink-0 bg-violet-500',
|
|
className,
|
|
)}
|
|
/>
|
|
);
|
|
}
|
|
|
|
const bg =
|
|
status === 'idle_warm' ? 'bg-emerald-500'
|
|
: status === 'error' ? 'bg-destructive'
|
|
: 'bg-muted-foreground/40';
|
|
|
|
return (
|
|
<span
|
|
aria-label={`Status: ${STATUS_LABEL[status]}`}
|
|
title={STATUS_LABEL[status]}
|
|
className={cn('inline-block w-1.5 h-1.5 rounded-full shrink-0', bg, className)}
|
|
/>
|
|
);
|
|
}
|