import type { AgentSchedule, AgentThread, ImageChunk, Message } from "./types" export type { AgentSchedule, AgentThread, Message } export class AgentsApiError extends Error { constructor( public readonly status: number, message: string ) { super(message) this.name = "AgentsApiError" } } export interface ThreadMessageRequest { content: string images?: Array model_id?: string | null effort?: string | null plan_mode?: boolean } export interface ScheduleCreateRequest { prompt: string schedule: string name?: string | null repo?: string | null model_id?: string | null effort?: string | null slack_report_channel?: string | null } export interface ScheduleUpdateRequest { prompt?: string | null schedule?: string | null name?: string | null repo?: string | null model_id?: string | null effort?: string | null enabled?: boolean | null slack_report_channel?: string | null } export interface ThreadPrDiffFile { path: string previousPath: string | null status: "added" | "removed" | "modified" | "renamed" | string additions: number deletions: number originalContent: string | null modifiedContent: string | null unrenderable: boolean } export interface ThreadPrDiff { prNumber: number baseSha: string headSha: string truncated: boolean files: Array } export interface ThreadRecoveryPatch { blob: Blob filename: string } export type WorkflowApprovalStatus = "pending" | "approved" | "rejected" export interface WorkflowDiffStats { files: number additions: number deletions: number } export interface WorkflowPushApproval { fingerprint: string status: WorkflowApprovalStatus repo: string branch: string baseSha: string headSha: string files: Array diffStats: WorkflowDiffStats diffPreview: string diffPreviewTruncated: boolean approvalUrl: string | null requestedAt: string | null decidedAt: string | null decidedBy: string | null } export interface WorkflowPushApprovalsResponse { threadId: string isOwner: boolean approvals: Array } export interface WorkflowApprovalDecision { status: string fingerprint: string } // Legacy alias kept for backward-compatible prop typing. export type WorkflowApproval = WorkflowPushApproval export interface WorkflowApprovalsPayload { approvals: Array } export interface ThreadsPageParams { limit?: number offset?: number all?: boolean resolved?: boolean viewed?: boolean source?: string status?: string q?: string } export interface ThreadsPage { items: Array total?: number limit: number offset: number hasMore?: boolean } export interface SidebarThreadsGroup { items: Array limit: number hasMore: boolean } export interface SidebarThreads { active: SidebarThreadsGroup resolved: SidebarThreadsGroup } const API_BASE = (import.meta.env.VITE_DASHBOARD_API_BASE_URL ?? "").replace( /\/$/, "" ) export const agentsLangGraphApiUrl = `${API_BASE}/dashboard/api` async function agentsRequest( path: string, init: RequestInit = {} ): Promise { const res = await fetch(`${API_BASE}/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 AgentsApiError(res.status, message) } if (res.status === 204) return undefined as T return (await res.json()) as T } function filenameFromContentDisposition(value: string | null): string | null { const match = /filename="([^"]+)"/.exec(value ?? "") return match?.[1] ?? null } async function agentsBlobRequest(path: string): Promise { const res = await fetch(`${API_BASE}/dashboard/api${path}`, { credentials: "include", headers: { Accept: "text/x-diff" }, }) 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 AgentsApiError(res.status, message) } return { blob: await res.blob(), filename: filenameFromContentDisposition(res.headers.get("content-disposition")) ?? "open-swe-recovery.patch", } } function buildThreadsPageQuery(params: ThreadsPageParams): string { const search = new URLSearchParams() if (params.limit != null) search.set("limit", String(params.limit)) if (params.offset != null) search.set("offset", String(params.offset)) if (params.all != null) search.set("all", String(params.all)) if (params.resolved != null) search.set("resolved", String(params.resolved)) if (params.viewed != null) search.set("viewed", String(params.viewed)) if (params.source) search.set("source", params.source) if (params.status) search.set("status", params.status) if (params.q) search.set("q", params.q) const query = search.toString() return query ? `?${query}` : "" } function buildSidebarThreadsQuery(params: { activeLimit?: number resolvedLimit?: number activeThreadId?: string }): string { const search = new URLSearchParams() if (params.activeLimit != null) { search.set("active_limit", String(params.activeLimit)) } if (params.resolvedLimit != null) { search.set("resolved_limit", String(params.resolvedLimit)) } if (params.activeThreadId) { search.set("active_thread_id", params.activeThreadId) } const query = search.toString() return query ? `?${query}` : "" } export const agentsApi = { langGraphApiUrl: agentsLangGraphApiUrl, listSidebarThreads: (params: { activeLimit?: number resolvedLimit?: number activeThreadId?: string }) => agentsRequest( `/threads/sidebar${buildSidebarThreadsQuery(params)}` ), listThreadsPage: (params: ThreadsPageParams = {}) => agentsRequest(`/threads/page${buildThreadsPageQuery(params)}`), resolveThread: (threadId: string, resolved: boolean) => agentsRequest( `/threads/${encodeURIComponent(threadId)}/resolve`, { method: "POST", body: JSON.stringify({ resolved }), } ), listSchedules: () => agentsRequest>("/schedules"), createSchedule: (body: ScheduleCreateRequest) => agentsRequest("/schedules", { method: "POST", body: JSON.stringify(body), }), updateSchedule: (scheduleId: string, body: ScheduleUpdateRequest) => agentsRequest( `/schedules/${encodeURIComponent(scheduleId)}`, { method: "PATCH", body: JSON.stringify(body), } ), deleteSchedule: (scheduleId: string) => agentsRequest(`/schedules/${encodeURIComponent(scheduleId)}`, { method: "DELETE", }), getThread: (threadId: string, options?: { markViewed?: boolean }) => agentsRequest( `/threads/${encodeURIComponent(threadId)}${ options?.markViewed === false ? "?mark_viewed=false" : "" }` ), queueMessage: (threadId: string, body: ThreadMessageRequest) => agentsRequest( `/threads/${encodeURIComponent(threadId)}/messages`, { method: "POST", body: JSON.stringify(body), } ), cancelThread: (threadId: string) => agentsRequest( `/threads/${encodeURIComponent(threadId)}/cancel`, { method: "POST", } ), adminCancelThread: (threadId: string) => agentsRequest( `/admin/threads/${encodeURIComponent(threadId)}/cancel`, { method: "POST", } ), deleteThread: (threadId: string) => agentsRequest(`/threads/${encodeURIComponent(threadId)}`, { method: "DELETE", }), getThreadPrDiff: (threadId: string) => agentsRequest( `/threads/${encodeURIComponent(threadId)}/pr-diff` ), downloadThreadRecoveryPatch: (threadId: string) => agentsBlobRequest( `/threads/${encodeURIComponent(threadId)}/recovery.patch` ), streamUrl: (threadId: string) => `${API_BASE}/dashboard/api/threads/${encodeURIComponent(threadId)}/stream`, listWorkflowApprovals: (threadId: string) => agentsRequest( `/workflow-approval/${encodeURIComponent(threadId)}` ), approveWorkflowPush: (threadId: string, fingerprint: string) => agentsRequest( `/workflow-approval/${encodeURIComponent(threadId)}/${encodeURIComponent( fingerprint )}/approve`, { method: "POST" } ), rejectWorkflowPush: (threadId: string, fingerprint: string) => agentsRequest( `/workflow-approval/${encodeURIComponent(threadId)}/${encodeURIComponent( fingerprint )}/reject`, { method: "POST" } ), } export type ThreadGroup = "today" | "last7" | "last30" | "older" export function groupThreads( threads: Array ): Record> { const todayStart = new Date() todayStart.setHours(0, 0, 0, 0) const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1000 const thirtyDaysAgo = Date.now() - 30 * 24 * 60 * 60 * 1000 const groups: Record> = { today: [], last7: [], last30: [], older: [], } for (const thread of [...threads].sort((a, b) => b.updatedAt - a.updatedAt)) { if (thread.updatedAt >= todayStart.getTime()) { groups.today.push(thread) } else if (thread.updatedAt >= sevenDaysAgo) { groups.last7.push(thread) } else if (thread.updatedAt >= thirtyDaysAgo) { groups.last30.push(thread) } else { groups.older.push(thread) } } return groups }