2b60dd2932
Persist platform_source across session creation, transcript ingestion, API query paths, and viewer state so Claude and Codex data can coexist without bleeding into each other. - add platform-source normalization helpers and persist platform_source in sdk_sessions via migration 24 with backfill and indexing - thread platformSource through CLI hooks, transcript processing, context generation, pagination, search routes, SSE payloads, and session management - expose source-aware project catalogs, viewer tabs, context preview selectors, and source badges for observations, prompts, and summaries - start the transcript watcher from the worker for transcript-based clients and preserve platform source during Codex ingestion - auto-start the worker from the MCP server for MCP-only clients and tighten stdio-driven cleanup during shutdown - keep createSDKSession backward compatible with existing custom-title callers while allowing explicit platform source forwarding
148 lines
4.7 KiB
TypeScript
148 lines
4.7 KiB
TypeScript
import { useState, useEffect, useRef } from 'react';
|
|
import { Observation, Summary, UserPrompt, StreamEvent, ProjectCatalog } from '../types';
|
|
import { API_ENDPOINTS } from '../constants/api';
|
|
import { TIMING } from '../constants/timing';
|
|
|
|
export function useSSE() {
|
|
const [observations, setObservations] = useState<Observation[]>([]);
|
|
const [summaries, setSummaries] = useState<Summary[]>([]);
|
|
const [prompts, setPrompts] = useState<UserPrompt[]>([]);
|
|
const [catalog, setCatalog] = useState<ProjectCatalog>({
|
|
projects: [],
|
|
sources: [],
|
|
projectsBySource: {}
|
|
});
|
|
const [isConnected, setIsConnected] = useState(false);
|
|
const [isProcessing, setIsProcessing] = useState(false);
|
|
const [queueDepth, setQueueDepth] = useState(0);
|
|
const eventSourceRef = useRef<EventSource | null>(null);
|
|
const reconnectTimeoutRef = useRef<NodeJS.Timeout>();
|
|
|
|
const updateCatalogForItem = (project: string, platformSource: string) => {
|
|
setCatalog(prev => {
|
|
const nextProjects = prev.projects.includes(project)
|
|
? prev.projects
|
|
: [...prev.projects, project];
|
|
const nextSources = prev.sources.includes(platformSource)
|
|
? prev.sources
|
|
: [...prev.sources, platformSource];
|
|
const sourceProjects = prev.projectsBySource[platformSource] || [];
|
|
|
|
return {
|
|
projects: nextProjects,
|
|
sources: nextSources,
|
|
projectsBySource: {
|
|
...prev.projectsBySource,
|
|
[platformSource]: sourceProjects.includes(project)
|
|
? sourceProjects
|
|
: [...sourceProjects, project]
|
|
}
|
|
};
|
|
});
|
|
};
|
|
|
|
useEffect(() => {
|
|
const connect = () => {
|
|
if (eventSourceRef.current) {
|
|
eventSourceRef.current.close();
|
|
}
|
|
|
|
const eventSource = new EventSource(API_ENDPOINTS.STREAM);
|
|
eventSourceRef.current = eventSource;
|
|
|
|
eventSource.onopen = () => {
|
|
console.log('[SSE] Connected');
|
|
setIsConnected(true);
|
|
if (reconnectTimeoutRef.current) {
|
|
clearTimeout(reconnectTimeoutRef.current);
|
|
}
|
|
};
|
|
|
|
eventSource.onerror = (error) => {
|
|
console.error('[SSE] Connection error:', error);
|
|
setIsConnected(false);
|
|
eventSource.close();
|
|
|
|
reconnectTimeoutRef.current = setTimeout(() => {
|
|
reconnectTimeoutRef.current = undefined;
|
|
console.log('[SSE] Attempting to reconnect...');
|
|
connect();
|
|
}, TIMING.SSE_RECONNECT_DELAY_MS);
|
|
};
|
|
|
|
eventSource.onmessage = (event) => {
|
|
const data: StreamEvent = JSON.parse(event.data);
|
|
|
|
switch (data.type) {
|
|
case 'initial_load':
|
|
console.log('[SSE] Initial load:', {
|
|
projects: data.projects?.length || 0,
|
|
sources: data.sources?.length || 0
|
|
});
|
|
setCatalog({
|
|
projects: data.projects || [],
|
|
sources: data.sources || [],
|
|
projectsBySource: data.projectsBySource || {}
|
|
});
|
|
break;
|
|
|
|
case 'new_observation':
|
|
if (data.observation) {
|
|
console.log('[SSE] New observation:', data.observation.id);
|
|
updateCatalogForItem(data.observation.project, data.observation.platform_source || 'claude');
|
|
setObservations(prev => [data.observation!, ...prev]);
|
|
}
|
|
break;
|
|
|
|
case 'new_summary':
|
|
if (data.summary) {
|
|
console.log('[SSE] New summary:', data.summary.id);
|
|
updateCatalogForItem(data.summary.project, data.summary.platform_source || 'claude');
|
|
setSummaries(prev => [data.summary!, ...prev]);
|
|
}
|
|
break;
|
|
|
|
case 'new_prompt':
|
|
if (data.prompt) {
|
|
console.log('[SSE] New prompt:', data.prompt.id);
|
|
updateCatalogForItem(data.prompt.project, data.prompt.platform_source || 'claude');
|
|
setPrompts(prev => [data.prompt!, ...prev]);
|
|
}
|
|
break;
|
|
|
|
case 'processing_status':
|
|
if (typeof data.isProcessing === 'boolean') {
|
|
console.log('[SSE] Processing status:', data.isProcessing, 'Queue depth:', data.queueDepth);
|
|
setIsProcessing(data.isProcessing);
|
|
setQueueDepth(data.queueDepth || 0);
|
|
}
|
|
break;
|
|
}
|
|
};
|
|
};
|
|
|
|
connect();
|
|
|
|
return () => {
|
|
if (eventSourceRef.current) {
|
|
eventSourceRef.current.close();
|
|
}
|
|
if (reconnectTimeoutRef.current) {
|
|
clearTimeout(reconnectTimeoutRef.current);
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
return {
|
|
observations,
|
|
summaries,
|
|
prompts,
|
|
projects: catalog.projects,
|
|
sources: catalog.sources,
|
|
projectsBySource: catalog.projectsBySource,
|
|
isProcessing,
|
|
queueDepth,
|
|
isConnected
|
|
};
|
|
}
|