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
+66 -7
View File
@@ -1,5 +1,5 @@
import { useState, useEffect, useCallback } from 'react';
import type { Settings } from '../types';
import type { ProjectCatalog, Settings } from '../types';
interface UseContextPreviewResult {
preview: string;
@@ -7,15 +7,31 @@ interface UseContextPreviewResult {
error: string | null;
refresh: () => Promise<void>;
projects: string[];
sources: string[];
selectedSource: string | null;
setSelectedSource: (source: string) => void;
selectedProject: string | null;
setSelectedProject: (project: string) => void;
}
function getPreferredSource(sources: string[]): string | null {
if (sources.includes('claude')) return 'claude';
if (sources.includes('codex')) return 'codex';
return sources[0] || null;
}
function withDefaultSources(sources: string[]): string[] {
const merged = ['claude', 'codex', ...sources];
return Array.from(new Set(merged));
}
export function useContextPreview(settings: Settings): UseContextPreviewResult {
const [preview, setPreview] = useState<string>('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [catalog, setCatalog] = useState<ProjectCatalog>({ projects: [], sources: [], projectsBySource: {} });
const [projects, setProjects] = useState<string[]>([]);
const [selectedSource, setSelectedSource] = useState<string | null>(null);
const [selectedProject, setSelectedProject] = useState<string | null>(null);
// Fetch projects on mount
@@ -23,11 +39,27 @@ export function useContextPreview(settings: Settings): UseContextPreviewResult {
async function fetchProjects() {
try {
const response = await fetch('/api/projects');
const data = await response.json();
if (data.projects && data.projects.length > 0) {
setProjects(data.projects);
setSelectedProject(data.projects[0]); // Default to first project
const data = await response.json() as ProjectCatalog;
const nextCatalog: ProjectCatalog = {
projects: data.projects || [],
sources: withDefaultSources(data.sources || []),
projectsBySource: data.projectsBySource || {}
};
setCatalog(nextCatalog);
const preferredSource = getPreferredSource(nextCatalog.sources);
setSelectedSource(preferredSource);
if (preferredSource) {
const sourceProjects = nextCatalog.projectsBySource[preferredSource] || [];
setProjects(sourceProjects);
setSelectedProject(sourceProjects[0] || null);
return;
}
setProjects(nextCatalog.projects);
setSelectedProject(nextCatalog.projects[0] || null);
} catch (err) {
console.error('Failed to fetch projects:', err);
}
@@ -35,6 +67,18 @@ export function useContextPreview(settings: Settings): UseContextPreviewResult {
fetchProjects();
}, []);
useEffect(() => {
if (!selectedSource) {
setProjects(catalog.projects);
setSelectedProject(prev => (prev && catalog.projects.includes(prev) ? prev : catalog.projects[0] || null));
return;
}
const sourceProjects = catalog.projectsBySource[selectedSource] || [];
setProjects(sourceProjects);
setSelectedProject(prev => (prev && sourceProjects.includes(prev) ? prev : sourceProjects[0] || null));
}, [catalog, selectedSource]);
const refresh = useCallback(async () => {
if (!selectedProject) {
setPreview('No project selected');
@@ -48,6 +92,10 @@ export function useContextPreview(settings: Settings): UseContextPreviewResult {
project: selectedProject
});
if (selectedSource) {
params.append('platformSource', selectedSource);
}
const response = await fetch(`/api/context/preview?${params}`);
const text = await response.text();
@@ -58,7 +106,7 @@ export function useContextPreview(settings: Settings): UseContextPreviewResult {
}
setIsLoading(false);
}, [selectedProject]);
}, [selectedProject, selectedSource]);
// Debounced refresh when settings or selectedProject change
useEffect(() => {
@@ -68,5 +116,16 @@ export function useContextPreview(settings: Settings): UseContextPreviewResult {
return () => clearTimeout(timeout);
}, [settings, refresh]);
return { preview, isLoading, error, refresh, projects, selectedProject, setSelectedProject };
return {
preview,
isLoading,
error,
refresh,
projects,
sources: catalog.sources,
selectedSource,
setSelectedSource,
selectedProject,
setSelectedProject
};
}