Merge branch 'main' into timothyjlaurent/Anthropic-error
This commit is contained in:
94
packages/dashboard/app/api/__tests__/chat-rooms-api.test.ts
Normal file
94
packages/dashboard/app/api/__tests__/chat-rooms-api.test.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
addChatRoomMember,
|
||||
createChatRoom,
|
||||
deleteChatRoom,
|
||||
deleteChatRoomMessage,
|
||||
fetchChatRoom,
|
||||
fetchChatRoomMembers,
|
||||
fetchChatRoomMessages,
|
||||
fetchChatRooms,
|
||||
postChatRoomMessage,
|
||||
removeChatRoomMember,
|
||||
updateChatRoom,
|
||||
} from "../legacy";
|
||||
|
||||
function jsonResponse(payload: unknown): Response {
|
||||
return new Response(JSON.stringify(payload), { status: 200, headers: { "content-type": "application/json" } });
|
||||
}
|
||||
|
||||
describe("chat room legacy API client", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("builds request for fetchChatRooms", async () => {
|
||||
const fetchMock = vi.spyOn(globalThis, "fetch").mockImplementation(async () => jsonResponse({ rooms: [] }));
|
||||
await fetchChatRooms({ status: "active", agentId: "agent-1" }, "proj-1");
|
||||
|
||||
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
|
||||
expect(url).toContain("/api/chat/rooms?");
|
||||
expect(url).toContain("projectId=proj-1");
|
||||
expect(url).toContain("status=active");
|
||||
expect(url).toContain("agentId=agent-1");
|
||||
expect(init.method).toBeUndefined();
|
||||
});
|
||||
|
||||
it("builds CRUD room endpoints", async () => {
|
||||
const fetchMock = vi.spyOn(globalThis, "fetch").mockImplementation(async () => jsonResponse({ success: true }));
|
||||
|
||||
await fetchChatRoom("room-1", "proj-1");
|
||||
await createChatRoom({ name: "Engineering" }, "proj-1");
|
||||
await updateChatRoom("room-1", { description: "desc" }, "proj-1");
|
||||
await deleteChatRoom("room-1", "proj-1");
|
||||
|
||||
expect((fetchMock.mock.calls[0] as [string])[0]).toContain("/api/chat/rooms/room-1?projectId=proj-1");
|
||||
|
||||
const [, createInit] = fetchMock.mock.calls[1] as [string, RequestInit];
|
||||
expect(createInit.method).toBe("POST");
|
||||
expect(createInit.body).toBe(JSON.stringify({ name: "Engineering", projectId: "proj-1" }));
|
||||
|
||||
const [, updateInit] = fetchMock.mock.calls[2] as [string, RequestInit];
|
||||
expect(updateInit.method).toBe("PATCH");
|
||||
expect(updateInit.body).toBe(JSON.stringify({ description: "desc" }));
|
||||
|
||||
const [, deleteInit] = fetchMock.mock.calls[3] as [string, RequestInit];
|
||||
expect(deleteInit.method).toBe("DELETE");
|
||||
});
|
||||
|
||||
it("builds member endpoints", async () => {
|
||||
const fetchMock = vi.spyOn(globalThis, "fetch").mockImplementation(async () => jsonResponse({ success: true }));
|
||||
|
||||
await fetchChatRoomMembers("room-1", "proj-2");
|
||||
await addChatRoomMember("room-1", { agentId: "agent-2", role: "owner" }, "proj-2");
|
||||
await removeChatRoomMember("room-1", "agent-2", "proj-2");
|
||||
|
||||
expect((fetchMock.mock.calls[0] as [string])[0]).toContain("/api/chat/rooms/room-1/members?projectId=proj-2");
|
||||
const [, addInit] = fetchMock.mock.calls[1] as [string, RequestInit];
|
||||
expect(addInit.method).toBe("POST");
|
||||
expect(addInit.body).toBe(JSON.stringify({ agentId: "agent-2", role: "owner" }));
|
||||
expect((fetchMock.mock.calls[2] as [string])[0]).toContain("/api/chat/rooms/room-1/members/agent-2?projectId=proj-2");
|
||||
});
|
||||
|
||||
it("builds message endpoints", async () => {
|
||||
const fetchMock = vi.spyOn(globalThis, "fetch").mockImplementation(async () => jsonResponse({ success: true }));
|
||||
|
||||
await fetchChatRoomMessages("room-1", { limit: 2, offset: 1, before: "2026-01-01" }, "proj-3");
|
||||
await postChatRoomMessage("room-1", { content: "hello", mentions: ["agent-x"] }, "proj-3");
|
||||
await deleteChatRoomMessage("room-1", "msg-1", "proj-3");
|
||||
|
||||
const [listUrl] = fetchMock.mock.calls[0] as [string, RequestInit];
|
||||
expect(listUrl).toContain("/api/chat/rooms/room-1/messages?");
|
||||
expect(listUrl).toContain("projectId=proj-3");
|
||||
expect(listUrl).toContain("limit=2");
|
||||
expect(listUrl).toContain("offset=1");
|
||||
expect(listUrl).toContain("before=2026-01-01");
|
||||
|
||||
const [, postInit] = fetchMock.mock.calls[1] as [string, RequestInit];
|
||||
expect(postInit.method).toBe("POST");
|
||||
expect(postInit.body).toBe(JSON.stringify({ content: "hello", mentions: ["agent-x"] }));
|
||||
|
||||
const [, delInit] = fetchMock.mock.calls[2] as [string, RequestInit];
|
||||
expect(delInit.method).toBe("DELETE");
|
||||
});
|
||||
});
|
||||
@@ -120,4 +120,23 @@ describe("streamChatResponse SSE parser", () => {
|
||||
expect(donePayloads).toEqual([{ messageId: "" }]);
|
||||
});
|
||||
});
|
||||
|
||||
it("fires onError when no stream events arrive before timeout", async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response(new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
controller.enqueue(new TextEncoder().encode(": connected\n\n"));
|
||||
},
|
||||
}), { status: 200 }));
|
||||
|
||||
const onError = vi.fn();
|
||||
streamChatResponse("s-1", "hi", { onError }, undefined, undefined, { firstEventTimeoutMs: 1_000 });
|
||||
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
await vi.advanceTimersByTimeAsync(1_100);
|
||||
|
||||
expect(onError).toHaveBeenCalledWith("Timed out waiting for first response event");
|
||||
vi.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type {
|
||||
Task,
|
||||
TaskDetail,
|
||||
TaskReviewData,
|
||||
TaskAttachment,
|
||||
TaskComment,
|
||||
TaskCreateInput,
|
||||
@@ -41,21 +42,12 @@ import type {
|
||||
AgentRating,
|
||||
AgentRatingSummary,
|
||||
AgentRatingInput,
|
||||
ChatAttachment,
|
||||
ChatMessage,
|
||||
ChatRoom,
|
||||
ChatRoomMember,
|
||||
ChatRoomMessage,
|
||||
EnrichedChatSession,
|
||||
Roadmap,
|
||||
RoadmapMilestone,
|
||||
RoadmapFeature,
|
||||
RoadmapCreateInput,
|
||||
RoadmapUpdateInput,
|
||||
RoadmapMilestoneCreateInput,
|
||||
RoadmapMilestoneUpdateInput,
|
||||
RoadmapFeatureCreateInput,
|
||||
RoadmapFeatureUpdateInput,
|
||||
RoadmapWithHierarchy,
|
||||
RoadmapExportBundle,
|
||||
RoadmapMissionPlanningHandoff,
|
||||
RoadmapFeatureTaskPlanningHandoff,
|
||||
TodoList,
|
||||
TodoItem,
|
||||
TodoListWithItems,
|
||||
@@ -68,6 +60,8 @@ import type {
|
||||
InsightStatus,
|
||||
InsightRun,
|
||||
InsightRunTrigger,
|
||||
EvalRun,
|
||||
EvalTaskResult,
|
||||
ResearchRunStatus,
|
||||
TaskPriority,
|
||||
TaskSourceIssue,
|
||||
@@ -78,6 +72,8 @@ import type {
|
||||
DockerVolumeMount,
|
||||
DockerExtraCli,
|
||||
DockerNodeStatus,
|
||||
ProjectNodePathMapping,
|
||||
ApprovalRequestStatus,
|
||||
} from "@fusion/core";
|
||||
import type { PlanningQuestion, PlanningSummary } from "@fusion/core";
|
||||
import type { ScheduledTask, ScheduledTaskCreateInput, ScheduledTaskUpdateInput, AutomationRunResult, Routine, RoutineCreateInput, RoutineUpdateInput, RoutineExecutionResult } from "@fusion/core";
|
||||
@@ -247,6 +243,40 @@ export async function fetchTaskDetail(id: string, projectId?: string): Promise<T
|
||||
throw new Error("Request failed");
|
||||
}
|
||||
|
||||
export interface UpdateTaskReviewRequest {
|
||||
reviewState: TaskDetail["reviewState"] | null;
|
||||
}
|
||||
|
||||
export interface TaskReviewResponse {
|
||||
reviewState: NonNullable<TaskDetail["reviewState"]>;
|
||||
automationStatus: string | null;
|
||||
emptyMessage?: string | null;
|
||||
prInfo?: TaskDetail["prInfo"];
|
||||
}
|
||||
|
||||
export interface RefreshTaskReviewResponse {
|
||||
reviewState: NonNullable<TaskDetail["reviewState"]>;
|
||||
automationStatus: string | null;
|
||||
prInfo?: TaskDetail["prInfo"];
|
||||
}
|
||||
|
||||
export interface SelectedReviewItem {
|
||||
id: string;
|
||||
source: "pr-review" | "reviewer-agent";
|
||||
threadId?: string;
|
||||
filePath?: string;
|
||||
lineNumber?: number;
|
||||
author?: string;
|
||||
summary: string;
|
||||
body: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export interface ReviseTaskReviewResponse {
|
||||
task: Task;
|
||||
reviewState: NonNullable<TaskDetail["reviewState"]>;
|
||||
}
|
||||
|
||||
export interface CreateTaskRequestOptions {
|
||||
transportNodeId?: string;
|
||||
localNodeId?: string;
|
||||
@@ -2341,9 +2371,18 @@ export function fetchRemote(remote?: string, projectId?: string): Promise<GitFet
|
||||
}
|
||||
|
||||
/** Pull current branch */
|
||||
export function pullBranch(projectId?: string): Promise<GitPullResult> {
|
||||
return api<GitPullResult>(withProjectId("/git/pull", projectId), {
|
||||
export function pullBranch(options?: { rebase?: boolean }, projectId?: string): Promise<GitPullResult>;
|
||||
export function pullBranch(projectId?: string): Promise<GitPullResult>;
|
||||
export function pullBranch(
|
||||
optionsOrProjectId?: { rebase?: boolean } | string,
|
||||
projectId?: string,
|
||||
): Promise<GitPullResult> {
|
||||
const options = typeof optionsOrProjectId === "string" ? undefined : optionsOrProjectId;
|
||||
const resolvedProjectId = typeof optionsOrProjectId === "string" ? optionsOrProjectId : projectId;
|
||||
|
||||
return api<GitPullResult>(withProjectId("/git/pull", resolvedProjectId), {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ rebase: options?.rebase ?? false }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2657,6 +2696,7 @@ export interface SubtaskItem {
|
||||
title: string;
|
||||
description: string;
|
||||
suggestedSize: "S" | "M" | "L";
|
||||
priority?: TaskPriority;
|
||||
dependsOn: string[];
|
||||
}
|
||||
|
||||
@@ -2684,7 +2724,13 @@ export interface AgentOnboardingSummary {
|
||||
patternAgentId?: string;
|
||||
rationale?: string;
|
||||
model?: string;
|
||||
/** Draft-only AI suggestion for eventual runtimeConfig.model selection. */
|
||||
modelHint?: string;
|
||||
/** Draft-only AI suggestion for eventual runtimeConfig.runtimeHint plugin runtime selection. */
|
||||
runtimeHint?: string;
|
||||
heartbeatProcedurePath?: string;
|
||||
heartbeatIntervalMs?: number;
|
||||
heartbeatEnabled?: boolean;
|
||||
}
|
||||
|
||||
export type OnboardingMode = "create" | "edit";
|
||||
@@ -2922,6 +2968,7 @@ export function createTasksFromPlanning(
|
||||
title: string;
|
||||
description: string;
|
||||
suggestedSize: "S" | "M" | "L";
|
||||
priority?: TaskPriority;
|
||||
dependsOn: string[];
|
||||
}>,
|
||||
projectId?: string,
|
||||
@@ -5073,6 +5120,66 @@ export function acceptTaskReview(taskId: string, projectId?: string): Promise<Ta
|
||||
});
|
||||
}
|
||||
|
||||
function mapTaskReviewDataToLegacy(data: TaskReviewData): TaskReviewResponse {
|
||||
return {
|
||||
reviewState: {
|
||||
source: data.mode,
|
||||
summary: data.summary ?? undefined,
|
||||
items: data.items.map((item) => ({
|
||||
id: item.itemId,
|
||||
body: item.body,
|
||||
author: { login: item.author },
|
||||
createdAt: item.createdAt ?? new Date(0).toISOString(),
|
||||
updatedAt: item.updatedAt ?? undefined,
|
||||
path: item.filePath,
|
||||
threadId: item.threadId,
|
||||
htmlUrl: item.url,
|
||||
state: item.reviewState ?? undefined,
|
||||
isResolved: item.isResolved,
|
||||
})),
|
||||
addressing: [],
|
||||
lastRefreshedAt: data.fetchedAt ?? undefined,
|
||||
refreshStatus: "ready",
|
||||
refreshSource: "initial-load",
|
||||
},
|
||||
automationStatus: null,
|
||||
};
|
||||
}
|
||||
|
||||
/** Fetch normalized task review data (PR mode or direct mode) */
|
||||
export async function fetchTaskReview(taskId: string, projectId?: string): Promise<TaskReviewResponse> {
|
||||
const data = await api<TaskReviewData>(withProjectId(`/tasks/${encodeURIComponent(taskId)}/review`, projectId));
|
||||
return mapTaskReviewDataToLegacy(data);
|
||||
}
|
||||
|
||||
/** Fetch canonical review payload for future review-tab rendering. */
|
||||
export function fetchTaskReviewData(taskId: string, projectId?: string): Promise<TaskReviewData> {
|
||||
return api<TaskReviewData>(withProjectId(`/tasks/${encodeURIComponent(taskId)}/review`, projectId));
|
||||
}
|
||||
|
||||
/** Refresh normalized task review data (PR mode or direct mode) */
|
||||
export async function refreshTaskReview(taskId: string, projectId?: string): Promise<RefreshTaskReviewResponse> {
|
||||
const data = await api<TaskReviewData>(withProjectId(`/tasks/${encodeURIComponent(taskId)}/review/refresh`, projectId), {
|
||||
method: "POST",
|
||||
});
|
||||
return mapTaskReviewDataToLegacy(data);
|
||||
}
|
||||
|
||||
/** Refresh canonical review payload for future review-tab rendering. */
|
||||
export function refreshTaskReviewData(taskId: string, projectId?: string): Promise<TaskReviewData> {
|
||||
return api<TaskReviewData>(withProjectId(`/tasks/${encodeURIComponent(taskId)}/review/refresh`, projectId), {
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
|
||||
/** Request an in-place revision pass for selected review items */
|
||||
export function reviseTaskReviewItems(taskId: string, selectedItems: SelectedReviewItem[], projectId?: string): Promise<ReviseTaskReviewResponse> {
|
||||
return api<ReviseTaskReviewResponse>(withProjectId(`/tasks/${encodeURIComponent(taskId)}/review/address`, projectId), {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ selectedItems, tab: "review" }),
|
||||
});
|
||||
}
|
||||
|
||||
/** Return task to agent - clear assignee and status, move to todo */
|
||||
export function returnTaskToAgent(taskId: string, projectId?: string): Promise<Task> {
|
||||
return api<Task>(withProjectId(`/tasks/${encodeURIComponent(taskId)}/return-to-agent`, projectId), {
|
||||
@@ -5531,6 +5638,34 @@ export interface NodeCreateInput {
|
||||
dockerConfig?: DockerNodeConfigInfo;
|
||||
}
|
||||
|
||||
/** Input for assigning a project path for a specific node during onboarding. */
|
||||
export interface NodeProjectMappingInput {
|
||||
projectId: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface RemoteNodeDiscoveredProject {
|
||||
id: string;
|
||||
name: string;
|
||||
path: string;
|
||||
status: "active" | "paused" | "errored" | "initializing";
|
||||
isolationMode: "in-process" | "child-process";
|
||||
}
|
||||
|
||||
export interface RemoteNodeProjectDiscoveryResult {
|
||||
projects: RemoteNodeDiscoveredProject[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Node onboarding payload used by dashboard UI.
|
||||
*
|
||||
* `projectMappings` is intentionally separate from `ProjectInfo.path` and `projects.nodeId`.
|
||||
* It captures node-specific filesystem paths for selected existing projects.
|
||||
*/
|
||||
export interface NodeOnboardingInput extends NodeCreateInput {
|
||||
projectMappings: NodeProjectMappingInput[];
|
||||
}
|
||||
|
||||
/** Input for updating an existing node */
|
||||
export type NodeUpdateInput = Partial<Pick<NodeCreateInput, "name" | "type" | "url" | "apiKey" | "maxConcurrent" | "dockerConfig">> & {
|
||||
status?: NodeStatus;
|
||||
@@ -5618,10 +5753,29 @@ export function fetchProjects(): Promise<ProjectInfo[]> {
|
||||
return api<ProjectInfo[]>("/projects");
|
||||
}
|
||||
|
||||
/** Project info with source node metadata (added by server for remote projects) */
|
||||
/** Dashboard-facing mapping contract for project availability on nodes. */
|
||||
export interface ProjectNodeAvailability {
|
||||
nodeId: string;
|
||||
nodeName?: string;
|
||||
path: string;
|
||||
available: boolean;
|
||||
}
|
||||
|
||||
/** Project info with source node metadata (added by server for remote projects). */
|
||||
export interface ProjectInfoWithSource extends ProjectInfo {
|
||||
/** Name of the source node (added by server for remote projects) */
|
||||
/** Name of the source node (added by server for remote projects). */
|
||||
_sourceNodeName?: string;
|
||||
/** Normalized per-node project mappings for dashboard UI. */
|
||||
nodeMappings?: ProjectNodeAvailability[];
|
||||
/** Compatibility fields accepted from in-flight server rollouts. */
|
||||
projectNodeMappings?: ProjectNodeAvailability[];
|
||||
pathMappings?: ProjectNodeAvailability[];
|
||||
}
|
||||
|
||||
export function hasNodeMappingsSupport(project: ProjectInfoWithSource): boolean {
|
||||
return Array.isArray(project.nodeMappings)
|
||||
|| Array.isArray(project.projectNodeMappings)
|
||||
|| Array.isArray(project.pathMappings);
|
||||
}
|
||||
|
||||
/** Fetch all registered projects from all nodes (local + remote) */
|
||||
@@ -5719,11 +5873,24 @@ export function registerNode(input: NodeCreateInput): Promise<NodeInfo> {
|
||||
});
|
||||
}
|
||||
|
||||
/** Discover projects from a remote node before registering it. */
|
||||
export function discoverRemoteNodeProjects(input: { url: string; apiKey?: string }): Promise<RemoteNodeProjectDiscoveryResult> {
|
||||
return api<RemoteNodeProjectDiscoveryResult>("/nodes/discover-projects", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
}
|
||||
|
||||
/** Fetch a single node by ID */
|
||||
export function fetchNode(id: string): Promise<NodeInfo> {
|
||||
return api<NodeInfo>(`/nodes/${encodeURIComponent(id)}`);
|
||||
}
|
||||
|
||||
/** Fetch all project path mappings for a node */
|
||||
export function fetchNodePathMappings(nodeId: string): Promise<ProjectNodePathMapping[]> {
|
||||
return api<ProjectNodePathMapping[]>(`/nodes/${encodeURIComponent(nodeId)}/path-mappings`);
|
||||
}
|
||||
|
||||
/** Update an existing node */
|
||||
export function updateNode(id: string, updates: NodeUpdateInput): Promise<NodeInfo> {
|
||||
return api<NodeInfo>(`/nodes/${encodeURIComponent(id)}`, {
|
||||
@@ -5838,6 +6005,43 @@ export function unregisterProject(id: string): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
/** Fetch all per-node path mappings for a project */
|
||||
export function fetchProjectPathMappings(projectId: string): Promise<ProjectNodePathMapping[]> {
|
||||
return api<ProjectNodePathMapping[]>(`/projects/${encodeURIComponent(projectId)}/path-mappings`);
|
||||
}
|
||||
|
||||
/** Fetch a single project-node path mapping */
|
||||
export function fetchProjectPathMapping(projectId: string, nodeId: string): Promise<ProjectNodePathMapping> {
|
||||
return api<ProjectNodePathMapping>(
|
||||
`/projects/${encodeURIComponent(projectId)}/path-mappings/${encodeURIComponent(nodeId)}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** Create or update a project-node path mapping */
|
||||
export function upsertProjectPathMapping(
|
||||
projectId: string,
|
||||
nodeId: string,
|
||||
path: string,
|
||||
): Promise<ProjectNodePathMapping> {
|
||||
return api<ProjectNodePathMapping>(
|
||||
`/projects/${encodeURIComponent(projectId)}/path-mappings/${encodeURIComponent(nodeId)}`,
|
||||
{
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ path }),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** Remove a project-node path mapping */
|
||||
export function removeProjectPathMapping(projectId: string, nodeId: string): Promise<void> {
|
||||
return api<void>(
|
||||
`/projects/${encodeURIComponent(projectId)}/path-mappings/${encodeURIComponent(nodeId)}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** Fetch health metrics for a specific project */
|
||||
export function fetchProjectHealth(id: string): Promise<ProjectHealth> {
|
||||
return api<ProjectHealth>(`/projects/${encodeURIComponent(id)}/health`);
|
||||
@@ -7132,223 +7336,6 @@ export async function previewEnrichedDescription(
|
||||
}
|
||||
}
|
||||
|
||||
// ── Roadmap API ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/** Fetch all roadmaps */
|
||||
export function fetchRoadmaps(projectId?: string): Promise<Roadmap[]> {
|
||||
return api<Roadmap[]>(withProjectId("/roadmaps", projectId));
|
||||
}
|
||||
|
||||
/** Create a new roadmap */
|
||||
export function createRoadmap(input: RoadmapCreateInput, projectId?: string): Promise<Roadmap> {
|
||||
return api<Roadmap>(withProjectId("/roadmaps", projectId), {
|
||||
method: "POST",
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
}
|
||||
|
||||
/** Fetch a single roadmap with full hierarchy (milestones and features) */
|
||||
export function fetchRoadmap(roadmapId: string, projectId?: string): Promise<RoadmapWithHierarchy> {
|
||||
return api<RoadmapWithHierarchy>(withProjectId(`/roadmaps/${encodeURIComponent(roadmapId)}`, projectId));
|
||||
}
|
||||
|
||||
/** Update roadmap metadata */
|
||||
export function updateRoadmap(roadmapId: string, updates: RoadmapUpdateInput, projectId?: string): Promise<Roadmap> {
|
||||
return api<Roadmap>(withProjectId(`/roadmaps/${encodeURIComponent(roadmapId)}`, projectId), {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(updates),
|
||||
});
|
||||
}
|
||||
|
||||
/** Delete a roadmap */
|
||||
export function deleteRoadmap(roadmapId: string, projectId?: string): Promise<void> {
|
||||
return api<void>(withProjectId(`/roadmaps/${encodeURIComponent(roadmapId)}`, projectId), {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
|
||||
/** Fetch milestones for a roadmap */
|
||||
export function fetchRoadmapMilestones(roadmapId: string, projectId?: string): Promise<RoadmapMilestone[]> {
|
||||
return api<RoadmapMilestone[]>(withProjectId(`/roadmaps/${encodeURIComponent(roadmapId)}/milestones`, projectId));
|
||||
}
|
||||
|
||||
/** Create a milestone in a roadmap */
|
||||
export function createRoadmapMilestone(roadmapId: string, input: RoadmapMilestoneCreateInput, projectId?: string): Promise<RoadmapMilestone> {
|
||||
return api<RoadmapMilestone>(withProjectId(`/roadmaps/${encodeURIComponent(roadmapId)}/milestones`, projectId), {
|
||||
method: "POST",
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
}
|
||||
|
||||
/** Update milestone metadata */
|
||||
export function updateRoadmapMilestone(milestoneId: string, updates: RoadmapMilestoneUpdateInput, projectId?: string): Promise<RoadmapMilestone> {
|
||||
return api<RoadmapMilestone>(withProjectId(`/roadmaps/milestones/${encodeURIComponent(milestoneId)}`, projectId), {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(updates),
|
||||
});
|
||||
}
|
||||
|
||||
/** Delete a milestone */
|
||||
export function deleteRoadmapMilestone(milestoneId: string, projectId?: string): Promise<void> {
|
||||
return api<void>(withProjectId(`/roadmaps/milestones/${encodeURIComponent(milestoneId)}`, projectId), {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
|
||||
/** Fetch features for a milestone */
|
||||
export function fetchRoadmapFeatures(milestoneId: string, projectId?: string): Promise<RoadmapFeature[]> {
|
||||
return api<RoadmapFeature[]>(withProjectId(`/roadmaps/milestones/${encodeURIComponent(milestoneId)}/features`, projectId));
|
||||
}
|
||||
|
||||
/** Create a feature in a milestone */
|
||||
export function createRoadmapFeature(milestoneId: string, input: RoadmapFeatureCreateInput, projectId?: string): Promise<RoadmapFeature> {
|
||||
return api<RoadmapFeature>(withProjectId(`/roadmaps/milestones/${encodeURIComponent(milestoneId)}/features`, projectId), {
|
||||
method: "POST",
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
}
|
||||
|
||||
/** Update feature metadata */
|
||||
export function updateRoadmapFeature(featureId: string, updates: RoadmapFeatureUpdateInput, projectId?: string): Promise<RoadmapFeature> {
|
||||
return api<RoadmapFeature>(withProjectId(`/roadmaps/features/${encodeURIComponent(featureId)}`, projectId), {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(updates),
|
||||
});
|
||||
}
|
||||
|
||||
/** Delete a feature */
|
||||
export function deleteRoadmapFeature(featureId: string, projectId?: string): Promise<void> {
|
||||
return api<void>(withProjectId(`/roadmaps/features/${encodeURIComponent(featureId)}`, projectId), {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
|
||||
/** Reorder milestones within a roadmap */
|
||||
export function reorderRoadmapMilestones(roadmapId: string, orderedMilestoneIds: string[], projectId?: string): Promise<void> {
|
||||
return api<void>(withProjectId(`/roadmaps/${encodeURIComponent(roadmapId)}/milestones/reorder`, projectId), {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ orderedMilestoneIds }),
|
||||
});
|
||||
}
|
||||
|
||||
/** Reorder features within a milestone */
|
||||
export function reorderRoadmapFeatures(milestoneId: string, orderedFeatureIds: string[], projectId?: string): Promise<void> {
|
||||
return api<void>(withProjectId(`/roadmaps/milestones/${encodeURIComponent(milestoneId)}/features/reorder`, projectId), {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ orderedFeatureIds }),
|
||||
});
|
||||
}
|
||||
|
||||
/** Move a feature to a different milestone or position */
|
||||
export function moveRoadmapFeature(
|
||||
featureId: string,
|
||||
targetMilestoneId: string,
|
||||
targetIndex: number,
|
||||
projectId?: string
|
||||
): Promise<void> {
|
||||
return api<void>(withProjectId(`/roadmaps/features/${encodeURIComponent(featureId)}/move`, projectId), {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ targetMilestoneId, targetIndex }),
|
||||
});
|
||||
}
|
||||
|
||||
/** Export a roadmap as a flat bundle for persistence/import/export */
|
||||
export function exportRoadmap(roadmapId: string, projectId?: string): Promise<RoadmapExportBundle> {
|
||||
return api<RoadmapExportBundle>(withProjectId(`/roadmaps/${encodeURIComponent(roadmapId)}/export`, projectId));
|
||||
}
|
||||
|
||||
/** Get mission planning handoff payload for a roadmap */
|
||||
export function getRoadmapMissionHandoff(roadmapId: string, projectId?: string): Promise<RoadmapMissionPlanningHandoff> {
|
||||
return api<RoadmapMissionPlanningHandoff>(withProjectId(`/roadmaps/${encodeURIComponent(roadmapId)}/handoff/mission`, projectId));
|
||||
}
|
||||
|
||||
/** Get task planning handoff payload for a single roadmap feature */
|
||||
export function getRoadmapFeatureHandoff(
|
||||
roadmapId: string,
|
||||
milestoneId: string,
|
||||
featureId: string,
|
||||
projectId?: string
|
||||
): Promise<RoadmapFeatureTaskPlanningHandoff> {
|
||||
return api<RoadmapFeatureTaskPlanningHandoff>(
|
||||
withProjectId(
|
||||
`/roadmaps/${encodeURIComponent(roadmapId)}/milestones/${encodeURIComponent(milestoneId)}/features/${encodeURIComponent(featureId)}/handoff/task`,
|
||||
projectId
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/** Combined handoff response type for roadmap handoff endpoint */
|
||||
export interface RoadmapHandoffResponse {
|
||||
mission: RoadmapMissionPlanningHandoff;
|
||||
features: RoadmapFeatureTaskPlanningHandoff[];
|
||||
}
|
||||
|
||||
/** Get both mission and feature handoff payloads for a roadmap */
|
||||
export function fetchRoadmapHandoff(roadmapId: string, projectId?: string): Promise<RoadmapHandoffResponse> {
|
||||
return api<RoadmapHandoffResponse>(withProjectId(`/roadmaps/${encodeURIComponent(roadmapId)}/handoff`, projectId));
|
||||
}
|
||||
|
||||
/** Response from milestone suggestion generation */
|
||||
export interface MilestoneSuggestionsResponse {
|
||||
suggestions: Array<{
|
||||
title: string;
|
||||
description?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
/** Generate milestone suggestions from a goal prompt */
|
||||
export function generateMilestoneSuggestions(
|
||||
roadmapId: string,
|
||||
goalPrompt: string,
|
||||
count?: number,
|
||||
projectId?: string
|
||||
): Promise<MilestoneSuggestionsResponse> {
|
||||
return api<MilestoneSuggestionsResponse>(
|
||||
withProjectId(`/roadmaps/${encodeURIComponent(roadmapId)}/suggestions/milestones`, projectId),
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
goalPrompt: goalPrompt.trim(),
|
||||
...(count !== undefined ? { count } : {}),
|
||||
}),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/** Response type for feature suggestions */
|
||||
export interface FeatureSuggestionsResponse {
|
||||
suggestions: Array<{
|
||||
title: string;
|
||||
description?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
/** Input for generating feature suggestions */
|
||||
export interface GenerateFeatureSuggestionsInput {
|
||||
/** Optional prompt to guide feature generation */
|
||||
prompt?: string;
|
||||
/** Number of features to generate (default 5, max 10) */
|
||||
count?: number;
|
||||
}
|
||||
|
||||
/** Generate feature suggestions for a milestone */
|
||||
export function generateFeatureSuggestions(
|
||||
milestoneId: string,
|
||||
input?: GenerateFeatureSuggestionsInput,
|
||||
projectId?: string
|
||||
): Promise<FeatureSuggestionsResponse> {
|
||||
return api<FeatureSuggestionsResponse>(
|
||||
withProjectId(`/roadmaps/milestones/${encodeURIComponent(milestoneId)}/suggestions/features`, projectId),
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
...(input?.prompt !== undefined ? { prompt: input.prompt.trim() } : {}),
|
||||
...(input?.count !== undefined ? { count: input.count } : {}),
|
||||
}),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// ── Todo API ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/** Fetch all todo lists with their items */
|
||||
@@ -7588,6 +7575,7 @@ export interface OutboxResponse {
|
||||
/** Response shape for GET /messages/unread-count */
|
||||
export interface UnreadCountResponse {
|
||||
unreadCount: number;
|
||||
pendingApprovalCount?: number;
|
||||
}
|
||||
|
||||
/** Response shape for POST /messages/read-all */
|
||||
@@ -7613,6 +7601,56 @@ export interface SendMessageInput {
|
||||
content: string;
|
||||
type: MessageType;
|
||||
metadata?: MessageMetadata;
|
||||
wakeImmediately?: boolean;
|
||||
}
|
||||
|
||||
export interface ApprovalRequestSummary {
|
||||
id: string;
|
||||
status: ApprovalRequestStatus;
|
||||
actionCategory: string;
|
||||
actionSummary: string;
|
||||
agentId: string;
|
||||
taskId?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
decidedAt?: string;
|
||||
decidedBy?: string;
|
||||
}
|
||||
|
||||
export interface ApprovalRequestDetail extends ApprovalRequestSummary {
|
||||
requester: {
|
||||
actorId: string;
|
||||
actorType: "agent" | "user" | "system";
|
||||
actorName: string;
|
||||
};
|
||||
runId?: string;
|
||||
requestedAt: string;
|
||||
completedAt?: string;
|
||||
targetAction: {
|
||||
category: string;
|
||||
action: string;
|
||||
summary: string;
|
||||
resourceType: string;
|
||||
resourceId: string;
|
||||
context?: Record<string, unknown>;
|
||||
};
|
||||
history: Array<{
|
||||
id: string;
|
||||
eventType: string;
|
||||
actor: {
|
||||
actorId: string;
|
||||
actorType: "agent" | "user" | "system";
|
||||
actorName: string;
|
||||
};
|
||||
note?: string;
|
||||
createdAt: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface ApprovalListResponse {
|
||||
requests: ApprovalRequestSummary[];
|
||||
total: number;
|
||||
pendingCount: number;
|
||||
}
|
||||
|
||||
/** Fetch inbox messages for the current user. */
|
||||
@@ -7698,6 +7736,34 @@ export function fetchAgentMailbox(agentId: string, projectId?: string): Promise<
|
||||
return api<AgentMailboxResponse>(withProjectId(`/agents/${encodeURIComponent(agentId)}/mailbox`, projectId));
|
||||
}
|
||||
|
||||
export function fetchApprovals(
|
||||
options?: { status?: ApprovalRequestStatus; limit?: number; offset?: number },
|
||||
projectId?: string,
|
||||
): Promise<ApprovalListResponse> {
|
||||
const params = new URLSearchParams();
|
||||
if (options?.status) params.set("status", options.status);
|
||||
if (options?.limit !== undefined) params.set("limit", String(options.limit));
|
||||
if (options?.offset !== undefined) params.set("offset", String(options.offset));
|
||||
if (projectId) params.set("projectId", projectId);
|
||||
const query = params.size > 0 ? `?${params.toString()}` : "";
|
||||
return api<ApprovalListResponse>(`/approvals${query}`);
|
||||
}
|
||||
|
||||
export function fetchApprovalDetail(id: string, projectId?: string): Promise<ApprovalRequestDetail> {
|
||||
return api<ApprovalRequestDetail>(withProjectId(`/approvals/${encodeURIComponent(id)}`, projectId));
|
||||
}
|
||||
|
||||
export function decideApproval(
|
||||
id: string,
|
||||
input: { decision: "approve" | "deny"; comment?: string },
|
||||
projectId?: string,
|
||||
): Promise<ApprovalRequestDetail> {
|
||||
return api<ApprovalRequestDetail>(withProjectId(`/approvals/${encodeURIComponent(id)}/decision`, projectId), {
|
||||
method: "POST",
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
}
|
||||
|
||||
/** Fetch reflection history for an agent. */
|
||||
export function fetchAgentReflections(agentId: string, limit?: number, projectId?: string): Promise<AgentReflection[]> {
|
||||
const params = new URLSearchParams();
|
||||
@@ -7843,8 +7909,13 @@ export async function updatePluginSettings(
|
||||
|
||||
export type PluginSetupStatusResponse =
|
||||
| { hasSetup: false }
|
||||
| { hasSetup: false; status: Extract<PluginSetupCheckResult, { status: "error" }> }
|
||||
| ({ hasSetup: true } & PluginSetupCheckResult);
|
||||
| ({ hasSetup: true } & PluginSetupCheckResult)
|
||||
| {
|
||||
hasSetup: true;
|
||||
setupCheckDeferred: true;
|
||||
deferredReason: "plugin-not-started";
|
||||
pluginState: PluginInstallation["state"];
|
||||
};
|
||||
|
||||
/** Fetch plugin setup status */
|
||||
export async function fetchPluginSetupStatus(id: string, projectId?: string): Promise<PluginSetupStatusResponse> {
|
||||
@@ -7985,6 +8056,27 @@ export interface ChatMessageListResponse {
|
||||
messages: ChatMessage[];
|
||||
}
|
||||
|
||||
export interface ChatRoomListResponse {
|
||||
rooms: ChatRoom[];
|
||||
}
|
||||
|
||||
export interface ChatRoomResponse {
|
||||
room: ChatRoom;
|
||||
members?: ChatRoomMember[];
|
||||
}
|
||||
|
||||
export interface ChatRoomMembersResponse {
|
||||
members: ChatRoomMember[];
|
||||
}
|
||||
|
||||
export interface ChatRoomMessageListResponse {
|
||||
messages: ChatRoomMessage[];
|
||||
}
|
||||
|
||||
export interface ChatRoomMessageResponse {
|
||||
message: ChatRoomMessage;
|
||||
}
|
||||
|
||||
/** Fetch all chat sessions for a project */
|
||||
export function fetchChatSessions(projectId?: string, status?: string): Promise<ChatSessionListResponse> {
|
||||
const search = new URLSearchParams();
|
||||
@@ -8098,6 +8190,114 @@ export function deleteChatMessage(
|
||||
);
|
||||
}
|
||||
|
||||
export function fetchChatRooms(
|
||||
options: { status?: string; agentId?: string } = {},
|
||||
projectId?: string,
|
||||
): Promise<ChatRoomListResponse> {
|
||||
const search = new URLSearchParams();
|
||||
if (projectId) search.set("projectId", projectId);
|
||||
if (options.status) search.set("status", options.status);
|
||||
if (options.agentId) search.set("agentId", options.agentId);
|
||||
const qs = search.toString();
|
||||
return api<ChatRoomListResponse>(`/chat/rooms${qs ? `?${qs}` : ""}`);
|
||||
}
|
||||
|
||||
export function fetchChatRoom(id: string, projectId?: string): Promise<ChatRoomResponse> {
|
||||
return api<ChatRoomResponse>(withProjectId(`/chat/rooms/${encodeURIComponent(id)}`, projectId));
|
||||
}
|
||||
|
||||
export function createChatRoom(
|
||||
input: { name: string; description?: string | null; createdBy?: string | null; memberAgentIds?: string[] },
|
||||
projectId?: string,
|
||||
): Promise<ChatRoomResponse> {
|
||||
const body = { ...input, ...(projectId ? { projectId } : {}) };
|
||||
return api<ChatRoomResponse>(withProjectId("/chat/rooms", projectId), {
|
||||
method: "POST",
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
}
|
||||
|
||||
export function updateChatRoom(
|
||||
id: string,
|
||||
updates: { name?: string; description?: string | null; status?: "active" | "archived" },
|
||||
projectId?: string,
|
||||
): Promise<{ room: ChatRoom }> {
|
||||
return api<{ room: ChatRoom }>(withProjectId(`/chat/rooms/${encodeURIComponent(id)}`, projectId), {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(updates),
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteChatRoom(id: string, projectId?: string): Promise<{ success: boolean }> {
|
||||
return api<{ success: boolean }>(withProjectId(`/chat/rooms/${encodeURIComponent(id)}`, projectId), {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
|
||||
export function fetchChatRoomMembers(id: string, projectId?: string): Promise<ChatRoomMembersResponse> {
|
||||
return api<ChatRoomMembersResponse>(withProjectId(`/chat/rooms/${encodeURIComponent(id)}/members`, projectId));
|
||||
}
|
||||
|
||||
export function addChatRoomMember(
|
||||
id: string,
|
||||
input: { agentId: string; role?: "owner" | "member" },
|
||||
projectId?: string,
|
||||
): Promise<{ member: ChatRoomMember }> {
|
||||
return api<{ member: ChatRoomMember }>(withProjectId(`/chat/rooms/${encodeURIComponent(id)}/members`, projectId), {
|
||||
method: "POST",
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
}
|
||||
|
||||
export function removeChatRoomMember(id: string, agentId: string, projectId?: string): Promise<{ success: boolean }> {
|
||||
return api<{ success: boolean }>(
|
||||
withProjectId(`/chat/rooms/${encodeURIComponent(id)}/members/${encodeURIComponent(agentId)}`, projectId),
|
||||
{ method: "DELETE" },
|
||||
);
|
||||
}
|
||||
|
||||
export function fetchChatRoomMessages(
|
||||
id: string,
|
||||
opts?: { limit?: number; offset?: number; before?: string },
|
||||
projectId?: string,
|
||||
): Promise<ChatRoomMessageListResponse> {
|
||||
const search = new URLSearchParams();
|
||||
if (opts?.limit !== undefined) search.set("limit", String(opts.limit));
|
||||
if (opts?.offset !== undefined) search.set("offset", String(opts.offset));
|
||||
if (opts?.before) search.set("before", opts.before);
|
||||
const qs = search.toString();
|
||||
return api<ChatRoomMessageListResponse>(
|
||||
withProjectId(`/chat/rooms/${encodeURIComponent(id)}/messages${qs ? `?${qs}` : ""}`, projectId),
|
||||
);
|
||||
}
|
||||
|
||||
export function postChatRoomMessage(
|
||||
id: string,
|
||||
input: { content: string; senderAgentId?: null; mentions?: string[]; attachments?: File[] | ChatAttachment[] },
|
||||
projectId?: string,
|
||||
): Promise<ChatRoomMessageResponse> {
|
||||
return api<ChatRoomMessageResponse>(withProjectId(`/chat/rooms/${encodeURIComponent(id)}/messages`, projectId), {
|
||||
method: "POST",
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteChatRoomMessage(
|
||||
id: string,
|
||||
messageId: string,
|
||||
projectId?: string,
|
||||
): Promise<{ success: boolean }> {
|
||||
return api<{ success: boolean }>(
|
||||
withProjectId(`/chat/rooms/${encodeURIComponent(id)}/messages/${encodeURIComponent(messageId)}`, projectId),
|
||||
{ method: "DELETE" },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Room POST /messages in FN-3808 is persist-only (201 JSON response).
|
||||
* Do not add streamChatRoomResponse until FN-3810 introduces AI invocation/streaming.
|
||||
*/
|
||||
|
||||
/** Cancel an in-flight chat generation. */
|
||||
export function cancelChatResponse(
|
||||
sessionId: string,
|
||||
@@ -8136,21 +8336,39 @@ export function streamChatResponse(
|
||||
},
|
||||
attachments?: File[],
|
||||
projectId?: string,
|
||||
options?: { maxReconnectAttempts?: number },
|
||||
options?: { maxReconnectAttempts?: number; firstEventTimeoutMs?: number },
|
||||
): { close: () => void; isConnected: () => boolean } {
|
||||
const url = buildApiUrl(withProjectId(`/chat/sessions/${encodeURIComponent(sessionId)}/messages`, projectId));
|
||||
|
||||
void options;
|
||||
|
||||
const abortController = new AbortController();
|
||||
let closedByUser = false;
|
||||
let terminated = false;
|
||||
let receivedStreamEvent = false;
|
||||
const firstEventTimeoutMs = Math.max(1_000, options?.firstEventTimeoutMs ?? 60_000);
|
||||
let firstEventTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
const clearFirstEventTimer = (): void => {
|
||||
if (firstEventTimer) {
|
||||
clearTimeout(firstEventTimer);
|
||||
firstEventTimer = null;
|
||||
}
|
||||
};
|
||||
|
||||
const markFirstEventReceived = (): void => {
|
||||
if (receivedStreamEvent) {
|
||||
return;
|
||||
}
|
||||
receivedStreamEvent = true;
|
||||
clearFirstEventTimer();
|
||||
};
|
||||
|
||||
const dispatchEvent = (eventName: string, rawData: string): void => {
|
||||
if (!eventName) {
|
||||
return;
|
||||
}
|
||||
|
||||
markFirstEventReceived();
|
||||
|
||||
switch (eventName) {
|
||||
case "thinking":
|
||||
try {
|
||||
@@ -8248,6 +8466,14 @@ export function streamChatResponse(
|
||||
}
|
||||
|
||||
handlers.onConnectionStateChange?.("connected");
|
||||
firstEventTimer = setTimeout(() => {
|
||||
if (terminated || closedByUser || receivedStreamEvent) {
|
||||
return;
|
||||
}
|
||||
terminated = true;
|
||||
handlers.onError?.("Timed out waiting for first response event");
|
||||
abortController.abort();
|
||||
}, firstEventTimeoutMs);
|
||||
|
||||
const reader = res.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
@@ -8319,14 +8545,20 @@ export function streamChatResponse(
|
||||
if (!terminated && !closedByUser && !hasUndispatchedTrailingFragment) {
|
||||
handlers.onError?.("Connection closed unexpectedly");
|
||||
}
|
||||
clearFirstEventTimer();
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof DOMException && err.name === "AbortError") {
|
||||
if (!closedByUser) {
|
||||
if (!closedByUser && !terminated) {
|
||||
handlers.onError?.("Connection aborted");
|
||||
}
|
||||
clearFirstEventTimer();
|
||||
return;
|
||||
}
|
||||
if (closedByUser) return;
|
||||
if (closedByUser) {
|
||||
clearFirstEventTimer();
|
||||
return;
|
||||
}
|
||||
clearFirstEventTimer();
|
||||
handlers.onError?.(err instanceof Error ? err.message : "Connection error");
|
||||
}
|
||||
})();
|
||||
@@ -8334,6 +8566,7 @@ export function streamChatResponse(
|
||||
return {
|
||||
close: () => {
|
||||
closedByUser = true;
|
||||
clearFirstEventTimer();
|
||||
abortController.abort();
|
||||
},
|
||||
isConnected: () => !closedByUser,
|
||||
@@ -8489,6 +8722,35 @@ export function getInsightCreateTaskData(
|
||||
|
||||
// ── Research API ────────────────────────────────────────────────────────────
|
||||
|
||||
export interface EvalsListOptions {
|
||||
q?: string;
|
||||
runId?: string;
|
||||
scoreMin?: number;
|
||||
scoreMax?: number;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
export function listEvals(options: EvalsListOptions = {}, projectId?: string): Promise<{ results: EvalTaskResult[]; count: number }> {
|
||||
const params = new URLSearchParams();
|
||||
if (options.q) params.set("q", options.q);
|
||||
if (options.runId) params.set("runId", options.runId);
|
||||
if (options.scoreMin !== undefined) params.set("scoreMin", String(options.scoreMin));
|
||||
if (options.scoreMax !== undefined) params.set("scoreMax", String(options.scoreMax));
|
||||
if (options.limit !== undefined) params.set("limit", String(options.limit));
|
||||
if (options.offset !== undefined) params.set("offset", String(options.offset));
|
||||
const suffix = params.size > 0 ? `?${params.toString()}` : "";
|
||||
return api<{ results: EvalTaskResult[]; count: number }>(withProjectId(`/evals${suffix}`, projectId));
|
||||
}
|
||||
|
||||
export function getEval(id: string, projectId?: string): Promise<{ result: EvalTaskResult }> {
|
||||
return api<{ result: EvalTaskResult }>(withProjectId(`/evals/${encodeURIComponent(id)}`, projectId));
|
||||
}
|
||||
|
||||
export function listEvalRuns(projectId?: string): Promise<{ runs: EvalRun[] }> {
|
||||
return api<{ runs: EvalRun[] }>(withProjectId("/evals/runs", projectId));
|
||||
}
|
||||
|
||||
export interface CreateResearchRunInput {
|
||||
query: string;
|
||||
providers: ResearchProviderOption[];
|
||||
|
||||
Reference in New Issue
Block a user