diff --git a/.github/.gitleaks.toml b/.github/.gitleaks.toml index 8eeef52349..3087a5f1d4 100644 --- a/.github/.gitleaks.toml +++ b/.github/.gitleaks.toml @@ -172,11 +172,16 @@ title = "gitleaks config" files = ['''^\.?gitleaks.toml$''', '''(.*?)(png|jpg|gif|doc|docx|pdf|bin|xls|pyc|zip)$''', '''(go.mod|go.sum)$'''] + # Ignore false positives on the public company URL (rule matches brand name + nearby alnum). + regexes = [ + '''(?i)linkedin\.com/company/agent-orchestrator''', + ] # Historical false positives from the ReverbCode rewrite graft (#2166). # All offenders are example secret-patterns in security docs and # secret-redaction test fixtures, living in files already deleted from # the tree. The push-event scan walks full history, so these commits are # allowlisted to keep `main` green without weakening detection on new code. + # Also includes settings-revamp commits that hit LinkedIn false positives. commits = [ "15ad44633a34151e81737183e6f35889a38ffbcb", "1efa966296178deaf161853f5bc1abf595e22223", @@ -188,4 +193,10 @@ title = "gitleaks config" "cb2fc088a83b394fc128235f4106bbfc88d8ede8", "f56338cbabd2185cab706ae09e1471b0bd9b7b43", "fc7d76ad54d6a7350de0fca298a1294ce8a1f3a2", + "c4c38ba20fc311ad2c89b7016301cabb08c3a4b9", + "45994b6d169d1004a02205b3a7ad23a4b1304ed3", + "73613281ab149c2dd4186fe83c4687c5464150fa", + "2a0c8a0f3e27ae78612b9896f07e0a79b181a431", + "d1d4092df1adfe6f9581b03484845485dfb91b76", + "423f0de6fa12d55785f2cc254e033edb94773a84", ] diff --git a/frontend/e2e/multi-pr.spec.ts b/frontend/e2e/multi-pr.spec.ts index de8eeac809..7d5891b194 100644 --- a/frontend/e2e/multi-pr.spec.ts +++ b/frontend/e2e/multi-pr.spec.ts @@ -3,7 +3,7 @@ import { expect, test } from "@playwright/test"; // dev:web (VITE_NO_ELECTRON=1) serves lib/mock-data.ts. The api-gateway // workspace owns a "stacked-auth" session ("auth stack") carrying three PRs: // #41 open, #42 draft, #40 merged — the multi-PR-per-session case this suite -// guards across the inspector rail and the PR board. +// guards across the inspector rail. test("the inspector rail stacks every PR a session owns, actionable-first", async ({ page }) => { await page.goto("/"); @@ -22,22 +22,3 @@ test("the inspector rail stacks every PR a session owns, actionable-first", asyn const cards = prSection.locator("text=/^PR #\\d+$/"); await expect(cards).toHaveText(["PR #41", "PR #42", "PR #40"]); }); - -test("the PR board lists one row per attributed PR, actionable PRs first", async ({ page }) => { - await page.goto("/#/prs"); - - await expect(page.getByRole("heading", { name: "Pull requests" })).toBeVisible(); - - // stacked-auth's three PRs keep actionable-first order across the whole board: - // open #41 before draft #42, and the lone merged PR (#40) sinks to the bottom. - const numbers = await page.locator("tbody tr td:first-child").allTextContents(); - expect(numbers.indexOf("#41")).toBeLessThan(numbers.indexOf("#42")); - expect(numbers.indexOf("#42")).toBeLessThan(numbers.indexOf("#40")); - expect(numbers.indexOf("#40")).toBe(numbers.length - 1); - - // Open/draft rows are actionable; the merged row is not. - const mergedRow = page.locator("tbody tr", { hasText: "#40" }); - await expect(mergedRow.getByRole("button", { name: "Merge" })).toHaveCount(0); - const openRow = page.locator("tbody tr", { hasText: "#41" }); - await expect(openRow.getByRole("button", { name: "Merge" })).toBeVisible(); -}); diff --git a/frontend/src/landing/content/docs/dashboard.mdx b/frontend/src/landing/content/docs/dashboard.mdx index 59b6304996..6a9098cbf4 100644 --- a/frontend/src/landing/content/docs/dashboard.mdx +++ b/frontend/src/landing/content/docs/dashboard.mdx @@ -54,18 +54,9 @@ Keep this change limited to the API route. Do not refactor the shared client. ## Review PRs -Open `/prs` to see every PR created by AO-managed sessions. +PR status lives on the session board and session detail pages. Cards move into **Review** or **Merge** when a linked PR needs attention, and the session detail view shows size, CI state, review decision, and unresolved threads. -The PR page lets you filter by: - -| Tab | Shows | -| ---------- | ---------------------------- | -| **All** | Open, merged, and closed PRs | -| **Open** | PRs still in progress | -| **Merged** | Completed PRs | -| **Closed** | Closed but unmerged PRs | - -Each PR shows its size, CI state, review decision, and unresolved threads. Use this page when you want a repository-level review queue instead of a session-by-session view. +Use the board when you want a queue of sessions that need review; open a session when you need the full PR context. ## Manage Projects @@ -94,7 +85,7 @@ The terminal reconnects automatically when the WebSocket drops. If terminal outp On smaller screens, the dashboard switches to a mobile layout: -- A bottom tab bar links to Dashboard, PRs, and Orchestrator. +- A bottom tab bar links to Dashboard and Orchestrator. - Sessions are ordered by urgency: Respond, Merge, Review, Pending, Working. - Tapping a card opens a bottom sheet with quick actions. - The terminal opens in a compact full-screen style. @@ -124,7 +115,7 @@ The favicon and document title also change when sessions need attention, so you | `/` | Project overview or current project board | | `/projects/{projectId}` | One project's board | | `/projects/{projectId}/settings` | Project behavior settings | -| `/prs` | PR review queue | +| `/settings` | Global app settings | | `/sessions/{id}` | Session detail and terminal | | `/projects/{projectId}/sessions/{id}` | Project-scoped session detail | | `/orchestrators?project={projectId}` | Pick or start an orchestrator session | diff --git a/frontend/src/renderer/__tests__/integration/pr-hydration.test.tsx b/frontend/src/renderer/__tests__/integration/pr-hydration.test.tsx index 88b7088cbd..fe49ac4807 100644 --- a/frontend/src/renderer/__tests__/integration/pr-hydration.test.tsx +++ b/frontend/src/renderer/__tests__/integration/pr-hydration.test.tsx @@ -3,10 +3,9 @@ import { render, screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import type { ReactNode } from "react"; -// Drives the real useWorkspaceQuery + real Board / PR-page consumers end to end -// for a normal project, mocking only the HTTP client and the router. Proves PR -// facts carried on the session list flow through the shared workspace cache into -// every consumer. +// Drives the real useWorkspaceQuery + SessionsBoard end to end for a normal +// project, mocking only the HTTP client and the router. Proves PR facts carried +// on the session list flow through the shared workspace cache into the board. const { getMock, navigateMock } = vi.hoisted(() => ({ getMock: vi.fn(), navigateMock: vi.fn() })); vi.mock("../../lib/api-client", () => ({ @@ -21,7 +20,6 @@ vi.mock("@tanstack/react-router", async (importOriginal) => { }); import { SessionsBoard } from "../../components/SessionsBoard"; -import { PullRequestsPage } from "../../components/PullRequestsPage"; // One ordinary project with one worker session that has multiple PRs. function respondWithProjectAndPRs() { @@ -258,16 +256,4 @@ describe("PR hydration for a normal project (#251)", () => { expect(screen.queryByText("changes requested")).not.toBeInTheDocument(); expect(screen.queryByRole("link", { name: "conflicts" })).not.toBeInTheDocument(); }); - - it("lists every session PR on the PR page instead of being empty", async () => { - renderWithProviders(); - - expect(await screen.findByText("#278")).toBeInTheDocument(); - expect(screen.getByText("#279")).toBeInTheDocument(); - expect(screen.getByText("#280")).toBeInTheDocument(); - expect(screen.getByText("#281")).toBeInTheDocument(); - expect(screen.getByText("#282")).toBeInTheDocument(); - expect(screen.queryByText("No open pull requests.")).not.toBeInTheDocument(); - expect(screen.getAllByText("fix the bug")).toHaveLength(5); - }); }); diff --git a/frontend/src/renderer/components/ConfirmDialog.tsx b/frontend/src/renderer/components/ConfirmDialog.tsx index 99467da2e7..553b99acc5 100644 --- a/frontend/src/renderer/components/ConfirmDialog.tsx +++ b/frontend/src/renderer/components/ConfirmDialog.tsx @@ -30,7 +30,7 @@ export function ConfirmDialog({ return ( - +
diff --git a/frontend/src/renderer/components/ConnectMobileModal.tsx b/frontend/src/renderer/components/ConnectMobileModal.tsx index 7be7de0f68..9e8a7f1cf9 100644 --- a/frontend/src/renderer/components/ConnectMobileModal.tsx +++ b/frontend/src/renderer/components/ConnectMobileModal.tsx @@ -1,14 +1,17 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { useState } from "react"; -import { Loader2 } from "lucide-react"; +import { useEffect, useRef, useState } from "react"; +import { Check, Copy, Info, Loader2, X } from "lucide-react"; import { QRCodeSVG } from "qrcode.react"; import { apiClient, apiErrorMessage } from "../lib/api-client"; -import { Button } from "./ui/button"; -import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "./ui/dialog"; +import { cn } from "../lib/utils"; +import { Dialog, DialogClose, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "./ui/dialog"; import { Switch } from "./ui/switch"; export const mobileStatusQueryKey = ["mobile-status"] as const; +/** Matches `--size-settings-mobile-qr-code`; qrcode.react needs a px number. */ +const QR_CODE_SIZE = 204; + interface MobileStatus { enabled: boolean; host: string; @@ -45,6 +48,13 @@ interface ConnectMobileModalProps { export function ConnectMobileModal({ open, onOpenChange }: ConnectMobileModalProps) { const queryClient = useQueryClient(); const [copied, setCopied] = useState(false); + const copiedTimeoutRef = useRef | null>(null); + + useEffect(() => { + return () => { + if (copiedTimeoutRef.current) clearTimeout(copiedTimeoutRef.current); + }; + }, []); const query = useQuery({ queryKey: mobileStatusQueryKey, @@ -87,15 +97,27 @@ export function ConnectMobileModal({ open, onOpenChange }: ConnectMobileModalPro const enabled = status?.enabled ?? false; const busy = enable.isPending || disable.isPending || regenerate.isPending; + const clearActionErrors = () => { + enable.reset(); + disable.reset(); + regenerate.reset(); + }; + const copyPassword = async () => { if (!status?.password) return; - await navigator.clipboard.writeText(status.password); - setCopied(true); - setTimeout(() => setCopied(false), 1500); + try { + await navigator.clipboard.writeText(status.password); + setCopied(true); + if (copiedTimeoutRef.current) clearTimeout(copiedTimeoutRef.current); + copiedTimeoutRef.current = setTimeout(() => setCopied(false), 1500); + } catch { + // Clipboard can reject (permissions / non-secure context). + } }; const onToggle = (next: boolean) => { if (busy) return; + clearActionErrors(); if (next) enable.mutate(); else disable.mutate(); }; @@ -108,85 +130,155 @@ export function ConnectMobileModal({ open, onOpenChange }: ConnectMobileModalPro return ( - - - Connect Mobile - Pair the Agent Orchestrator mobile app with this desktop over your LAN. - - - {query.isLoading ? ( -

Checking status…

- ) : query.isError ? ( -

- {query.error instanceof Error ? query.error.message : "Failed to load mobile status."} -

- ) : status ? ( -
- {/* Toggle row — always visible. Flipping it starts/stops the bridge. */} -
-
- Enable mobile - - Open a password-protected port on your local network so your phone can connect. - -
-
- {busy && } - -
-
- - {actionError &&

{actionError}

} + + + + +
+ + + Connect Mobile + + + Pair the Agent Orchestrator mobile app with this desktop over your LAN. + + - {/* Pairing details — revealed below the toggle only when enabled. */} - {enabled && ( -
-
- + {query.isLoading ? ( +

Checking status…

+ ) : query.isError ? ( +

+ {query.error instanceof Error ? query.error.message : "Failed to load mobile status."} +

+ ) : status ? ( +
+ {/* Toggle row — always visible. Flipping it starts/stops the bridge. */} +
+
+ + Enable mobile + + + Open a password-protected port on your local network so your phone can connect. +
- -
- - - {status.host}:{status.port} - - - -
- {status.password} - -
-
+
+ {busy &&
+
- {status.warning && ( -

- {status.warning} -

+ {actionError &&

{actionError}

} + + {/* Pairing details — expand/collapse with the enable toggle. */} +
+
+
+
+
+ +
+

Scan to pair

+
+ + {status.warning && ( +

+

+ )} + +
+
+ Address + + {status.host}:{status.port} + +
+
+ Password +
+ {status.password} + +
+
+
-
- + +
- )} -
- ) : null} +
+ ) : null} +
); } - -function Row({ label, children }: { label: string; children: React.ReactNode }) { - return ( -
- {label} - {children} -
- ); -} diff --git a/frontend/src/renderer/components/CreateProjectAgentSheet.tsx b/frontend/src/renderer/components/CreateProjectAgentSheet.tsx index f49431f7f3..45c25fafa9 100644 --- a/frontend/src/renderer/components/CreateProjectAgentSheet.tsx +++ b/frontend/src/renderer/components/CreateProjectAgentSheet.tsx @@ -148,7 +148,7 @@ export function CreateProjectAgentSheet({ return ( !isBusy && onOpenChange(next)}> - +
diff --git a/frontend/src/renderer/components/CreateProjectFlow.tsx b/frontend/src/renderer/components/CreateProjectFlow.tsx index ccacbb92ef..5740c09270 100644 --- a/frontend/src/renderer/components/CreateProjectFlow.tsx +++ b/frontend/src/renderer/components/CreateProjectFlow.tsx @@ -252,7 +252,7 @@ function CreateProjectModeDialog({ return ( - +
@@ -378,7 +378,7 @@ function CreateProjectFolderDialog({ return ( - +
- } - /> -
-
+ <> + + navigate({ to: "/" })}> + setMobileOpen(true)} /> - -
-
-
+ + setFeedbackOpen(true)} /> + + {/* Connect with us — temporarily disabled + +
+ {SETTINGS_SOCIAL_LINKS.map(({ id, label, href }) => { + const Icon = SOCIAL_ICONS[id]; + return ( + { + event.preventDefault(); + void aoBridge.app.openExternal(href); + }} + className="inline-flex items-center gap-2.5 text-settings-label" + > + + + {label} + + ); + })} +
+
+ */} + + + + + ); } + +// function LinkedInIcon({ className, ...props }: SVGProps) { +// return ( +// +// +// +// ); +// } +// +// function XSocialIcon({ className, ...props }: SVGProps) { +// return ( +// +// +// +// ); +// } diff --git a/frontend/src/renderer/components/NewTaskDialog.tsx b/frontend/src/renderer/components/NewTaskDialog.tsx index a88d7b4154..94c0514648 100644 --- a/frontend/src/renderer/components/NewTaskDialog.tsx +++ b/frontend/src/renderer/components/NewTaskDialog.tsx @@ -119,7 +119,7 @@ export function NewTaskDialog({ open, projectId, onCreated, onOpenChange }: NewT return ( - +
diff --git a/frontend/src/renderer/components/OrchestratorReplacementDialog.tsx b/frontend/src/renderer/components/OrchestratorReplacementDialog.tsx index 8bfedf0b54..a67a1f1292 100644 --- a/frontend/src/renderer/components/OrchestratorReplacementDialog.tsx +++ b/frontend/src/renderer/components/OrchestratorReplacementDialog.tsx @@ -35,7 +35,7 @@ export function OrchestratorReplacementDialog({ return ( - +
diff --git a/frontend/src/renderer/components/PullRequestsPage.test.tsx b/frontend/src/renderer/components/PullRequestsPage.test.tsx deleted file mode 100644 index 90a770bb63..0000000000 --- a/frontend/src/renderer/components/PullRequestsPage.test.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { render, screen, waitFor, within } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { PullRequestsPage } from "./PullRequestsPage"; -import type { PRState, PullRequestFacts, WorkspaceSession, WorkspaceSummary } from "../types/workspace"; - -const { navigateMock, postMock, useWorkspaceQueryMock } = vi.hoisted(() => ({ - navigateMock: vi.fn(), - postMock: vi.fn(), - useWorkspaceQueryMock: vi.fn(), -})); - -vi.mock("@tanstack/react-router", () => ({ useNavigate: () => navigateMock })); -vi.mock("../hooks/useWorkspaceQuery", () => ({ - useWorkspaceQuery: () => useWorkspaceQueryMock(), - workspaceQueryKey: ["workspaces"], -})); -vi.mock("../lib/api-client", () => ({ - apiClient: { POST: (...args: unknown[]) => postMock(...args) }, - apiErrorMessage: (e: unknown) => (e instanceof Error ? e.message : "error"), -})); - -const pr = (n: number, state: PRState): PullRequestFacts => ({ - url: `https://example.com/pr/${n}`, - number: n, - state, - ci: "passing", - review: "approved", - mergeability: "mergeable", - reviewComments: false, - updatedAt: "2026-06-15T00:00:00Z", -}); - -const session = (id: string, prs: PullRequestFacts[]): WorkspaceSession => ({ - id, - workspaceId: "proj-1", - workspaceName: "my-app", - title: id, - provider: "claude-code", - kind: "worker", - branch: "feat/ns", - status: "review_pending", - updatedAt: "2026-06-15T00:00:00Z", - prs, -}); - -function setWorkspaces(sessions: WorkspaceSession[]) { - const data: WorkspaceSummary[] = [{ id: "proj-1", name: "my-app", path: "/p", sessions }]; - useWorkspaceQueryMock.mockReturnValue({ data, isError: false, isLoading: false }); -} - -function renderPage() { - render( - - - , - ); -} - -beforeEach(() => { - navigateMock.mockReset(); - postMock.mockReset().mockResolvedValue({ data: { method: "squash" }, error: undefined }); -}); - -afterEach(() => vi.restoreAllMocks()); - -describe("PullRequestsPage", () => { - it("renders one row per PR across sessions, actionable PRs first", () => { - setWorkspaces([session("auth", [pr(41, "open"), pr(42, "draft"), pr(40, "merged")])]); - renderPage(); - - const rows = screen.getAllByRole("row").slice(1); // drop header - const numbers = rows.map((r) => within(r).getByText(/^#\d+$/).textContent); - expect(numbers).toEqual(["#41", "#42", "#40"]); - }); - - it("merges the PR by its own number, not the session's", async () => { - setWorkspaces([session("auth", [pr(41, "open"), pr(42, "draft")])]); - renderPage(); - const user = userEvent.setup(); - - const childRow = screen.getByText("#42").closest("tr")!; - await user.click(within(childRow).getByRole("button", { name: "Merge" })); - - await waitFor(() => expect(postMock).toHaveBeenCalledTimes(1)); - expect(postMock).toHaveBeenCalledWith("/api/v1/prs/{id}/merge", { params: { path: { id: "42" } } }); - }); - - it("shows an empty state when no session has a PR", () => { - setWorkspaces([session("idle", [])]); - renderPage(); - expect(screen.getByText("No open pull requests.")).toBeInTheDocument(); - }); -}); diff --git a/frontend/src/renderer/components/PullRequestsPage.tsx b/frontend/src/renderer/components/PullRequestsPage.tsx deleted file mode 100644 index 09bf1b9719..0000000000 --- a/frontend/src/renderer/components/PullRequestsPage.tsx +++ /dev/null @@ -1,185 +0,0 @@ -import { useNavigate } from "@tanstack/react-router"; -import { useMutation, useQueries, useQueryClient } from "@tanstack/react-query"; -import { useState } from "react"; -import { apiClient, apiErrorMessage } from "../lib/api-client"; -import { useWorkspaceQuery, workspaceQueryKey } from "../hooks/useWorkspaceQuery"; -import { - sessionScmSummaryQueryKey, - sessionScmSummaryQueryOptions, - type SessionPRSummary, -} from "../hooks/useSessionScmSummary"; -import { comparePRDisplaySummaries, prDiffSummary, sessionPRDisplaySummaries } from "../lib/pr-display"; -import type { WorkspaceSession } from "../types/workspace"; -import { DashboardSubhead } from "./DashboardSubhead"; -import { Badge } from "./ui/badge"; -import { Button } from "./ui/button"; -import { PRSummaryParts } from "./PRSummaryDisplay"; -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "./ui/table"; -import { cn } from "../lib/utils"; - -type PRState = SessionPRSummary["state"]; - -const stateTone: Record = { - open: "border-success/40 bg-success/10 text-success", - draft: "border-border bg-raised text-muted-foreground", - merged: "border-accent/40 bg-accent-weak text-accent", - closed: "border-error/40 bg-error/10 text-error", -}; - -type PRRow = { - pr: SessionPRSummary; - session: WorkspaceSession; -}; - -// The PR board, ported from agent-orchestrator's PullRequestsPage. One row per -// attributed PR — a session can own several (a stack or independent PRs), so we -// flatMap the session's prs list rather than assuming one. Actions hit -// /prs/{number}/merge and /resolve-comments. Per-PR CI/review facts also live on -// the session route's inspector. -export function PullRequestsPage() { - const navigate = useNavigate(); - const workspaceQuery = useWorkspaceQuery(); - const sessions = (workspaceQuery.data ?? []).flatMap((w) => w.sessions); - const prQueries = useQueries({ - queries: sessions.map((session) => sessionScmSummaryQueryOptions(session.id)), - }); - const rows: PRRow[] = sessions - .flatMap((session, index) => - sessionPRDisplaySummaries(session, prQueries[index]?.data).map((pr) => ({ pr, session })), - ) - .sort((a, b) => comparePRDisplaySummaries(a.pr, b.pr)); - - return ( -
- - -
- {rows.length === 0 ? ( -

No open pull requests.

- ) : ( - - - - PR - Worker - State - Actions - - - - {rows.map((row) => ( - - void navigate({ - to: "/projects/$projectId/sessions/$sessionId", - params: { projectId: row.session.workspaceId, sessionId: row.session.id }, - }) - } - /> - ))} - -
- )} -
-
- ); -} - -function PRRowView({ row, onOpen }: { row: PRRow; onOpen: () => void }) { - const queryClient = useQueryClient(); - const [note, setNote] = useState<{ ok: boolean; text: string } | null>(null); - const refresh = () => { - void queryClient.invalidateQueries({ queryKey: workspaceQueryKey }); - void queryClient.invalidateQueries({ queryKey: sessionScmSummaryQueryKey() }); - }; - - const merge = useMutation({ - mutationFn: async () => { - const { data, error } = await apiClient.POST("/api/v1/prs/{id}/merge", { - params: { path: { id: String(row.pr.number) } }, - }); - if (error) throw new Error(apiErrorMessage(error)); - return data; - }, - onSuccess: (data) => { - setNote({ ok: true, text: `merged (${data?.method ?? "squash"})` }); - refresh(); - }, - onError: (e) => setNote({ ok: false, text: e instanceof Error ? e.message : "merge failed" }), - }); - - const resolve = useMutation({ - mutationFn: async () => { - const { error } = await apiClient.POST("/api/v1/prs/{id}/resolve-comments", { - params: { path: { id: String(row.pr.number) } }, - }); - if (error) throw new Error(apiErrorMessage(error)); - }, - onSuccess: () => { - setNote({ ok: true, text: "comments resolved" }); - refresh(); - }, - onError: (e) => setNote({ ok: false, text: e instanceof Error ? e.message : "resolve failed" }), - }); - - const actionable = row.pr.state === "open" || row.pr.state === "draft"; - - return ( - - #{row.pr.number} - -
{row.pr.title || row.session.title}
-
- {[ - row.session.workspaceName, - row.pr.sourceBranch || row.session.branch, - row.pr.targetBranch ? `-> ${row.pr.targetBranch}` : "", - prDiffSummary(row.pr), - ] - .filter(Boolean) - .join(" · ")} -
- -
- - - {row.pr.state} - - - e.stopPropagation()}> - {note ? ( - {note.text} - ) : actionable ? ( -
- - -
- ) : ( - - )} -
-
- ); -} diff --git a/frontend/src/renderer/components/ReportProblemDialog.tsx b/frontend/src/renderer/components/ReportProblemDialog.tsx deleted file mode 100644 index bc88e2a391..0000000000 --- a/frontend/src/renderer/components/ReportProblemDialog.tsx +++ /dev/null @@ -1,220 +0,0 @@ -import * as Dialog from "@radix-ui/react-dialog"; -import { ChevronDown, GitPullRequest, Mail, MessageSquare, Send, X } from "lucide-react"; -import { useEffect, useId, useMemo, useState } from "react"; -import { - collectReportProblemDiagnostics, - formatReportProblemDraft, - reportProblemDestinationUrl, - type ReportProblemDiagnostics, - type ReportProblemInput, - type ReportProblemOutput, -} from "../lib/report-problem"; -import { aoBridge } from "../lib/bridge"; -import { Button } from "./ui/button"; -import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "./ui/dropdown-menu"; -import { Input } from "./ui/input"; - -type ReportProblemDialogProps = { - open: boolean; - onOpenChange: (open: boolean) => void; -}; - -const DEFAULT_DIAGNOSTICS: ReportProblemDiagnostics = { - appVersion: "unknown", - buildMode: "unknown", - daemonState: "unknown", - generatedAt: "unknown", - platform: "unknown", - routeSurface: "unknown", -}; - -const OUTPUT_LABELS: Record = { - github: "GitHub", - discord: "Discord", - email: "Email", -}; - -const OUTPUT_ACTION_LABELS: Record = { - github: "Copy and raise GitHub issue", - discord: "Copy and open Discord", - email: "Copy and open email", -}; - -const OUTPUT_DESTINATION_LABELS: Record = { - github: "GitHub issue", - discord: "Discord", - email: "Email support", -}; - -export function ReportProblemDialog({ open, onOpenChange }: ReportProblemDialogProps) { - const summaryId = useId(); - const detailsId = useId(); - const [selectedOutput, setSelectedOutput] = useState("github"); - const [summary, setSummary] = useState(""); - const [details, setDetails] = useState(""); - const [copiedOutput, setCopiedOutput] = useState(null); - const [copyError, setCopyError] = useState(null); - const [diagnostics, setDiagnostics] = useState(DEFAULT_DIAGNOSTICS); - - useEffect(() => { - if (!open) { - setSummary(""); - setDetails(""); - setSelectedOutput("github"); - setCopiedOutput(null); - setCopyError(null); - return; - } - let active = true; - void collectReportProblemDiagnostics().then((nextDiagnostics) => { - if (active) setDiagnostics(nextDiagnostics); - }); - return () => { - active = false; - }; - }, [open]); - - const input = useMemo( - () => ({ - summary, - details, - }), - [summary, details], - ); - - const draft = useMemo( - () => formatReportProblemDraft(input, diagnostics, selectedOutput), - [input, diagnostics, selectedOutput], - ); - - const copyDraft = async () => { - setCopyError(null); - const output = selectedOutput; - try { - await aoBridge.clipboard.writeText(draft); - const destinationUrl = reportProblemDestinationUrl(input, diagnostics, output); - if (destinationUrl) { - await aoBridge.app.openExternal(destinationUrl); - } - setCopiedOutput(output); - setSummary(""); - setDetails(""); - setSelectedOutput("github"); - } catch (err) { - setCopyError(err instanceof Error ? err.message : "Could not copy report draft"); - setCopiedOutput(null); - } - }; - - const selectOutput = (output: ReportProblemOutput) => { - setSelectedOutput(output); - setCopiedOutput(null); - setCopyError(null); - }; - - return ( - - - - -
-
- Report a problem - - Write a short note, then copy it to GitHub, Discord, or email. - -
- - - -
- -
-
- - setSummary(event.target.value)} - placeholder="Brief title" - /> -
- -
- -