feat: Add session management and global Claude terminal

Phase 1 - Accurate study status detection:
- Add is_optimization_running() to check for active processes
- Add get_accurate_study_status() with proper status logic
- Status now: not_started, running, paused, completed
- Add "paused" status styling (orange) to Home page

Phase 2 - Global Claude terminal:
- Create ClaudeTerminalContext for app-level state
- Create GlobalClaudeTerminal floating component
- Terminal persists across page navigation
- Shows green indicator when connected
- Remove inline terminal from Dashboard

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Antoine
2025-12-05 12:56:34 -05:00
parent fb2d06236a
commit 5c660ff270
8 changed files with 292 additions and 56 deletions

View File

@@ -1,7 +1,9 @@
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { StudyProvider } from './context/StudyContext';
import { ClaudeTerminalProvider } from './context/ClaudeTerminalContext';
import { MainLayout } from './components/layout/MainLayout';
import { GlobalClaudeTerminal } from './components/GlobalClaudeTerminal';
import Home from './pages/Home';
import Dashboard from './pages/Dashboard';
import Results from './pages/Results';
@@ -19,19 +21,24 @@ function App() {
return (
<QueryClientProvider client={queryClient}>
<StudyProvider>
<BrowserRouter>
<Routes>
{/* Home page - no sidebar layout */}
<Route path="/" element={<Home />} />
<ClaudeTerminalProvider>
<BrowserRouter>
<Routes>
{/* Home page - no sidebar layout */}
<Route path="/" element={<Home />} />
{/* Study pages - with sidebar layout */}
<Route element={<MainLayout />}>
<Route path="dashboard" element={<Dashboard />} />
<Route path="results" element={<Results />} />
<Route path="analytics" element={<Dashboard />} />
</Route>
</Routes>
</BrowserRouter>
{/* Study pages - with sidebar layout */}
<Route element={<MainLayout />}>
<Route path="dashboard" element={<Dashboard />} />
<Route path="results" element={<Results />} />
<Route path="analytics" element={<Dashboard />} />
</Route>
</Routes>
{/* Global Claude Terminal - persists across navigation */}
<GlobalClaudeTerminal />
</BrowserRouter>
</ClaudeTerminalProvider>
</StudyProvider>
</QueryClientProvider>
);

View File

@@ -13,6 +13,7 @@ import {
FolderOpen
} from 'lucide-react';
import { useStudy } from '../context/StudyContext';
import { useClaudeTerminal } from '../context/ClaudeTerminalContext';
interface ClaudeTerminalProps {
isExpanded?: boolean;
@@ -26,17 +27,24 @@ export const ClaudeTerminal: React.FC<ClaudeTerminalProps> = ({
onClose
}) => {
const { selectedStudy } = useStudy();
const { setIsConnected: setGlobalConnected } = useClaudeTerminal();
const terminalRef = useRef<HTMLDivElement>(null);
const xtermRef = useRef<Terminal | null>(null);
const fitAddonRef = useRef<FitAddon | null>(null);
const wsRef = useRef<WebSocket | null>(null);
const [isConnected, setIsConnected] = useState(false);
const [isConnected, setIsConnectedLocal] = useState(false);
const [isConnecting, setIsConnecting] = useState(false);
const [_error, setError] = useState<string | null>(null);
const [cliAvailable, setCliAvailable] = useState<boolean | null>(null);
const [contextSet, setContextSet] = useState(false);
const [settingContext, setSettingContext] = useState(false);
// Sync local connection state to global context
const setIsConnected = useCallback((connected: boolean) => {
setIsConnectedLocal(connected);
setGlobalConnected(connected);
}, [setGlobalConnected]);
// Check CLI availability
useEffect(() => {
fetch('/api/terminal/status')
@@ -251,9 +259,13 @@ export const ClaudeTerminal: React.FC<ClaudeTerminalProps> = ({
if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN || !selectedStudy?.id) return;
setSettingContext(true);
// Send context message - Claude should use CLAUDE.md and .claude/skills/ for guidance
const contextMessage = `Context: Working on study "${selectedStudy.id}" at studies/${selectedStudy.id}/. ` +
`Read .claude/skills/ for task protocols. Use atomizer conda env. Acknowledge briefly.`;
// Send context message with POS bootstrap instructions and study context
const contextMessage =
`You are helping with Atomizer optimization. ` +
`First read: .claude/skills/00_BOOTSTRAP.md for task routing. ` +
`Then follow the Protocol Execution Framework. ` +
`Study context: Working on "${selectedStudy.id}" at studies/${selectedStudy.id}/. ` +
`Use atomizer conda env. Acknowledge briefly.`;
wsRef.current.send(JSON.stringify({ type: 'input', data: contextMessage + '\n' }));
// Mark as done after Claude has had time to process

View File

@@ -0,0 +1,50 @@
import React from 'react';
import { useClaudeTerminal } from '../context/ClaudeTerminalContext';
import { ClaudeTerminal } from './ClaudeTerminal';
import { Terminal } from 'lucide-react';
/**
* GlobalClaudeTerminal - A floating terminal that persists across page navigation
*
* This component renders at the App level and maintains the Claude Code session
* even when the user navigates between pages. It can be minimized to a floating
* button or expanded to a side panel.
*/
export const GlobalClaudeTerminal: React.FC = () => {
const { isOpen, setIsOpen, isExpanded, setIsExpanded, isConnected } = useClaudeTerminal();
// Floating button when terminal is closed
if (!isOpen) {
return (
<button
onClick={() => setIsOpen(true)}
className={`fixed bottom-6 right-6 p-4 rounded-full shadow-lg transition-all z-50 ${
isConnected
? 'bg-green-600 hover:bg-green-500'
: 'bg-primary-600 hover:bg-primary-500'
}`}
title={isConnected ? 'Claude Terminal (Connected)' : 'Open Claude Terminal'}
>
<Terminal className="w-6 h-6 text-white" />
{isConnected && (
<span className="absolute -top-1 -right-1 w-3 h-3 bg-green-400 rounded-full border-2 border-dark-900 animate-pulse" />
)}
</button>
);
}
// Terminal panel
return (
<div className={`fixed z-50 transition-all duration-200 ${
isExpanded
? 'inset-4'
: 'bottom-6 right-6 w-[650px] h-[500px]'
}`}>
<ClaudeTerminal
isExpanded={isExpanded}
onToggleExpand={() => setIsExpanded(!isExpanded)}
onClose={() => setIsOpen(false)}
/>
</div>
);
};

View File

@@ -0,0 +1,42 @@
import React, { createContext, useContext, useState, ReactNode } from 'react';
interface ClaudeTerminalContextType {
// Terminal visibility state
isOpen: boolean;
setIsOpen: (open: boolean) => void;
isExpanded: boolean;
setIsExpanded: (expanded: boolean) => void;
// Connection state (updated by the terminal component)
isConnected: boolean;
setIsConnected: (connected: boolean) => void;
}
const ClaudeTerminalContext = createContext<ClaudeTerminalContextType | undefined>(undefined);
export const ClaudeTerminalProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [isOpen, setIsOpen] = useState(false);
const [isExpanded, setIsExpanded] = useState(false);
const [isConnected, setIsConnected] = useState(false);
return (
<ClaudeTerminalContext.Provider value={{
isOpen,
setIsOpen,
isExpanded,
setIsExpanded,
isConnected,
setIsConnected
}}>
{children}
</ClaudeTerminalContext.Provider>
);
};
export const useClaudeTerminal = () => {
const context = useContext(ClaudeTerminalContext);
if (context === undefined) {
throw new Error('useClaudeTerminal must be used within a ClaudeTerminalProvider');
}
return context;
};

View File

@@ -4,9 +4,9 @@ import { Terminal } from 'lucide-react';
import { useOptimizationWebSocket } from '../hooks/useWebSocket';
import { apiClient } from '../api/client';
import { useStudy } from '../context/StudyContext';
import { useClaudeTerminal } from '../context/ClaudeTerminalContext';
import { Card } from '../components/common/Card';
import { ControlPanel } from '../components/dashboard/ControlPanel';
import { ClaudeTerminal } from '../components/ClaudeTerminal';
import { ParetoPlot } from '../components/ParetoPlot';
import { ParallelCoordinatesPlot } from '../components/ParallelCoordinatesPlot';
import { ParameterImportanceChart } from '../components/ParameterImportanceChart';
@@ -64,9 +64,8 @@ export default function Dashboard() {
// Chart library toggle: 'recharts' (faster) or 'plotly' (more interactive but slower)
const [chartLibrary, setChartLibrary] = useState<'plotly' | 'recharts'>('recharts');
// Claude chat panel state
const [chatOpen, setChatOpen] = useState(false);
const [chatExpanded, setChatExpanded] = useState(false);
// Claude terminal from global context
const { isOpen: claudeTerminalOpen, setIsOpen: setClaudeTerminalOpen, isConnected: claudeConnected } = useClaudeTerminal();
const showAlert = (type: 'success' | 'warning', message: string) => {
const id = alertIdCounter;
@@ -353,16 +352,21 @@ export default function Dashboard() {
<div className="flex gap-2">
{/* Claude Code Terminal Toggle Button */}
<button
onClick={() => setChatOpen(!chatOpen)}
onClick={() => setClaudeTerminalOpen(!claudeTerminalOpen)}
className={`flex items-center gap-2 px-4 py-2 rounded-lg transition-colors ${
chatOpen
claudeTerminalOpen
? 'bg-primary-600 text-white'
: 'bg-dark-700 text-dark-200 hover:bg-dark-600 hover:text-white border border-dark-600'
: claudeConnected
? 'bg-green-700 text-white border border-green-600'
: 'bg-dark-700 text-dark-200 hover:bg-dark-600 hover:text-white border border-dark-600'
}`}
title="Open Claude Code terminal"
title={claudeConnected ? 'Claude Terminal (Connected)' : 'Open Claude Code terminal'}
>
<Terminal className="w-4 h-4" />
<span className="hidden sm:inline">Claude Code</span>
{claudeConnected && !claudeTerminalOpen && (
<span className="w-2 h-2 bg-green-400 rounded-full animate-pulse" />
)}
</button>
{selectedStudyId && (
<StudyReportViewer studyId={selectedStudyId} />
@@ -417,10 +421,10 @@ export default function Dashboard() {
<ControlPanel onStatusChange={refreshStudies} horizontal />
</div>
{/* Main Layout: Charts + Claude Terminal */}
<div className={`grid gap-4 ${chatOpen ? 'grid-cols-12' : 'grid-cols-1'}`}>
{/* Main Layout: Charts (Claude Terminal is now global/floating) */}
<div className="grid gap-4 grid-cols-1">
{/* Main Content - Charts stacked vertically */}
<main className={chatOpen ? 'col-span-7' : 'col-span-1'}>
<main>
{/* Study Name Header + Metrics in one row */}
<div className="mb-4 pb-3 border-b border-dark-600 flex items-center justify-between">
<div>
@@ -790,17 +794,6 @@ export default function Dashboard() {
/>
</div>
</main>
{/* Claude Code Terminal - Right Sidebar (wider for better visibility) */}
{chatOpen && (
<aside className="col-span-5 h-[calc(100vh-12rem)] sticky top-4">
<ClaudeTerminal
isExpanded={chatExpanded}
onToggleExpand={() => setChatExpanded(!chatExpanded)}
onClose={() => setChatOpen(false)}
/>
</aside>
)}
</div>
</div>
);

View File

@@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom';
import {
FolderOpen,
Play,
Pause,
CheckCircle,
Clock,
AlertCircle,
@@ -64,6 +65,8 @@ const Home: React.FC = () => {
switch (status) {
case 'running':
return <Play className="w-3.5 h-3.5" />;
case 'paused':
return <Pause className="w-3.5 h-3.5" />;
case 'completed':
return <CheckCircle className="w-3.5 h-3.5" />;
case 'not_started':
@@ -81,6 +84,12 @@ const Home: React.FC = () => {
card: 'border-green-500/30 hover:border-green-500/50',
glow: 'shadow-green-500/10'
};
case 'paused':
return {
badge: 'bg-orange-500/20 text-orange-400 border-orange-500/30',
card: 'border-orange-500/30 hover:border-orange-500/50',
glow: 'shadow-orange-500/10'
};
case 'completed':
return {
badge: 'bg-blue-500/20 text-blue-400 border-blue-500/30',

View File

@@ -10,7 +10,7 @@ export default defineConfig({
strictPort: false, // Allow fallback to next available port
proxy: {
'/api': {
target: 'http://127.0.0.1:8001', // Use 127.0.0.1 instead of localhost
target: 'http://127.0.0.1:8000', // Use 127.0.0.1 instead of localhost
changeOrigin: true,
secure: false,
ws: true,