Tesseract / studio /src /hooks /useSession.js
rkbosamia's picture
Guard loadSessions against non-array API response
0a1a762
Raw
History Blame Contribute Delete
4.22 kB
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 }
}