Files
boocode/apps/web/src/components/StatusDot.tsx
indifferentketchup 48ee63a286 v1.12.1: rich status indicator + server-side workspace pane sync
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>
2026-05-21 20:32:02 +00:00

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)}
/>
);
}