diff --git a/frontend/node_modules b/frontend/node_modules
deleted file mode 120000
index f7f2174ec1..0000000000
--- a/frontend/node_modules
+++ /dev/null
@@ -1 +0,0 @@
-/Users/psudokit/.ao/data/worktrees/ao/ao-5/frontend/node_modules
\ No newline at end of file
diff --git a/frontend/src/renderer/components/GlobalSettingsForm.test.tsx b/frontend/src/renderer/components/GlobalSettingsForm.test.tsx
index 86b30a8dd4..2aaa0db637 100644
--- a/frontend/src/renderer/components/GlobalSettingsForm.test.tsx
+++ b/frontend/src/renderer/components/GlobalSettingsForm.test.tsx
@@ -280,6 +280,7 @@ describe("GlobalSettingsForm", () => {
await user.click(await screen.findByRole("button", { name: "Report a problem" }));
expect(await screen.findByRole("dialog", { name: "Report a problem" })).toBeInTheDocument();
await user.type(screen.getByLabelText("Title"), "Need help with setup");
+ await user.type(screen.getByLabelText("What happened?"), "The setup flow stalls after the first prompt.");
await user.click(screen.getByRole("radio", { name: "Discord" }));
expect(screen.getByRole("button", { name: /copy & open discord/i })).toBeInTheDocument();
@@ -288,6 +289,8 @@ describe("GlobalSettingsForm", () => {
await waitFor(() => expect(writeText).toHaveBeenCalledTimes(1));
expect(writeText.mock.calls[0][0]).toContain("**AO feedback**");
expect(screen.getByText("Discord draft copied.")).toBeInTheDocument();
+ expect(screen.getByLabelText("Title")).toHaveValue("");
+ expect(screen.getByLabelText("What happened?")).toHaveValue("");
await user.click(screen.getByRole("radio", { name: "Email" }));
expect(screen.getByRole("button", { name: /copy & open email/i })).toBeInTheDocument();
@@ -295,6 +298,7 @@ describe("GlobalSettingsForm", () => {
expect(screen.queryByText("Discord draft copied.")).not.toBeInTheDocument();
expect(screen.getByRole("button", { name: /copy & open email/i })).toBeDisabled();
await user.type(screen.getByLabelText("Title"), "Need help with setup");
+ await user.type(screen.getByLabelText("What happened?"), "The setup flow stalls after the first prompt.");
await user.click(screen.getByRole("button", { name: /copy & open email/i }));
await waitFor(() => expect(writeText).toHaveBeenCalledTimes(2));
diff --git a/frontend/src/renderer/components/settings/ReportProblemDialog.test.tsx b/frontend/src/renderer/components/settings/ReportProblemDialog.test.tsx
new file mode 100644
index 0000000000..5ef22934ed
--- /dev/null
+++ b/frontend/src/renderer/components/settings/ReportProblemDialog.test.tsx
@@ -0,0 +1,37 @@
+/**
+ * @vitest-environment jsdom
+ */
+import { act } from "react";
+import { fireEvent, render, screen } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+import { ReportProblemDialog } from "./ReportProblemDialog";
+
+describe("ReportProblemDialog", () => {
+ it("disables primary action when fields are empty or whitespace-only, and enables when both summary and details contain non-whitespace text", async () => {
+ await act(async () => {
+ render();
+ await Promise.resolve();
+ });
+
+ const summaryInput = screen.getByPlaceholderText(/brief title/i);
+ const detailsInput = screen.getByPlaceholderText(/share what happened/i);
+ const submitButton = screen.getByRole("button", { name: /copy & create github issue/i });
+
+ expect(submitButton).toBeDisabled();
+
+ fireEvent.change(summaryInput, { target: { value: "Test Summary" } });
+ expect(submitButton).toBeDisabled();
+
+ fireEvent.change(summaryInput, { target: { value: " " } });
+ fireEvent.change(detailsInput, { target: { value: "Test Details" } });
+ expect(submitButton).toBeDisabled();
+
+ fireEvent.change(summaryInput, { target: { value: "Test Summary" } });
+ fireEvent.change(detailsInput, { target: { value: " " } });
+ expect(submitButton).toBeDisabled();
+
+ fireEvent.change(summaryInput, { target: { value: "Test Summary" } });
+ fireEvent.change(detailsInput, { target: { value: "Test Details" } });
+ expect(submitButton).toBeEnabled();
+ });
+});
diff --git a/frontend/src/renderer/components/settings/ReportProblemDialog.tsx b/frontend/src/renderer/components/settings/ReportProblemDialog.tsx
index d62afdf8af..a1ad54c7d2 100644
--- a/frontend/src/renderer/components/settings/ReportProblemDialog.tsx
+++ b/frontend/src/renderer/components/settings/ReportProblemDialog.tsx
@@ -59,10 +59,10 @@ const DESTINATIONS: {
action: string;
icon: (props: DestinationIconProps) => ReactNode;
}[] = [
- { value: "github", label: "GitHub", action: "Copy & Create GitHub Issue", icon: GithubIcon },
- { value: "discord", label: "Discord", action: "Copy & Open Discord", icon: DiscordIcon },
- { value: "email", label: "Email", action: "Copy & Open Email", icon: EmailIcon },
-];
+ { value: "github", label: "GitHub", action: "Copy & Create GitHub Issue", icon: GithubIcon },
+ { value: "discord", label: "Discord", action: "Copy & Open Discord", icon: DiscordIcon },
+ { value: "email", label: "Email", action: "Copy & Open Email", icon: EmailIcon },
+ ];
export function ReportProblemDialog({ open, onOpenChange }: ReportProblemDialogProps) {
const titleId = useId();
@@ -98,7 +98,7 @@ export function ReportProblemDialog({ open, onOpenChange }: ReportProblemDialogP
const input = { summary, details };
const draft = formatReportProblemDraft(input, diagnostics, selectedOutput);
const destination = DESTINATIONS.find((option) => option.value === selectedOutput) ?? DESTINATIONS[0];
- const canCopy = summary.trim().length > 0;
+ const canSubmit = summary.trim().length > 0 && details.trim().length > 0;
const clearStatus = () => {
setCopiedOutput(null);
@@ -106,7 +106,7 @@ export function ReportProblemDialog({ open, onOpenChange }: ReportProblemDialogP
};
const copyDraft = async () => {
- if (!canCopy) return;
+ if (!canSubmit) return;
setCopyError(null);
const output = selectedOutput;
try {
@@ -230,9 +230,12 @@ export function ReportProblemDialog({ open, onOpenChange }: ReportProblemDialogP