feat(frontend): tailor feedback report prompts

This commit is contained in:
swyam sharma 2026-07-06 09:54:30 +05:30
parent 00fff21034
commit d683438cd5
5 changed files with 147 additions and 41 deletions

View File

@ -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" };

View File

@ -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
<div className="space-y-1.5">
<label className="text-[12px] font-medium text-muted-foreground" htmlFor={summaryId}>
Summary
{fieldCopy.summaryLabel}
</label>
<Input
id={summaryId}
value={summary}
onChange={(event) => setSummary(event.target.value)}
placeholder="One sentence summary"
placeholder={fieldCopy.summaryPlaceholder}
/>
</div>
<div className="space-y-1.5">
<label className="text-[12px] font-medium text-muted-foreground" htmlFor={detailsId}>
Details / repro
{fieldCopy.detailsLabel}
</label>
<textarea
id={detailsId}
className="min-h-[112px] w-full resize-y rounded-md border border-border bg-transparent px-3 py-2 text-[13px] leading-relaxed text-foreground outline-none transition placeholder:text-passive focus-visible:border-accent focus-visible:ring-2 focus-visible:ring-accent-weak"
value={details}
onChange={(event) => setDetails(event.target.value)}
placeholder="Steps, context, or the rough idea"
placeholder={fieldCopy.detailsPlaceholder}
/>
</div>
<div className="space-y-1.5">
<label className="text-[12px] font-medium text-muted-foreground" htmlFor={expectedId}>
Expected / request
{fieldCopy.expectedLabel}
</label>
<textarea
id={expectedId}
className="min-h-[84px] w-full resize-y rounded-md border border-border bg-transparent px-3 py-2 text-[13px] leading-relaxed text-foreground outline-none transition placeholder:text-passive focus-visible:border-accent focus-visible:ring-2 focus-visible:ring-accent-weak"
value={expected}
onChange={(event) => setExpected(event.target.value)}
placeholder="What should happen or what you want"
placeholder={fieldCopy.expectedPlaceholder}
/>
</div>
@ -240,7 +245,7 @@ export function ReportProblemDialog({ open, onOpenChange }: ReportProblemDialogP
</Button>
<Button type="button" onClick={() => void copyDraft("email")}>
<Clipboard className="size-3.5" aria-hidden="true" />
Copy and open email
Copy email draft
</Button>
</div>
</Dialog.Content>

View File

@ -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 () => {

View File

@ -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 () => {

View File

@ -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<ReportProblemType, string> = {
question: "Setup question",
};
const REPORT_FIELD_COPY: Record<ReportProblemType, ReportProblemFieldCopy> = {
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),