feat(frontend): tailor feedback report prompts
This commit is contained in:
parent
00fff21034
commit
d683438cd5
|
|
@ -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" };
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
Loading…
Reference in New Issue