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>
189 lines
4.4 KiB
TypeScript
189 lines
4.4 KiB
TypeScript
// Tiny in-app event bus for session metadata changes that need to propagate
|
|
// across hooks (e.g. AI rename arriving via WS in the session view needs to
|
|
// also refresh the sidebar's session list).
|
|
|
|
import type { Chat, ErrorReason, Project, Session } from '@/api/types';
|
|
import type { Attachment } from '@/lib/attachments';
|
|
|
|
export interface SessionRenamedEvent {
|
|
type: 'session_renamed';
|
|
session_id: string;
|
|
name: string;
|
|
}
|
|
|
|
export interface ProjectCreatedEvent {
|
|
type: 'project_created';
|
|
project: Project;
|
|
}
|
|
|
|
export interface ProjectDeletedEvent {
|
|
type: 'project_deleted';
|
|
project_id: string;
|
|
}
|
|
|
|
export interface SessionCreatedEvent {
|
|
type: 'session_created';
|
|
session: Session;
|
|
project_id: string;
|
|
}
|
|
|
|
export interface SessionDeletedEvent {
|
|
type: 'session_deleted';
|
|
session_id: string;
|
|
project_id: string;
|
|
}
|
|
|
|
export interface SessionUpdatedEvent {
|
|
type: 'session_updated';
|
|
session_id: string;
|
|
project_id: string;
|
|
name: string;
|
|
updated_at: string;
|
|
}
|
|
|
|
export interface SessionWorkspaceUpdatedEvent {
|
|
type: 'session_workspace_updated';
|
|
session_id: string;
|
|
workspace_panes: import('@/api/types').WorkspacePane[];
|
|
}
|
|
|
|
export interface SessionLoadedEvent {
|
|
type: 'session_loaded';
|
|
session_id: string;
|
|
project_id: string;
|
|
}
|
|
|
|
export interface OpenFileInBrowserEvent {
|
|
type: 'open_file_in_browser';
|
|
path: string; // project-relative
|
|
}
|
|
|
|
export interface AttachChatFileEvent {
|
|
type: 'attach_chat_file';
|
|
attachment: Omit<Attachment, 'id'>;
|
|
}
|
|
|
|
export interface OpenChatInActivePaneEvent {
|
|
type: 'open_chat_in_active_pane';
|
|
chat_id: string;
|
|
}
|
|
|
|
// Client-side event fired by the sidebar Settings button when a session is
|
|
// currently mounted. Session.tsx subscribes and calls
|
|
// panesHook.toggleSettingsPane() (open on first click, close on second).
|
|
// Sidebar handles the no-session case by navigating to /settings directly.
|
|
export interface OpenSettingsPaneEvent {
|
|
type: 'open_settings_pane';
|
|
}
|
|
|
|
export interface SessionArchivedEvent {
|
|
type: 'session_archived';
|
|
session_id: string;
|
|
project_id: string;
|
|
}
|
|
|
|
export interface ChatCreatedEvent {
|
|
type: 'chat_created';
|
|
chat: Chat;
|
|
session_id: string;
|
|
}
|
|
|
|
export interface ChatUpdatedEvent {
|
|
type: 'chat_updated';
|
|
chat_id: string;
|
|
session_id: string;
|
|
name: string | null;
|
|
updated_at: string;
|
|
}
|
|
|
|
export interface ChatArchivedEvent {
|
|
type: 'chat_archived';
|
|
chat_id: string;
|
|
session_id: string;
|
|
}
|
|
|
|
export interface ChatUnarchivedEvent {
|
|
type: 'chat_unarchived';
|
|
chat: Chat;
|
|
}
|
|
|
|
export interface ChatDeletedEvent {
|
|
type: 'chat_deleted';
|
|
chat_id: string;
|
|
session_id: string;
|
|
}
|
|
|
|
export interface ProjectArchivedEvent {
|
|
type: 'project_archived';
|
|
project_id: string;
|
|
}
|
|
|
|
export interface ProjectUnarchivedEvent {
|
|
type: 'project_unarchived';
|
|
project: Project;
|
|
}
|
|
|
|
export interface ProjectUpdatedEvent {
|
|
type: 'project_updated';
|
|
project_id: string;
|
|
name: string;
|
|
}
|
|
|
|
// v1.8 mobile-tabs: broadcast on user channel from inference.ts so any device
|
|
// subscribed sees a chat working/idle/error. Frontend stores per-chat; panes
|
|
// derive their dot from pane.activeChatId.
|
|
// v1.8.2: optional `reason` carries a machine-readable code when status is
|
|
// 'error'. UI prefers reason for inline error rendering.
|
|
export interface ChatStatusEvent {
|
|
type: 'chat_status';
|
|
chat_id: string;
|
|
status: 'streaming' | 'tool_running' | 'waiting_for_input' | 'idle' | 'error';
|
|
at: string;
|
|
reason?: ErrorReason;
|
|
}
|
|
|
|
export type SessionEvent =
|
|
| SessionRenamedEvent
|
|
| ProjectCreatedEvent
|
|
| ProjectDeletedEvent
|
|
| SessionCreatedEvent
|
|
| SessionDeletedEvent
|
|
| SessionUpdatedEvent
|
|
| SessionWorkspaceUpdatedEvent
|
|
| SessionLoadedEvent
|
|
| OpenFileInBrowserEvent
|
|
| AttachChatFileEvent
|
|
| OpenChatInActivePaneEvent
|
|
| OpenSettingsPaneEvent
|
|
| SessionArchivedEvent
|
|
| ChatCreatedEvent
|
|
| ChatUpdatedEvent
|
|
| ChatArchivedEvent
|
|
| ChatUnarchivedEvent
|
|
| ChatDeletedEvent
|
|
| ProjectArchivedEvent
|
|
| ProjectUnarchivedEvent
|
|
| ProjectUpdatedEvent
|
|
| ChatStatusEvent;
|
|
type Listener = (event: SessionEvent) => void;
|
|
|
|
const listeners = new Set<Listener>();
|
|
|
|
export const sessionEvents = {
|
|
emit(event: SessionEvent) {
|
|
for (const listener of listeners) {
|
|
try {
|
|
listener(event);
|
|
} catch {
|
|
// swallow — one bad listener shouldn't break others
|
|
}
|
|
}
|
|
},
|
|
subscribe(listener: Listener): () => void {
|
|
listeners.add(listener);
|
|
return () => {
|
|
listeners.delete(listener);
|
|
};
|
|
},
|
|
};
|