import type { ChatMessage, Mode, SessionMeta } from "../types"; import { request } from "./core"; /** One chat message as returned by GET /api/v1/sessions/{id}/messages. Mirrors * the `chat_messages` row; `mode`/`mood`/`image`/`thinking` are optional. */ export interface MessageDto { id: string; sessionId: string; role: "user" | "assistant" | "system"; content: string; mode?: Mode; mood?: string; image?: string; thinking?: string; /** Source-platform tag (design/16 §4.4): "web" or an IM channel platform. */ platform?: string; /** Concrete client/device that produced the message (design/16 §4.5). */ deviceId?: string; /** Sender display name of a third-party message (design/16 §4.6). Blank for the owner. */ senderName?: string; /** The role that spoke this message (design/22 §7-5, backing column * `chat_messages.persona_id`); `''` means "inherit the current default role". */ personaId?: string; createdAt: number; } /** Map a wire message into a display ChatMessage (carrying its sessionId). */ function toChatMessage(m: MessageDto): ChatMessage { return { id: m.id, sessionId: m.sessionId, role: m.role, content: m.content, mode: m.mode, mood: m.mood, image: m.image, thinking: m.thinking, platform: m.platform, deviceId: m.deviceId, senderName: m.senderName, personaId: m.personaId, createdAt: m.createdAt, }; } /** GET /api/v1/sessions — the main + secondary sessions for this web account. */ export function fetchSessions(): Promise { return request("/sessions"); } /** `POST /api/v1/sessions` reply: the new session's id, plus the bound role when * one was requested (docs/api-client.md §5) — the full summary is not echoed. `id` * is accepted too so a backend that returns a summary still reads correctly. */ export interface CreateSessionResult { sessionId?: string; id?: string; personaId?: string; } /** POST /api/v1/sessions — creates a new secondary session. `title` is the * optional display name; `personaId` binds the session to a role (design/22 * §4.5). An omitted/empty `personaId` leaves the session inheriting the global * default role. The reply carries only the new id, so callers seed the new row * locally and let the next `GET /sessions` supply the metadata. */ export function createSubSession(opts: { title?: string; personaId?: string; } = {}): Promise { return request("/sessions", { method: "POST", body: JSON.stringify({ ...(opts.title ? { title: opts.title } : {}), ...(opts.personaId ? { personaId: opts.personaId } : {}), }), }); } /** One PATCH /api/v1/sessions/{id} change set. At least one field must be set; * the gateway rejects an empty body with 400. `personaId: ""` explicitly * re-binds the session to "inherit the default role" (design/22 §4.5). */ export interface SessionPatch { title?: string; pinned?: boolean; personaId?: string; } /** PATCH /api/v1/sessions/{id} — updates a session's display metadata and/or its * bound role. The reply echoes only the fields that were changed. */ export function patchSession( sessionId: string, patch: SessionPatch ): Promise<{ id: string } & SessionPatch> { return request<{ id: string } & SessionPatch>( `/sessions/${encodeURIComponent(sessionId)}`, { method: "PATCH", body: JSON.stringify(patch) } ); } /** GET /api/v1/sessions/{id}/messages?before=&limit=&platform= — hot-paginated * history, optionally filtered to one source platform (design/16 §4.4). The wire * returns newest-first, but callers render chronologically (oldest first), so we * reverse here: page 1 is the most recent `limit` messages in display order, and * `before` (the oldest message's createdAt) walks upward. The Web client omits * `platform` for the full shared main session; an IM adapter passes its own * platform to see only its lane. */ export async function fetchMessages( sessionId: string, before?: number, limit = 50, platform?: string ): Promise { const qs = new URLSearchParams(); qs.set("limit", String(limit)); if (before !== undefined) qs.set("before", String(before)); if (platform) qs.set("platform", platform); const list = await request( `/sessions/${encodeURIComponent(sessionId)}/messages?${qs.toString()}` ); return list.map(toChatMessage).reverse(); } /** DELETE /api/v1/sessions/{id} — removes a secondary session (and its messages). */ export function deleteSession( sessionId: string ): Promise<{ id: string; deleted: true }> { return request<{ id: string; deleted: true }>( `/sessions/${encodeURIComponent(sessionId)}`, { method: "DELETE" } ); } /** POST /api/v1/sessions/{id}/read — advance this account's read cursor for the * session to `before` (ms since epoch; omitted = "now"). One cursor per session, * forward only: a stale client can never re-open messages as unread. Unknown * session → 404 (ApiError). */ export function markSessionRead( sessionId: string, before?: number ): Promise<{ sessionId: string; readAt: number }> { return request<{ sessionId: string; readAt: number }>( `/sessions/${encodeURIComponent(sessionId)}/read`, { method: "POST", body: JSON.stringify(before === undefined ? {} : { before }), } ); }