feat: isolate Claude and Codex session sources

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
This commit is contained in:
huakson
2026-03-24 08:43:56 -03:00
parent e2a230286d
commit 2b60dd2932
46 changed files with 3665 additions and 607 deletions
+56 -18
View File
@@ -1,5 +1,5 @@
import { useState, useEffect, useRef } from 'react';
import { Observation, Summary, UserPrompt, StreamEvent } from '../types';
import { Observation, Summary, UserPrompt, StreamEvent, ProjectCatalog } from '../types';
import { API_ENDPOINTS } from '../constants/api';
import { TIMING } from '../constants/timing';
@@ -7,16 +7,42 @@ export function useSSE() {
const [observations, setObservations] = useState<Observation[]>([]);
const [summaries, setSummaries] = useState<Summary[]>([]);
const [prompts, setPrompts] = useState<UserPrompt[]>([]);
const [projects, setProjects] = useState<string[]>([]);
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 = () => {
// Clean up existing connection
if (eventSourceRef.current) {
eventSourceRef.current.close();
}
@@ -27,7 +53,6 @@ export function useSSE() {
eventSource.onopen = () => {
console.log('[SSE] Connected');
setIsConnected(true);
// Clear any pending reconnect
if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current);
}
@@ -38,9 +63,8 @@ export function useSSE() {
setIsConnected(false);
eventSource.close();
// Reconnect after delay
reconnectTimeoutRef.current = setTimeout(() => {
reconnectTimeoutRef.current = undefined; // Clear before reconnecting
reconnectTimeoutRef.current = undefined;
console.log('[SSE] Attempting to reconnect...');
connect();
}, TIMING.SSE_RECONNECT_DELAY_MS);
@@ -52,32 +76,37 @@ export function useSSE() {
switch (data.type) {
case 'initial_load':
console.log('[SSE] Initial load:', {
projects: data.projects?.length || 0
projects: data.projects?.length || 0,
sources: data.sources?.length || 0
});
setCatalog({
projects: data.projects || [],
sources: data.sources || [],
projectsBySource: data.projectsBySource || {}
});
// Only load projects list - data will come via pagination
setProjects(data.projects || []);
break;
case 'new_observation':
if (data.observation) {
console.log('[SSE] New observation:', data.observation.id);
setObservations(prev => [data.observation, ...prev]);
updateCatalogForItem(data.observation.project, data.observation.platform_source || 'claude');
setObservations(prev => [data.observation!, ...prev]);
}
break;
case 'new_summary':
if (data.summary) {
const summary = data.summary;
console.log('[SSE] New summary:', summary.id);
setSummaries(prev => [summary, ...prev]);
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) {
const prompt = data.prompt;
console.log('[SSE] New prompt:', prompt.id);
setPrompts(prev => [prompt, ...prev]);
console.log('[SSE] New prompt:', data.prompt.id);
updateCatalogForItem(data.prompt.project, data.prompt.platform_source || 'claude');
setPrompts(prev => [data.prompt!, ...prev]);
}
break;
@@ -94,7 +123,6 @@ export function useSSE() {
connect();
// Cleanup on unmount
return () => {
if (eventSourceRef.current) {
eventSourceRef.current.close();
@@ -105,5 +133,15 @@ export function useSSE() {
};
}, []);
return { observations, summaries, prompts, projects, isProcessing, queueDepth, isConnected };
return {
observations,
summaries,
prompts,
projects: catalog.projects,
sources: catalog.sources,
projectsBySource: catalog.projectsBySource,
isProcessing,
queueDepth,
isConnected
};
}