codexmobile-relay / client /src /hooks /useProjectController.js
Codex
deploy: CodexMobile Relay
90f0300
Raw
History Blame Contribute Delete
8.68 kB
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
};
}