"use client"; import { createSerialQueue } from "./serial-queue"; import type { NumoConversation, NumoConversationDetail, NumoConversationPatch } from "./assistant-types"; /** A bounded, newest-first Numo history page across every accessible project. */ export async function fetchConversations(limit: number): Promise<{ conversations: NumoConversation[]; hasMore: boolean }> { const res = await fetch(`/api/assistant/conversations?limit=${encodeURIComponent(limit)}`); if (!res.ok) return { conversations: [], hasMore: false }; return res.json(); } /** Read the common timeline, retaining the distinction between chat and work. */ export async function fetchNumoConversation(id: string): Promise { const res = await fetch(`/api/numo/conversations/${encodeURIComponent(id)}`); if (!res.ok) throw new Error("Unable to read conversation"); return res.json(); } export async function updateConversation(id: string, patch: NumoConversationPatch): Promise { const res = await fetch(`/api/numo/conversations/${encodeURIComponent(id)}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(patch), }); return res.ok; } /** Update a conversation and retain the server's explicit validation message. */ export async function updateConversationWithResult( id: string, patch: NumoConversationPatch, ): Promise<{ ok: true } | { ok: false; error: string }> { try { const res = await fetch(`/api/numo/conversations/${encodeURIComponent(id)}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(patch), }); if (res.ok) return { ok: true }; const data = await res.json().catch(() => null) as { error?: unknown } | null; return { ok: false, error: typeof data?.error === "string" ? data.error : `HTTP ${res.status}`, }; } catch { return { ok: false, error: "Unable to save conversation settings" }; } } export async function deleteConversation( conversationId: string ): Promise { const res = await fetch( `/api/assistant/conversations?id=${encodeURIComponent(conversationId)}`, { method: "DELETE" } ); return res.ok; } /** Open conversation, and its scope (MIN-353). The pointer lives in base: * it survives reloading, the next tab and the desktop app. */ export interface ActiveConversationRef { conversationId: string | null; projectId: string | null; } export async function fetchActiveConversation(): Promise { const res = await fetch("/api/assistant/active-conversation"); if (!res.ok) return { conversationId: null, projectId: null }; return res.json(); } /** * Pointer writes, END TO END. * * They follow each other closely and cancel each other: “new * conversation” clears, the first message written. Launched in parallel, nothing * guarantees that they arrive in the order in which they left — a DELETE * processed after the PUT leaves the base without a pointer while the thread is alive. * And ignoring the response of a stale write would not change anything: the server * has already applied it. Only the TRANSMIT order is controlled, hence the queue. */ const pointerWrites = createSerialQueue(); /** `null` clears the pointer (“new conversation”). Best-effort: lose * writing only costs a failed restart, never a message. */ export function setActiveConversation( conversationId: string | null ): Promise { return pointerWrites(async () => { try { await fetch("/api/assistant/active-conversation", { method: conversationId ? "PUT" : "DELETE", ...(conversationId ? { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ conversationId }), } : {}), }); } catch { // Network cut — the pointer will update at the next message. } }); }