import useAppStore from '../store/appStore' import { getProcessStatus, getTree, readFile } from '../api/projects' // When opening a session we default to this file if it exists in the tree. // Just a convention — the React template's entry is here; other templates // will eventually carry the path in their manifest (Phase 5a). const DEFAULT_ENTRY = 'frontend/src/App.jsx' export function useSession() { const { setSessions, setActiveSessionId, setMessages, setProject, resetProject, resetSessionTokens, resetInferenceStats, setRuntime, resetRuntime, stopStream, clearPacing, } = useAppStore() async function loadSessions() { const res = await fetch('/api/sessions') const data = await res.json() const sessions = Array.isArray(data) ? data : [] setSessions(sessions) return sessions } async function createSession(name = 'New Session', template) { const body = { name } if (template) body.template = template const res = await fetch('/api/sessions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }) const session = await res.json() await loadSessions() return session } async function openSession(id) { // Mid-stream session switch: abort the in-flight chat so it doesn't // append to a session the user is no longer looking at. Same for the // pacing pill, which would otherwise stay visible across the switch. stopStream() clearPacing() const res = await fetch(`/api/sessions/${id}`) const data = await res.json() setActiveSessionId(data.id) // Rehydrate historical assistant turns with their tool log so old // conversations render the same way they originally streamed. const messages = (data.messages || []).map((m) => { if (m.role !== 'assistant' || !m.tool_calls_json) return m try { return { ...m, toolCalls: JSON.parse(m.tool_calls_json) } } catch { return m } }) setMessages(messages) resetSessionTokens() resetInferenceStats() // Reset runtime so the Run button shows the correct state for the new project. resetRuntime() const projectId = data.project_id if (!projectId) { resetProject() return } const tree = await getTree(projectId) // Prefetch every file. Scaffold templates are small (<20 files); Phase 5 // will switch to lazy load when project sizes grow. const fileEntries = tree.filter((e) => !e.is_dir) const contents = await Promise.all( fileEntries.map((e) => readFile(projectId, e.path).then((c) => [e.path, c]).catch(() => null), ), ) const files = Object.fromEntries(contents.filter(Boolean)) // Open the entry file by default if present, otherwise the first file in tree. const activeFile = files[DEFAULT_ENTRY] != null ? DEFAULT_ENTRY : (fileEntries[0]?.path ?? null) setProject({ id: projectId, tree, files, activeFile, dirty: {} }) // Restore process status if dev servers are already running from a previous visit. try { const processStatus = await getProcessStatus(projectId) const { overall, frontend, backend } = processStatus if (overall !== 'idle') { setRuntime({ status: overall, frontendPort: frontend.port, backendPort: backend.port, frontendStatus: frontend.status, backendStatus: backend.status, }) } } catch { /* not fatal — user can click Run again */ } } async function deleteSession(id) { await fetch(`/api/sessions/${id}`, { method: 'DELETE' }) const sessions = await loadSessions() if (sessions.length > 0) { await openSession(sessions[0].id) } else { setActiveSessionId(null) setMessages([]) resetProject() } } async function renameSession(id, name) { await fetch(`/api/sessions/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name }), }) await loadSessions() } return { loadSessions, createSession, openSession, deleteSession, renameSession } }