Files
fusion/packages/dashboard/app/api/report.ts
gsxdsm a11f789b0c FN-8327: fall back to issues when Discussions are disabled
Reports can select a Discussion category and reliably file to Issues when Discussions are unavailable.

- Add Discussion-disabled detection across GitHub GraphQL operations.
- Let report submission select a category and disclose the actual filing destination.
- Preserve duplicate handling and document the fallback behavior.

Files changed:
 .changeset/fn-8327-discussion-fallback.md          |  7 +++
 docs/dashboard-guide.md                            |  4 +-
 packages/dashboard/app/api/report.ts               | 22 +++++--
 packages/dashboard/app/components/ReportModal.tsx  |  7 ++-
 .../app/components/__tests__/ReportModal.test.tsx  | 13 ++++
 .../src/__tests__/github-discussions.test.ts       | 29 +++++++--
 .../src/__tests__/report-pipeline.test.ts          | 63 ++++++++++++++-----
 .../dashboard/src/__tests__/report-routes.test.ts  | 15 +++--
 packages/dashboard/src/github.ts                   | 71 ++++++++++++++++------
 packages/dashboard/src/report-pipeline.ts          | 54 +++++++++++-----
 .../dashboard/src/routes/register-report-routes.ts | 12 +++-
 11 files changed, 227 insertions(+), 70 deletions(-)

Fusion-Task-Id: FN-8327

Fusion-Task-Lineage: 510d3d4f-ffc9-4167-bee8-7043e0ddbd9c

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-19 20:42:05 -07:00

40 lines
2.9 KiB
TypeScript

import type { ReportActionType, ReportTarget } from "@fusion/core";
async function post<T>(path: string, body: unknown): Promise<T> {
const response = await fetch(path, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) });
if (!response.ok) throw new Error((await response.json().catch(() => ({ error: response.statusText }))).error ?? response.statusText);
return response.json() as Promise<T>;
}
/** Filed report responses expose the actual GitHub destination after fallback. */
export interface FiledReportResponse { kind: "filed"; url: string; destination: "issue" | "discussion"; }
export interface ReportResponse {
kind: string;
destination?: "issue" | "discussion";
url?: string;
message?: string;
report?: { userPrompt: string; sourcePrompt?: string; summary?: string; body?: string; context?: Record<string, unknown>; sessionToken?: string };
issue?: { number: number; url: string; title: string; discussionId?: string; roadmap?: true };
answer?: { summary?: string; content?: string };
}
export interface ReportContextInput { actionType: ReportActionType; userPrompt: string; contextRefs?: { taskId?: string; agentId?: string }; activityTrace?: string[]; screenshotArtifactId?: string; }
export function reportDraft(input: ReportContextInput & { targetType?: ReportTarget; discussionCategoryId?: string }) { return post<ReportResponse>("/api/report/draft", input); }
export function reportFile(input: { actionType: ReportActionType; report: unknown; targetType?: ReportTarget; discussionCategoryId?: string; endorseIssueNumber?: number; endorseDiscussionId?: string; endorseRoadmapIssueNumber?: number; activityTrace?: string[]; screenshotArtifactId?: string }) { return post<ReportResponse>("/api/report/file", input); }
export function reportHelp(question: string) { return post<{ answered?: boolean; answer?: { summary?: string; content?: string } }>("/api/report/help", { question }); }
/** Upload is intentionally multipart: screenshot bytes never join JSON report text. */
export async function reportAttachment(screenshot: Blob): Promise<{ artifactId: string }> {
const form = new FormData(); form.append("screenshot", screenshot, "report-screenshot.png");
const response = await fetch("/api/report/attachment", { method: "POST", body: form });
if (!response.ok) throw new Error((await response.json().catch(() => ({ error: response.statusText }))).error ?? response.statusText);
return response.json() as Promise<{ artifactId: string }>;
}
export interface DiscussionCategoryOption { id: string; name: string; slug: string; }
export async function listDiscussionCategories(): Promise<{ categories: DiscussionCategoryOption[]; reason?: string }> {
const response = await fetch("/api/report/discussion-categories");
if (!response.ok) throw new Error((await response.json().catch(() => ({ error: response.statusText }))).error ?? response.statusText);
return response.json();
}