// Project / conversation / message / tab persistence — backed by the // daemon's SQLite store. All writes round-trip through HTTP so projects // stay coherent across multiple browser tabs and across restarts. // // These helpers fail soft (returning null / [] on transport errors) so // the UI can stay rendered when the daemon is briefly unreachable. import type { ChatMessage, Conversation, OpenTabsState, Project, ProjectMetadata, ProjectTemplate, } from '../types'; export async function listProjects(): Promise { try { const resp = await fetch('/api/projects'); if (!resp.ok) return []; const json = (await resp.json()) as { projects: Project[] }; return json.projects ?? []; } catch { return []; } } export async function getProject(id: string): Promise { try { const resp = await fetch(`/api/projects/${encodeURIComponent(id)}`); if (!resp.ok) return null; const json = (await resp.json()) as { project: Project }; return json.project; } catch { return null; } } export async function createProject(input: { name: string; skillId: string | null; designSystemId: string | null; pendingPrompt?: string; metadata?: ProjectMetadata; }): Promise<{ project: Project; conversationId: string } | null> { try { const id = crypto.randomUUID(); const resp = await fetch('/api/projects', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id, ...input }), }); if (!resp.ok) return null; return (await resp.json()) as { project: Project; conversationId: string }; } catch { return null; } } // ---------- templates ---------- export async function listTemplates(): Promise { try { const resp = await fetch('/api/templates'); if (!resp.ok) return []; const json = (await resp.json()) as { templates: ProjectTemplate[] }; return json.templates ?? []; } catch { return []; } } export async function getTemplate(id: string): Promise { try { const resp = await fetch(`/api/templates/${encodeURIComponent(id)}`); if (!resp.ok) return null; const json = (await resp.json()) as { template: ProjectTemplate }; return json.template; } catch { return null; } } export async function saveTemplate(input: { name: string; description?: string; sourceProjectId: string; }): Promise { try { const resp = await fetch('/api/templates', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input), }); if (!resp.ok) return null; const json = (await resp.json()) as { template: ProjectTemplate }; return json.template; } catch { return null; } } export async function deleteTemplate(id: string): Promise { try { const resp = await fetch(`/api/templates/${encodeURIComponent(id)}`, { method: 'DELETE', }); return resp.ok; } catch { return false; } } export async function patchProject( id: string, patch: Partial, ): Promise { try { const resp = await fetch(`/api/projects/${encodeURIComponent(id)}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(patch), }); if (!resp.ok) return null; const json = (await resp.json()) as { project: Project }; return json.project; } catch { return null; } } export async function deleteProject(id: string): Promise { try { const resp = await fetch(`/api/projects/${encodeURIComponent(id)}`, { method: 'DELETE', }); return resp.ok; } catch { return false; } } // ---------- conversations ---------- export async function listConversations( projectId: string, ): Promise { try { const resp = await fetch( `/api/projects/${encodeURIComponent(projectId)}/conversations`, ); if (!resp.ok) return []; const json = (await resp.json()) as { conversations: Conversation[] }; return json.conversations ?? []; } catch { return []; } } export async function createConversation( projectId: string, title?: string, ): Promise { try { const resp = await fetch( `/api/projects/${encodeURIComponent(projectId)}/conversations`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title }), }, ); if (!resp.ok) return null; const json = (await resp.json()) as { conversation: Conversation }; return json.conversation; } catch { return null; } } export async function patchConversation( projectId: string, conversationId: string, patch: Partial, ): Promise { try { const resp = await fetch( `/api/projects/${encodeURIComponent(projectId)}/conversations/${encodeURIComponent(conversationId)}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(patch), }, ); if (!resp.ok) return null; const json = (await resp.json()) as { conversation: Conversation }; return json.conversation; } catch { return null; } } export async function deleteConversation( projectId: string, conversationId: string, ): Promise { try { const resp = await fetch( `/api/projects/${encodeURIComponent(projectId)}/conversations/${encodeURIComponent(conversationId)}`, { method: 'DELETE' }, ); return resp.ok; } catch { return false; } } // ---------- messages ---------- export async function listMessages( projectId: string, conversationId: string, ): Promise { try { const resp = await fetch( `/api/projects/${encodeURIComponent(projectId)}/conversations/${encodeURIComponent(conversationId)}/messages`, ); if (!resp.ok) return []; const json = (await resp.json()) as { messages: ChatMessage[] }; return json.messages ?? []; } catch { return []; } } export async function saveMessage( projectId: string, conversationId: string, message: ChatMessage, ): Promise { try { await fetch( `/api/projects/${encodeURIComponent(projectId)}/conversations/${encodeURIComponent(conversationId)}/messages/${encodeURIComponent(message.id)}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(message), }, ); } catch { // best-effort persistence — UI keeps the message in-memory either way } } // ---------- tabs ---------- export async function loadTabs(projectId: string): Promise { try { const resp = await fetch( `/api/projects/${encodeURIComponent(projectId)}/tabs`, ); if (!resp.ok) return { tabs: [], active: null }; return (await resp.json()) as OpenTabsState; } catch { return { tabs: [], active: null }; } } export async function saveTabs( projectId: string, state: OpenTabsState, ): Promise { try { await fetch(`/api/projects/${encodeURIComponent(projectId)}/tabs`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(state), }); } catch { // best-effort } }