Spaces:
Running
Running
| import { useCallback, useRef } from 'react'; | |
| import { apiFetch, isPairingRequiredError } from '../api.js'; | |
| import { canUseAppShellFromStatus, connectionStateFromStatus } from '../relay-status.js'; | |
| import { isDraftSession } from '../app-core-utils.js'; | |
| import { createProjectSessionActions } from './project-session-actions.js'; | |
| function blurActiveElement() { | |
| if (typeof document.activeElement?.blur === 'function') { | |
| document.activeElement.blur(); | |
| } | |
| } | |
| export function useProjectController(app, runRegistry) { | |
| const sessionLoadIdRef = useRef(0); | |
| const loadStatus = useCallback(async () => { | |
| const data = await apiFetch('/api/status'); | |
| app.setStatus(data); | |
| app.setAuthenticated(canUseAppShellFromStatus(data)); | |
| app.setConnectionState(connectionStateFromStatus(data)); | |
| runRegistry.syncActiveRunsFromStatus(data); | |
| return data; | |
| }, [app, runRegistry]); | |
| const clearSelectedMessages = () => { | |
| sessionLoadIdRef.current += 1; | |
| app.setMessages([]); | |
| }; | |
| const loadSessionMessages = async (session) => { | |
| if (!session?.id) { | |
| clearSelectedMessages(); | |
| return false; | |
| } | |
| const loadId = ++sessionLoadIdRef.current; | |
| const data = await apiFetch(`/api/sessions/${encodeURIComponent(session.id)}/messages?limit=120`); | |
| if (sessionLoadIdRef.current !== loadId) { | |
| return false; | |
| } | |
| app.setMessages(data.messages || []); | |
| return true; | |
| }; | |
| const loadSessions = useCallback(async (project, chooseLatest = true) => { | |
| if (!project) { | |
| app.setSelectedSession(null); | |
| clearSelectedMessages(); | |
| return; | |
| } | |
| app.setLoadingProjectId(project.id); | |
| try { | |
| const data = await apiFetch(`/api/projects/${encodeURIComponent(project.id)}/sessions`); | |
| const nextSessions = data.sessions || []; | |
| app.setSessionsByProject((current) => ({ ...current, [project.id]: nextSessions })); | |
| if (chooseLatest) { | |
| const next = nextSessions[0] || null; | |
| app.setSelectedSession(next); | |
| if (next) { | |
| await loadSessionMessages(next); | |
| } else { | |
| clearSelectedMessages(); | |
| } | |
| } else { | |
| app.setSelectedSession(null); | |
| clearSelectedMessages(); | |
| } | |
| } finally { | |
| app.setLoadingProjectId((current) => (current === project.id ? null : current)); | |
| } | |
| }, [app]); | |
| const loadProjects = useCallback(async () => { | |
| const data = await apiFetch('/api/projects'); | |
| const list = data.projects || []; | |
| app.setProjects(list); | |
| const hiddenIds = app.hiddenProjectIdsRef.current; | |
| const visibleList = list.filter((project) => !hiddenIds.has(project.id)); | |
| const currentSelected = app.selectedProjectRef.current; | |
| const preferred = | |
| visibleList.find((project) => project.id === currentSelected?.id) || | |
| visibleList.find((project) => project.name.toLowerCase() === 'codexmobile') || | |
| visibleList.find((project) => project.path.toLowerCase().includes('codexmobile')) || | |
| visibleList[0] || | |
| null; | |
| app.setSelectedProject(preferred); | |
| if (preferred) { | |
| app.setExpandedProjectIds((current) => ({ ...current, [preferred.id]: true })); | |
| } | |
| await loadSessions(preferred); | |
| }, [app, loadSessions]); | |
| const bootstrap = useCallback(async () => { | |
| try { | |
| const currentStatus = await loadStatus(); | |
| if (canUseAppShellFromStatus(currentStatus)) { | |
| await loadProjects(); | |
| app.setSyncing(true); | |
| apiFetch('/api/sync', { method: 'POST' }) | |
| .then(async () => { | |
| await loadStatus(); | |
| const project = app.selectedProjectRef.current; | |
| if (project?.id) { | |
| await refreshProjectSessions(project); | |
| } else { | |
| await loadProjects(); | |
| } | |
| }) | |
| .catch(() => null) | |
| .finally(() => app.setSyncing(false)); | |
| } | |
| } catch (error) { | |
| if (isPairingRequiredError(error)) { | |
| app.setAuthenticated(false); | |
| app.setConnectionState('pairing_required'); | |
| } | |
| } | |
| }, [app, loadProjects, loadStatus]); | |
| const handleSync = async () => { | |
| app.setSyncing(true); | |
| try { | |
| await apiFetch('/api/sync', { method: 'POST' }); | |
| const emptyHiddenProjectIds = new Set(); | |
| app.hiddenProjectIdsRef.current = emptyHiddenProjectIds; | |
| app.setHiddenProjectIds(emptyHiddenProjectIds); | |
| await loadStatus(); | |
| await loadProjects(); | |
| } finally { | |
| app.setSyncing(false); | |
| } | |
| }; | |
| const handleHideProject = async (project) => { | |
| if (!project?.id) { | |
| return; | |
| } | |
| const nextHiddenProjectIds = new Set(app.hiddenProjectIdsRef.current); | |
| nextHiddenProjectIds.add(project.id); | |
| app.hiddenProjectIdsRef.current = nextHiddenProjectIds; | |
| app.setHiddenProjectIds(nextHiddenProjectIds); | |
| app.setExpandedProjectIds((current) => { | |
| const next = { ...current }; | |
| delete next[project.id]; | |
| return next; | |
| }); | |
| if (app.selectedProjectRef.current?.id !== project.id) { | |
| return; | |
| } | |
| const nextProject = app.projects.find((item) => item.id !== project.id && !nextHiddenProjectIds.has(item.id)) || null; | |
| app.selectedProjectRef.current = nextProject; | |
| app.selectedSessionRef.current = null; | |
| app.setSelectedProject(nextProject); | |
| app.setSelectedSession(null); | |
| clearSelectedMessages(); | |
| app.setAttachments([]); | |
| app.setInput(''); | |
| if (nextProject) { | |
| app.setExpandedProjectIds((current) => ({ ...current, [nextProject.id]: true })); | |
| await loadSessions(nextProject, true); | |
| } | |
| }; | |
| const handleToggleProject = async (project) => { | |
| const isExpanded = Boolean(app.expandedProjectIds[project.id]); | |
| if (isExpanded) { | |
| app.setExpandedProjectIds((current) => { | |
| const next = { ...current }; | |
| delete next[project.id]; | |
| return next; | |
| }); | |
| return; | |
| } | |
| app.setExpandedProjectIds((current) => ({ ...current, [project.id]: true })); | |
| const projectChanged = app.selectedProject?.id !== project.id; | |
| app.setSelectedProject(project); | |
| if (projectChanged) { | |
| app.setSelectedSession(null); | |
| clearSelectedMessages(); | |
| } | |
| if (!app.sessionsByProject[project.id]) { | |
| await loadSessions(project, false); | |
| } | |
| }; | |
| const handleSelectSession = async (project, session) => { | |
| const nextProject = | |
| project || | |
| app.projects.find((item) => item.id === session?.projectId) || | |
| app.selectedProjectRef.current; | |
| blurActiveElement(); | |
| if (nextProject?.id) { | |
| app.selectedProjectRef.current = nextProject; | |
| app.setSelectedProject(nextProject); | |
| app.setExpandedProjectIds((current) => ({ ...current, [nextProject.id]: true })); | |
| } | |
| app.selectedSessionRef.current = session; | |
| app.setSelectedSession(session); | |
| app.setAttachments([]); | |
| app.setInput(''); | |
| if (isDraftSession(session)) { | |
| clearSelectedMessages(); | |
| app.setDrawerOpen(false); | |
| return; | |
| } | |
| try { | |
| await loadSessionMessages(session); | |
| } catch (error) { | |
| console.warn(`[project] failed to load session messages session=${session.id}:`, error.message || error); | |
| } finally { | |
| app.setDrawerOpen(false); | |
| } | |
| }; | |
| const refreshProjectSessions = async (project) => { | |
| if (!project?.id) { | |
| return; | |
| } | |
| const [projectData, sessionData] = await Promise.all([ | |
| apiFetch('/api/projects'), | |
| apiFetch(`/api/projects/${encodeURIComponent(project.id)}/sessions`) | |
| ]); | |
| const nextProjects = projectData.projects || []; | |
| app.setProjects(nextProjects); | |
| const nextSessions = sessionData.sessions || []; | |
| app.setSessionsByProject((current) => ({ ...current, [project.id]: nextSessions })); | |
| const currentProjectId = app.selectedProjectRef.current?.id || app.selectedProject?.id; | |
| if (currentProjectId && currentProjectId !== project.id) { | |
| return; | |
| } | |
| const nextSelectedProject = nextProjects.find((item) => item.id === currentProjectId); | |
| if (nextSelectedProject) { | |
| app.setSelectedProject(nextSelectedProject); | |
| } | |
| if (!app.selectedSessionRef.current && nextSessions[0]) { | |
| const nextSession = nextSessions[0]; | |
| app.setSelectedSession(nextSession); | |
| await loadSessionMessages(nextSession); | |
| } | |
| }; | |
| const { | |
| handleRenameSession, | |
| handleDeleteSession, | |
| handleNewConversation | |
| } = createProjectSessionActions({ app, clearSelectedMessages, refreshProjectSessions, blurActiveElement }); | |
| return { | |
| loadStatus, | |
| loadProjects, | |
| bootstrap, | |
| handleSync, | |
| handleToggleProject, | |
| handleHideProject, | |
| handleSelectSession, | |
| handleRenameSession, | |
| handleDeleteSession, | |
| handleNewConversation | |
| }; | |
| } | |