/** * Client for the plan-review API — plain HTTP, no realtime transport. * * The agent publishes the plan markdown; reviewers read it and leave * whole-document comments. On approve/reject the server reads those comments and * hands them to the agent as the instruction for the follow-up run. */ const API_BASE = (import.meta.env.VITE_DASHBOARD_API_BASE_URL ?? "").replace( /\/$/, "" ) function apiBase(): string { if (API_BASE) return API_BASE return typeof window !== "undefined" ? window.location.origin : "" } export interface PlanUser { id: string login: string email: string | null name: string } export type PlanStatus = | "planning" | "ready" | "revising" | "approved" | "cancelled" export interface PlanData { threadId: string status: PlanStatus markdown: string isOwner: boolean user: PlanUser } export interface PlanComment { id: string author: string author_login: string body: string created_at: string } export class PlanApiError extends Error { constructor( public readonly status: number, message: string ) { super(message) this.name = "PlanApiError" } } async function req(path: string, init: RequestInit = {}): Promise { const res = await fetch(`${apiBase()}/dashboard/api${path}`, { ...init, credentials: "include", headers: { "Content-Type": "application/json", ...(init.headers ?? {}) }, }) if (!res.ok) { let message = res.statusText try { const body = await res.json() if (body?.detail) message = typeof body.detail === "string" ? body.detail : JSON.stringify(body.detail) } catch { /* ignore */ } throw new PlanApiError(res.status, message) } if (res.status === 204) return undefined as T return (await res.json()) as T } export function getPlan(threadId: string): Promise { return req(`/plan/${encodeURIComponent(threadId)}`) } export async function getPlanComments( threadId: string ): Promise> { const { comments } = await req<{ comments: Array }>( `/plan/${encodeURIComponent(threadId)}/comments` ) return comments } export function addPlanComment( threadId: string, body: string ): Promise { return req(`/plan/${encodeURIComponent(threadId)}/comments`, { method: "POST", body: JSON.stringify({ body }), }) } export function deletePlanComment( threadId: string, commentId: string ): Promise<{ ok: boolean }> { return req( `/plan/${encodeURIComponent(threadId)}/comments/${encodeURIComponent(commentId)}`, { method: "DELETE" } ) } export function updatePlan( threadId: string, markdown: string ): Promise<{ status: PlanStatus; markdown: string }> { return req(`/plan/${encodeURIComponent(threadId)}`, { method: "PUT", body: JSON.stringify({ markdown }), }) } export function approvePlan(threadId: string): Promise<{ status: string }> { return req(`/plan/${encodeURIComponent(threadId)}/approve`, { method: "POST", }) } export function rejectPlan(threadId: string): Promise<{ status: string }> { return req(`/plan/${encodeURIComponent(threadId)}/reject`, { method: "POST", }) }