Merge branch 'pr-1472' into integration/validation-batch

# Conflicts:
#	plugin/scripts/context-generator.cjs
#	plugin/scripts/mcp-server.cjs
#	plugin/scripts/worker-service.cjs
#	plugin/ui/viewer-bundle.js
#	src/cli/handlers/context.ts
#	src/services/sqlite/SessionStore.ts
#	src/services/sqlite/migrations/runner.ts
#	src/services/worker-service.ts
#	src/shared/SettingsDefaultsManager.ts
This commit is contained in:
Alex Newman
2026-04-06 14:23:18 -07:00
50 changed files with 3852 additions and 683 deletions
+124 -1
View File
@@ -355,6 +355,14 @@
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.03);
}
.header-main {
display: flex;
align-items: center;
gap: 18px;
min-width: 0;
flex-wrap: wrap;
}
.sidebar-header {
padding: 14px 18px;
border-bottom: 1px solid var(--color-border-primary);
@@ -549,6 +557,42 @@
font-size: 13px;
}
.source-tabs {
display: inline-flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
}
.source-tab {
background: transparent;
border: 1px solid var(--color-border-primary);
color: var(--color-text-secondary);
border-radius: 999px;
padding: 6px 12px;
font-size: 12px;
line-height: 1;
font-weight: 600;
letter-spacing: 0.01em;
cursor: pointer;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
white-space: nowrap;
}
.source-tab:hover {
background: var(--color-bg-card-hover);
border-color: var(--color-border-focus);
color: var(--color-text-primary);
transform: translateY(-1px);
}
.source-tab.active {
background: linear-gradient(135deg, var(--color-bg-button) 0%, var(--color-accent-primary) 100%);
border-color: var(--color-bg-button);
color: var(--color-text-button);
box-shadow: 0 2px 8px rgba(9, 105, 218, 0.18);
}
.settings-btn,
.theme-toggle-btn {
background: var(--color-bg-card);
@@ -887,6 +931,49 @@
letter-spacing: 0.5px;
}
.card-source {
padding: 2px 8px;
border-radius: 999px;
font-weight: 600;
font-size: 10px;
letter-spacing: 0.04em;
text-transform: uppercase;
border: 1px solid transparent;
}
.source-claude {
background: rgba(255, 138, 61, 0.12);
color: #c25a00;
border-color: rgba(255, 138, 61, 0.22);
}
.source-codex {
background: rgba(33, 150, 243, 0.12);
color: #0f5ba7;
border-color: rgba(33, 150, 243, 0.24);
}
.source-cursor {
background: rgba(124, 58, 237, 0.12);
color: #6d28d9;
border-color: rgba(124, 58, 237, 0.24);
}
[data-theme="dark"] .source-claude {
color: #ffb067;
border-color: rgba(255, 176, 103, 0.2);
}
[data-theme="dark"] .source-codex {
color: #8fc7ff;
border-color: rgba(143, 199, 255, 0.2);
}
[data-theme="dark"] .source-cursor {
color: #c4b5fd;
border-color: rgba(196, 181, 253, 0.2);
}
.card-title {
font-size: 17px;
margin-bottom: 14px;
@@ -1483,6 +1570,10 @@
padding: 14px 20px;
}
.header-main {
gap: 12px;
}
.status {
gap: 6px;
}
@@ -1491,6 +1582,11 @@
max-width: 160px;
}
.source-tab {
padding: 6px 10px;
font-size: 11px;
}
/* Hide icon links (docs, github, twitter) on tablet */
.icon-link {
display: none;
@@ -1544,6 +1640,28 @@
gap: 8px;
}
.header-main {
gap: 10px;
}
.source-tabs {
width: 100%;
flex-wrap: nowrap;
overflow-x: auto;
padding-bottom: 2px;
scrollbar-width: none;
}
.source-tabs::-webkit-scrollbar {
display: none;
}
.source-tab {
flex-shrink: 0;
padding: 5px 10px;
font-size: 11px;
}
.logomark {
height: 28px;
}
@@ -1732,6 +1850,11 @@
white-space: nowrap;
}
.preview-selector select:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.preview-selector select {
background: var(--color-bg-card);
border: 1px solid var(--color-border-primary);
@@ -2873,4 +2996,4 @@
<script src="viewer-bundle.js"></script>
</body>
</html>
</html>
+42 -21
View File
@@ -13,39 +13,57 @@ import { mergeAndDeduplicateByProject } from './utils/data';
export function App() {
const [currentFilter, setCurrentFilter] = useState('');
const [currentSource, setCurrentSource] = useState('all');
const [contextPreviewOpen, setContextPreviewOpen] = useState(false);
const [logsModalOpen, setLogsModalOpen] = useState(false);
const [paginatedObservations, setPaginatedObservations] = useState<Observation[]>([]);
const [paginatedSummaries, setPaginatedSummaries] = useState<Summary[]>([]);
const [paginatedPrompts, setPaginatedPrompts] = useState<UserPrompt[]>([]);
const { observations, summaries, prompts, projects, isProcessing, queueDepth, isConnected } = useSSE();
const { observations, summaries, prompts, projects, sources, projectsBySource, isProcessing, queueDepth, isConnected } = useSSE();
const { settings, saveSettings, isSaving, saveStatus } = useSettings();
const { stats, refreshStats } = useStats();
const { preference, resolvedTheme, setThemePreference } = useTheme();
const pagination = usePagination(currentFilter);
const pagination = usePagination(currentFilter, currentSource);
const availableProjects = useMemo(() => {
if (currentSource === 'all') {
return projects;
}
return projectsBySource[currentSource] || [];
}, [currentSource, projects, projectsBySource]);
const matchesSelection = useCallback((item: { project: string; platform_source: string }) => {
const matchesProject = !currentFilter || item.project === currentFilter;
const matchesSource = currentSource === 'all' || (item.platform_source || 'claude') === currentSource;
return matchesProject && matchesSource;
}, [currentFilter, currentSource]);
useEffect(() => {
if (currentFilter && !availableProjects.includes(currentFilter)) {
setCurrentFilter('');
}
}, [availableProjects, currentFilter]);
// Merge SSE live data with paginated data, filtering by project when active
const allObservations = useMemo(() => {
const live = currentFilter
? observations.filter(o => o.project === currentFilter)
: observations;
return mergeAndDeduplicateByProject(live, paginatedObservations);
}, [observations, paginatedObservations, currentFilter]);
const live = observations.filter(matchesSelection);
const paginated = paginatedObservations.filter(matchesSelection);
return mergeAndDeduplicateByProject(live, paginated);
}, [observations, paginatedObservations, matchesSelection]);
const allSummaries = useMemo(() => {
const live = currentFilter
? summaries.filter(s => s.project === currentFilter)
: summaries;
return mergeAndDeduplicateByProject(live, paginatedSummaries);
}, [summaries, paginatedSummaries, currentFilter]);
const live = summaries.filter(matchesSelection);
const paginated = paginatedSummaries.filter(matchesSelection);
return mergeAndDeduplicateByProject(live, paginated);
}, [summaries, paginatedSummaries, matchesSelection]);
const allPrompts = useMemo(() => {
const live = currentFilter
? prompts.filter(p => p.project === currentFilter)
: prompts;
return mergeAndDeduplicateByProject(live, paginatedPrompts);
}, [prompts, paginatedPrompts, currentFilter]);
const live = prompts.filter(matchesSelection);
const paginated = paginatedPrompts.filter(matchesSelection);
return mergeAndDeduplicateByProject(live, paginated);
}, [prompts, paginatedPrompts, matchesSelection]);
// Toggle context preview modal
const toggleContextPreview = useCallback(() => {
@@ -78,24 +96,27 @@ export function App() {
} catch (error) {
console.error('Failed to load more data:', error);
}
}, [currentFilter, pagination.observations, pagination.summaries, pagination.prompts]);
}, [pagination.observations, pagination.summaries, pagination.prompts]);
// Reset paginated data and load first page when filter changes
// Reset paginated data and load first page when project/source changes
useEffect(() => {
setPaginatedObservations([]);
setPaginatedSummaries([]);
setPaginatedPrompts([]);
handleLoadMore();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentFilter]);
}, [currentFilter, currentSource]);
return (
<>
<Header
isConnected={isConnected}
projects={projects}
projects={availableProjects}
sources={sources}
currentFilter={currentFilter}
currentSource={currentSource}
onFilterChange={setCurrentFilter}
onSourceChange={setCurrentSource}
isProcessing={isProcessing}
queueDepth={queueDepth}
themePreference={preference}
@@ -136,7 +136,17 @@ export function ContextSettingsModal({
}, [settings]);
// Get context preview based on current form state
const { preview, isLoading, error, projects, selectedProject, setSelectedProject } = useContextPreview(formState);
const {
preview,
isLoading,
error,
projects,
sources,
selectedSource,
setSelectedSource,
selectedProject,
setSelectedProject
} = useContextPreview(formState);
const updateSetting = useCallback((key: keyof Settings, value: string) => {
const newState = { ...formState, [key]: value };
@@ -174,10 +184,23 @@ export function ContextSettingsModal({
<h2>Settings</h2>
<div className="header-controls">
<label className="preview-selector">
Preview for:
Source:
<select
value={selectedSource || ''}
onChange={(e) => setSelectedSource(e.target.value)}
disabled={sources.length === 0}
>
{sources.map(source => (
<option key={source} value={source}>{source}</option>
))}
</select>
</label>
<label className="preview-selector">
Project:
<select
value={selectedProject || ''}
onChange={(e) => setSelectedProject(e.target.value)}
disabled={projects.length === 0}
>
{projects.map(project => (
<option key={project} value={project}>{project}</option>
+44 -10
View File
@@ -7,8 +7,11 @@ import { useSpinningFavicon } from '../hooks/useSpinningFavicon';
interface HeaderProps {
isConnected: boolean;
projects: string[];
sources: string[];
currentFilter: string;
currentSource: string;
onFilterChange: (filter: string) => void;
onSourceChange: (source: string) => void;
isProcessing: boolean;
queueDepth: number;
themePreference: ThemePreference;
@@ -16,11 +19,26 @@ interface HeaderProps {
onContextPreviewToggle: () => void;
}
function formatSourceLabel(source: string): string {
if (source === 'all') return 'All';
if (source === 'claude') return 'Claude';
if (source === 'codex') return 'Codex';
return source.charAt(0).toUpperCase() + source.slice(1);
}
function buildSourceTabs(sources: string[]): string[] {
const merged = ['all', 'claude', 'codex', ...sources];
return Array.from(new Set(merged.filter(Boolean)));
}
export function Header({
isConnected,
projects,
sources,
currentFilter,
currentSource,
onFilterChange,
onSourceChange,
isProcessing,
queueDepth,
themePreference,
@@ -28,20 +46,36 @@ export function Header({
onContextPreviewToggle
}: HeaderProps) {
useSpinningFavicon(isProcessing);
const availableSources = buildSourceTabs(sources);
return (
<div className="header">
<h1>
<div style={{ position: 'relative', display: 'inline-block' }}>
<img src="claude-mem-logomark.webp" alt="" className={`logomark ${isProcessing ? 'spinning' : ''}`} />
{queueDepth > 0 && (
<div className="queue-bubble">
{queueDepth}
</div>
)}
<div className="header-main">
<h1>
<div style={{ position: 'relative', display: 'inline-block' }}>
<img src="claude-mem-logomark.webp" alt="" className={`logomark ${isProcessing ? 'spinning' : ''}`} />
{queueDepth > 0 && (
<div className="queue-bubble">
{queueDepth}
</div>
)}
</div>
<span className="logo-text">claude-mem</span>
</h1>
<div className="source-tabs" role="tablist" aria-label="Context source tabs">
{availableSources.map(source => (
<button
key={source}
type="button"
className={`source-tab ${currentSource === source ? 'active' : ''}`}
onClick={() => onSourceChange(source)}
aria-pressed={currentSource === source}
>
{formatSourceLabel(source)}
</button>
))}
</div>
<span className="logo-text">claude-mem</span>
</h1>
</div>
<div className="status">
<a
href="https://docs.claude-mem.ai"
@@ -52,6 +52,9 @@ export function ObservationCard({ observation }: ObservationCardProps) {
<span className={`card-type type-${observation.type}`}>
{observation.type}
</span>
<span className={`card-source source-${observation.platform_source || 'claude'}`}>
{observation.platform_source || 'claude'}
</span>
<span className="card-project">{observation.project}</span>
</div>
<div className="view-mode-toggles">
+3
View File
@@ -14,6 +14,9 @@ export function PromptCard({ prompt }: PromptCardProps) {
<div className="card-header">
<div className="card-header-left">
<span className="card-type">Prompt</span>
<span className={`card-source source-${prompt.platform_source || 'claude'}`}>
{prompt.platform_source || 'claude'}
</span>
<span className="card-project">{prompt.project}</span>
</div>
</div>
+3
View File
@@ -21,6 +21,9 @@ export function SummaryCard({ summary }: SummaryCardProps) {
<header className="summary-card-header">
<div className="summary-badge-row">
<span className="card-type summary-badge">Session Summary</span>
<span className={`card-source source-${summary.platform_source || 'claude'}`}>
{summary.platform_source || 'claude'}
</span>
<span className="summary-project-badge">{summary.project}</span>
</div>
{summary.request && (
+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
};
}
+23 -10
View File
@@ -14,7 +14,7 @@ type DataItem = Observation | Summary | UserPrompt;
/**
* Generic pagination hook for observations, summaries, and prompts
*/
function usePaginationFor(endpoint: string, dataType: DataType, currentFilter: string) {
function usePaginationFor(endpoint: string, dataType: DataType, currentFilter: string, currentSource: string) {
const [state, setState] = useState<PaginationState>({
isLoading: false,
hasMore: true
@@ -22,7 +22,7 @@ function usePaginationFor(endpoint: string, dataType: DataType, currentFilter: s
// Track offset and filter in refs to handle synchronous resets
const offsetRef = useRef(0);
const lastFilterRef = useRef(currentFilter);
const lastSelectionRef = useRef(`${currentSource}::${currentFilter}`);
const stateRef = useRef(state);
/**
@@ -31,16 +31,17 @@ function usePaginationFor(endpoint: string, dataType: DataType, currentFilter: s
*/
const loadMore = useCallback(async (): Promise<DataItem[]> => {
// Check if filter changed - if so, reset pagination synchronously
const filterChanged = lastFilterRef.current !== currentFilter;
const selectionKey = `${currentSource}::${currentFilter}`;
const filterChanged = lastSelectionRef.current !== selectionKey;
if (filterChanged) {
offsetRef.current = 0;
lastFilterRef.current = currentFilter;
lastSelectionRef.current = selectionKey;
// Reset state both in React state and ref synchronously
const newState = { isLoading: false, hasMore: true };
setState(newState);
stateRef.current = newState; // Update ref immediately to avoid stale checks
stateRef.current = newState; // Update ref immediately to avoid stale checks
}
// Prevent concurrent requests using ref (always current)
@@ -49,6 +50,7 @@ function usePaginationFor(endpoint: string, dataType: DataType, currentFilter: s
return [];
}
stateRef.current = { ...stateRef.current, isLoading: true };
setState(prev => ({ ...prev, isLoading: true }));
// Build query params using current offset from ref
@@ -62,6 +64,10 @@ function usePaginationFor(endpoint: string, dataType: DataType, currentFilter: s
params.append('project', currentFilter);
}
if (currentSource && currentSource !== 'all') {
params.append('platformSource', currentSource);
}
const response = await fetch(`${endpoint}?${params}`);
if (!response.ok) {
@@ -70,6 +76,13 @@ function usePaginationFor(endpoint: string, dataType: DataType, currentFilter: s
const data = await response.json() as { items: DataItem[], hasMore: boolean };
const nextState = {
...stateRef.current,
isLoading: false,
hasMore: data.hasMore
};
stateRef.current = nextState;
setState(prev => ({
...prev,
isLoading: false,
@@ -80,7 +93,7 @@ function usePaginationFor(endpoint: string, dataType: DataType, currentFilter: s
offsetRef.current += UI.PAGINATION_PAGE_SIZE;
return data.items;
}, [currentFilter, endpoint, dataType]);
}, [currentFilter, currentSource, endpoint, dataType]);
return {
...state,
@@ -91,10 +104,10 @@ function usePaginationFor(endpoint: string, dataType: DataType, currentFilter: s
/**
* Hook for paginating observations
*/
export function usePagination(currentFilter: string) {
const observations = usePaginationFor(API_ENDPOINTS.OBSERVATIONS, 'observations', currentFilter);
const summaries = usePaginationFor(API_ENDPOINTS.SUMMARIES, 'summaries', currentFilter);
const prompts = usePaginationFor(API_ENDPOINTS.PROMPTS, 'prompts', currentFilter);
export function usePagination(currentFilter: string, currentSource: string) {
const observations = usePaginationFor(API_ENDPOINTS.OBSERVATIONS, 'observations', currentFilter, currentSource);
const summaries = usePaginationFor(API_ENDPOINTS.SUMMARIES, 'summaries', currentFilter, currentSource);
const prompts = usePaginationFor(API_ENDPOINTS.PROMPTS, 'prompts', currentFilter, currentSource);
return {
observations,
+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
};
}
+12
View File
@@ -2,6 +2,7 @@ export interface Observation {
id: number;
memory_session_id: string;
project: string;
platform_source: string;
type: string;
title: string | null;
subtitle: string | null;
@@ -20,6 +21,7 @@ export interface Summary {
id: number;
session_id: string;
project: string;
platform_source: string;
request?: string;
investigated?: string;
learned?: string;
@@ -32,6 +34,7 @@ export interface UserPrompt {
id: number;
content_session_id: string;
project: string;
platform_source: string;
prompt_number: number;
prompt_text: string;
created_at_epoch: number;
@@ -48,10 +51,19 @@ export interface StreamEvent {
summaries?: Summary[];
prompts?: UserPrompt[];
projects?: string[];
sources?: string[];
projectsBySource?: Record<string, string[]>;
observation?: Observation;
summary?: Summary;
prompt?: UserPrompt;
isProcessing?: boolean;
queueDepth?: number;
}
export interface ProjectCatalog {
projects: string[];
sources: string[];
projectsBySource: Record<string, string[]>;
}
export interface Settings {