diff --git a/frontend/src/main.ts b/frontend/src/main.ts
index 716728e2a..334c6873a 100644
--- a/frontend/src/main.ts
+++ b/frontend/src/main.ts
@@ -209,7 +209,7 @@ function createWindow(): void {
// navigate the privileged window away from the app origin. External links go to
// the OS browser. Keep this in place before exposing any daemon output to the renderer.
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
- if (/^(https?:\/\/|mailto:)/.test(url)) {
+ if (/^https?:\/\//.test(url)) {
void shell.openExternal(url);
}
return { action: "deny" };
diff --git a/frontend/src/renderer/components/ReportProblemDialog.tsx b/frontend/src/renderer/components/ReportProblemDialog.tsx
index 07e91e097..243fbfef1 100644
--- a/frontend/src/renderer/components/ReportProblemDialog.tsx
+++ b/frontend/src/renderer/components/ReportProblemDialog.tsx
@@ -5,6 +5,7 @@ import {
collectReportProblemDiagnostics,
formatReportProblemDraft,
reportProblemDestinationUrl,
+ reportProblemFieldCopy,
type ReportProblemDiagnostics,
type ReportProblemInput,
type ReportProblemOutput,
@@ -82,13 +83,17 @@ export function ReportProblemDialog({ open, onOpenChange }: ReportProblemDialogP
() => formatReportProblemDraft(input, diagnostics, previewOutput),
[input, diagnostics, previewOutput],
);
+ const fieldCopy = reportProblemFieldCopy(type);
const copyDraft = async (output: ReportProblemOutput) => {
setCopyError(null);
setPreviewOutput(output);
try {
await aoBridge.clipboard.writeText(formatReportProblemDraft(input, diagnostics, output));
- window.open(reportProblemDestinationUrl(input, diagnostics, output), "_blank", "noopener,noreferrer");
+ const destinationUrl = reportProblemDestinationUrl(input, diagnostics, output);
+ if (destinationUrl) {
+ window.open(destinationUrl, "_blank", "noopener,noreferrer");
+ }
setCopiedOutput(output);
} catch (err) {
setCopyError(err instanceof Error ? err.message : "Could not copy report draft");
@@ -140,39 +145,39 @@ export function ReportProblemDialog({ open, onOpenChange }: ReportProblemDialogP
setSummary(event.target.value)}
- placeholder="One sentence summary"
+ placeholder={fieldCopy.summaryPlaceholder}
/>
@@ -240,7 +245,7 @@ export function ReportProblemDialog({ open, onOpenChange }: ReportProblemDialogP
diff --git a/frontend/src/renderer/components/Sidebar.test.tsx b/frontend/src/renderer/components/Sidebar.test.tsx
index 91b541d59..dc1cbe346 100644
--- a/frontend/src/renderer/components/Sidebar.test.tsx
+++ b/frontend/src/renderer/components/Sidebar.test.tsx
@@ -324,12 +324,12 @@ describe("Sidebar", () => {
await user.click(feedbackButton);
expect(await screen.findByRole("dialog", { name: "Report a problem" })).toBeInTheDocument();
- await user.type(screen.getByLabelText("Summary"), "Create project fails in /Users/alice/private-repo");
+ await user.type(screen.getByLabelText("Bug summary"), "Create project fails in /Users/alice/private-repo");
await user.type(
- screen.getByLabelText("Details / repro"),
+ screen.getByLabelText("Steps to reproduce"),
"Open http://127.0.0.1:5173/projects/demo?access_token=local-secret and click Create.",
);
- await user.type(screen.getByLabelText("Expected / request"), "Show a clear prerequisite error.");
+ await user.type(screen.getByLabelText("Expected behavior"), "Show a clear prerequisite error.");
expect(screen.getByLabelText("Report preview")).toHaveTextContent("[redacted-local-path]");
await user.click(screen.getByRole("button", { name: "Copy and open GitHub" }));
@@ -350,7 +350,7 @@ describe("Sidebar", () => {
);
});
- it("copies Discord and email drafts when daemon diagnostics are unavailable", async () => {
+ it("copies Discord with an official invite and keeps email draft copy-only when no support address exists", async () => {
const user = userEvent.setup();
const writeText = vi.fn().mockResolvedValue(undefined);
const open = vi.spyOn(window, "open").mockReturnValue(null);
@@ -361,16 +361,48 @@ describe("Sidebar", () => {
await user.click(screen.getAllByRole("button", { name: "Feedback" })[0]);
expect(await screen.findByRole("dialog", { name: "Report a problem" })).toBeInTheDocument();
- await user.type(screen.getByLabelText("Summary"), "Need help with setup");
+ await user.type(screen.getByLabelText("Bug summary"), "Need help with setup");
await user.click(screen.getByRole("button", { name: "Copy and open Discord" }));
- await user.click(screen.getByRole("button", { name: "Copy and open email" }));
+ await user.click(screen.getByRole("button", { name: "Copy email draft" }));
await waitFor(() => expect(writeText).toHaveBeenCalledTimes(2));
expect(writeText.mock.calls[0][0]).toContain("Daemon: unknown");
expect(writeText.mock.calls[1][0]).toContain("AO feedback");
- expect(open).toHaveBeenNthCalledWith(1, "https://discord.com/invite/UZv7JjxbwG", "_blank", "noopener,noreferrer");
- expect(open).toHaveBeenNthCalledWith(2, expect.stringMatching(/^mailto:\?/), "_blank", "noopener,noreferrer");
+ expect(open).toHaveBeenCalledOnce();
+ expect(open).toHaveBeenCalledWith("https://discord.com/invite/UZv7JjxbwG", "_blank", "noopener,noreferrer");
+ });
+
+ it("shows report-type specific prompts instead of one generic questionnaire", async () => {
+ const user = userEvent.setup();
+ renderSidebar();
+
+ await user.click(screen.getAllByRole("button", { name: "Feedback" })[0]);
+ expect(await screen.findByRole("dialog", { name: "Report a problem" })).toBeInTheDocument();
+ expect(screen.getByLabelText("Bug summary")).toHaveAttribute("placeholder", "Short description of the bug");
+ expect(screen.getByLabelText("Steps to reproduce")).toHaveAttribute(
+ "placeholder",
+ "1. Open...\n2. Click...\n3. See...",
+ );
+ expect(screen.getByLabelText("Expected behavior")).toHaveAttribute(
+ "placeholder",
+ "What should have happened instead?",
+ );
+
+ await chooseOption(screen.getByRole("combobox", { name: "Report type" }), "Feature request");
+ expect(screen.getByLabelText("Feature summary")).toHaveAttribute("placeholder", "Short description of the idea");
+ expect(screen.getByLabelText("Problem / use case")).toBeInTheDocument();
+ expect(screen.getByLabelText("Requested behavior")).toBeInTheDocument();
+
+ await chooseOption(screen.getByRole("combobox", { name: "Report type" }), "General feedback");
+ expect(screen.getByLabelText("Feedback summary")).toBeInTheDocument();
+ expect(screen.getByLabelText("Message")).toBeInTheDocument();
+ expect(screen.getByLabelText("Optional context")).toBeInTheDocument();
+
+ await chooseOption(screen.getByRole("combobox", { name: "Report type" }), "Setup question");
+ expect(screen.getByLabelText("Question summary")).toBeInTheDocument();
+ expect(screen.getByLabelText("What are you trying to do?")).toBeInTheDocument();
+ expect(screen.getByLabelText("What did you try?")).toBeInTheDocument();
});
it("renames a session inline and persists via the daemon", async () => {
diff --git a/frontend/src/renderer/lib/report-problem.test.ts b/frontend/src/renderer/lib/report-problem.test.ts
index c9af8eacb..0c2af80f2 100644
--- a/frontend/src/renderer/lib/report-problem.test.ts
+++ b/frontend/src/renderer/lib/report-problem.test.ts
@@ -83,8 +83,38 @@ describe("report problem drafts", () => {
expect(draft).toContain("No diagnostics included");
});
- it("builds copy handoff destinations for GitHub, Discord, and email", () => {
- const github = new URL(reportProblemDestinationUrl(completeInput, diagnostics, "github"));
+ it("uses report-type specific field labels in generated drafts", () => {
+ const featureDraft = formatReportProblemDraft(
+ {
+ type: "feature",
+ summary: "Make feedback reports easier to send",
+ details: "Users want fewer generic bug-report questions for non-bug reports.",
+ expected: "Show feature-specific prompts and output labels.",
+ includeDiagnostics: false,
+ },
+ diagnostics,
+ "github",
+ );
+ expect(featureDraft).toContain("## Problem / use case");
+ expect(featureDraft).toContain("## Requested behavior");
+
+ const questionDraft = formatReportProblemDraft(
+ {
+ type: "question",
+ summary: "Need help setting up Claude Code",
+ details: "Trying to create my first project.",
+ expected: "I installed the CLI and checked PATH.",
+ includeDiagnostics: false,
+ },
+ diagnostics,
+ "email",
+ );
+ expect(questionDraft).toContain("What are you trying to do?:");
+ expect(questionDraft).toContain("What did you try?:");
+ });
+
+ it("builds copy handoff destinations for GitHub and Discord while leaving email copy-only", () => {
+ const github = new URL(reportProblemDestinationUrl(completeInput, diagnostics, "github")!);
expect(`${github.origin}${github.pathname}`).toBe("https://github.com/AgentWrapper/agent-orchestrator/issues/new");
expect(github.searchParams.get("title")).toBe("Terminal keeps reconnecting after daemon restart");
expect(github.searchParams.get("body")).toContain("[redacted-local-path]");
@@ -94,11 +124,7 @@ describe("report problem drafts", () => {
"https://discord.com/invite/UZv7JjxbwG",
);
- const email = reportProblemDestinationUrl(completeInput, diagnostics, "email");
- const params = new URLSearchParams(email.replace(/^mailto:\?/, ""));
- expect(params.get("subject")).toBe("AO feedback: Terminal keeps reconnecting after daemon restart");
- expect(params.get("body")).toContain("[redacted-local-path]");
- expect(params.get("body")).toContain("[redacted-local-url]");
+ expect(reportProblemDestinationUrl(completeInput, diagnostics, "email")).toBeNull();
});
it("derives route surface from the hash-history route", async () => {
diff --git a/frontend/src/renderer/lib/report-problem.ts b/frontend/src/renderer/lib/report-problem.ts
index 83944beeb..e97e7420c 100644
--- a/frontend/src/renderer/lib/report-problem.ts
+++ b/frontend/src/renderer/lib/report-problem.ts
@@ -12,6 +12,15 @@ export type ReportProblemInput = {
includeDiagnostics: boolean;
};
+export type ReportProblemFieldCopy = {
+ summaryLabel: string;
+ summaryPlaceholder: string;
+ detailsLabel: string;
+ detailsPlaceholder: string;
+ expectedLabel: string;
+ expectedPlaceholder: string;
+};
+
export type ReportProblemDiagnostics = {
appVersion: string;
buildMode: string;
@@ -35,6 +44,41 @@ const REPORT_TYPE_LABELS: Record = {
question: "Setup question",
};
+const REPORT_FIELD_COPY: Record = {
+ bug: {
+ summaryLabel: "Bug summary",
+ summaryPlaceholder: "Short description of the bug",
+ detailsLabel: "Steps to reproduce",
+ detailsPlaceholder: "1. Open...\n2. Click...\n3. See...",
+ expectedLabel: "Expected behavior",
+ expectedPlaceholder: "What should have happened instead?",
+ },
+ feature: {
+ summaryLabel: "Feature summary",
+ summaryPlaceholder: "Short description of the idea",
+ detailsLabel: "Problem / use case",
+ detailsPlaceholder: "What problem are you trying to solve?",
+ expectedLabel: "Requested behavior",
+ expectedPlaceholder: "What should AO do?",
+ },
+ feedback: {
+ summaryLabel: "Feedback summary",
+ summaryPlaceholder: "Short summary",
+ detailsLabel: "Message",
+ detailsPlaceholder: "What should the AO team know?",
+ expectedLabel: "Optional context",
+ expectedPlaceholder: "Anything else that would help?",
+ },
+ question: {
+ summaryLabel: "Question summary",
+ summaryPlaceholder: "Short description of the setup question",
+ detailsLabel: "What are you trying to do?",
+ detailsPlaceholder: "Describe the setup or workflow you are attempting.",
+ expectedLabel: "What did you try?",
+ expectedPlaceholder: "Commands, docs, or fixes you already tried.",
+ },
+};
+
const LOCAL_URL_PATTERN =
/(?:\bfile:\/\/\/\S+|\bapp:\/\/renderer\/\S+|\bhttps?:\/\/(?:localhost|127\.0\.0\.1|\[::1\])(?::\d+)?\S*)/gi;
const LOCAL_PATH_PATTERN = /(?:\/Users\/|\/home\/|\/tmp\/|\/private\/var\/|\/var\/folders\/)\S+|\b[A-Za-z]:\\[^\s)]+/g;
@@ -74,22 +118,24 @@ export async function collectReportProblemDiagnostics(now = new Date()): Promise
};
}
+export function reportProblemFieldCopy(type: ReportProblemType): ReportProblemFieldCopy {
+ return REPORT_FIELD_COPY[type];
+}
+
export function formatReportProblemDraft(
input: ReportProblemInput,
diagnostics: ReportProblemDiagnostics,
output: ReportProblemOutput,
): string {
const fields = normalizeInput(input);
- const diagnosticsBlock = input.includeDiagnostics
- ? formatDiagnostics(diagnostics)
- : "No diagnostics included.";
+ const diagnosticsBlock = input.includeDiagnostics ? formatDiagnostics(diagnostics) : "No diagnostics included.";
if (output === "discord") {
return [
`**AO ${fields.typeLabel}**`,
`Summary: ${fields.summary}`,
- `Details: ${fields.details}`,
- `Expected/request: ${fields.expected}`,
+ `${fields.detailsLabel}: ${fields.details}`,
+ `${fields.expectedLabel}: ${fields.expected}`,
"",
"Safe diagnostics:",
diagnosticsBlock,
@@ -107,10 +153,10 @@ export function formatReportProblemDraft(
`Type: ${fields.typeLabel}`,
`Summary: ${fields.summary}`,
"",
- "Details / repro:",
+ `${fields.detailsLabel}:`,
fields.details,
"",
- "Expected / request:",
+ `${fields.expectedLabel}:`,
fields.expected,
"",
"Safe diagnostics:",
@@ -129,10 +175,10 @@ export function formatReportProblemDraft(
"## Summary",
fields.summary,
"",
- "## Details / repro",
+ `## ${fields.detailsLabel}`,
fields.details,
"",
- "## Expected / request",
+ `## ${fields.expectedLabel}`,
fields.expected,
"",
"## Safe diagnostics",
@@ -146,18 +192,12 @@ export function reportProblemDestinationUrl(
input: ReportProblemInput,
diagnostics: ReportProblemDiagnostics,
output: ReportProblemOutput,
-): string {
+): string | null {
if (output === "discord") return DISCORD_INVITE_URL;
+ if (output === "email") return null;
const title = reportTitle(input);
const draft = formatReportProblemDraft(input, diagnostics, output);
- if (output === "email") {
- const params = new URLSearchParams({
- subject: `AO feedback: ${title}`,
- body: draft.replace(/^Subject:[^\n]*\n\n/, ""),
- });
- return `mailto:?${params.toString()}`;
- }
const url = new URL(GITHUB_NEW_ISSUE_URL);
url.searchParams.set("title", title);
@@ -166,8 +206,11 @@ export function reportProblemDestinationUrl(
}
function normalizeInput(input: ReportProblemInput) {
+ const fieldCopy = REPORT_FIELD_COPY[input.type];
return {
typeLabel: REPORT_TYPE_LABELS[input.type],
+ detailsLabel: fieldCopy.detailsLabel,
+ expectedLabel: fieldCopy.expectedLabel,
summary: valueOrPlaceholder(input.summary),
details: valueOrPlaceholder(input.details),
expected: valueOrPlaceholder(input.expected),