diff --git a/.changeset/fn-7369-confirm-other.md b/.changeset/fn-7369-confirm-other.md
new file mode 100644
index 0000000000..d7cf313df3
--- /dev/null
+++ b/.changeset/fn-7369-confirm-other.md
@@ -0,0 +1,7 @@
+---
+"@runfusion/fusion": patch
+---
+
+summary: Let Planning Mode Yes/No questions accept a custom Other answer.
+category: fix
+dev: Extends confirm-question handling to preserve user-authored alternatives via `_other`.
diff --git a/packages/dashboard/app/components/PlanningModeModal.css b/packages/dashboard/app/components/PlanningModeModal.css
index 7f86b2161a..d09d11d632 100644
--- a/packages/dashboard/app/components/PlanningModeModal.css
+++ b/packages/dashboard/app/components/PlanningModeModal.css
@@ -1033,6 +1033,12 @@ An empty footer must NOT reserve vertical space or paint its divider band. When
gap: var(--space-sm);
}
+.planning-confirm-answer {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-sm);
+}
+
.planning-confirm-group {
display: flex;
gap: var(--space-md);
@@ -1732,6 +1738,10 @@ Tablet embedded Planning keeps the desktop two-pane shell, so the summary footer
}
/* Confirm buttons: mobile-friendly sizing */
+ .planning-confirm-group {
+ flex-direction: column;
+ }
+
.planning-confirm-btn {
min-height: 36px;
padding: var(--space-sm) var(--space-md);
diff --git a/packages/dashboard/app/components/PlanningModeModal.tsx b/packages/dashboard/app/components/PlanningModeModal.tsx
index 8012151ab7..37c3572e63 100644
--- a/packages/dashboard/app/components/PlanningModeModal.tsx
+++ b/packages/dashboard/app/components/PlanningModeModal.tsx
@@ -2465,7 +2465,14 @@ function QuestionForm({ question: rawQuestion, progress, historyEntries, onSubmi
if (question.type === "text") {
nextResponse = { [question.id]: textValue };
} else if (question.type === "confirm") {
- nextResponse = { [question.id]: response[question.id] === true };
+ const trimmedOther = otherValue.trim();
+ /*
+ FNXC:PlanningInterview 2026-07-01-00:00:
+ GitHub #1832 requires Yes/No Planning Mode questions to offer an Other path so users can replace a forced boolean with their own answer. Reuse the reserved `_other` payload shape used by structured selection questions instead of inventing a confirm-only custom-answer contract.
+ */
+ nextResponse = isOtherSelected && trimmedOther.length > 0
+ ? { [PLANNING_OTHER_RESPONSE_KEY]: trimmedOther }
+ : { [question.id]: response[question.id] === true };
} else if (question.type === "single_select") {
const trimmedOther = otherValue.trim();
/*
@@ -2522,7 +2529,7 @@ function QuestionForm({ question: rawQuestion, progress, historyEntries, onSubmi
*/
return (Array.isArray(response[question.id] as unknown) && (response[question.id] as unknown[]).length > 0) || (isOtherSelected && otherValue.trim().length > 0);
case "confirm":
- return response[question.id] !== undefined;
+ return response[question.id] !== undefined || (isOtherSelected && otherValue.trim().length > 0);
default:
return true;
}
@@ -2687,21 +2694,54 @@ function QuestionForm({ question: rawQuestion, progress, historyEntries, onSubmi
)}
{question.type === "confirm" && (
-
-
-
+
+
+
+
+
+
+ {isOtherSelected && (
+
+
+ )}
)}
diff --git a/packages/dashboard/app/components/__tests__/PlanningModeModal.planning-flow.test.tsx b/packages/dashboard/app/components/__tests__/PlanningModeModal.planning-flow.test.tsx
index a402186cb3..cb643eae14 100644
--- a/packages/dashboard/app/components/__tests__/PlanningModeModal.planning-flow.test.tsx
+++ b/packages/dashboard/app/components/__tests__/PlanningModeModal.planning-flow.test.tsx
@@ -624,6 +624,131 @@ describe("PlanningModeModal", () => {
});
});
+ it.each(["desktop", "mobile"] as const)("lets confirm questions submit an Other answer on %s", async (viewportMode) => {
+ window.sessionStorage.setItem("fusion-tab-id", "tab-self");
+ mockViewport(viewportMode);
+ mockConnectPlanningStream.mockImplementationOnce((_sessionId: string, _projectId: string | undefined, handlers: any) => {
+ setTimeout(() => {
+ handlers.onQuestion?.({
+ id: "q-confirm-scope",
+ type: "confirm",
+ question: "Proceed with this scope?",
+ description: "Choose Yes, No, or write a different answer.",
+ });
+ }, 10);
+ return {
+ close: vi.fn(),
+ isConnected: vi.fn().mockReturnValue(true),
+ };
+ });
+
+ render(
+ ,
+ );
+
+ fireEvent.change(screen.getByPlaceholderText(/e.g., Build a user authentication/), {
+ target: { value: "Build auth system" },
+ });
+ fireEvent.click(screen.getByText("Start Planning"));
+
+ await waitFor(() => {
+ expect(screen.getByText("Proceed with this scope?")).toBeDefined();
+ });
+
+ expect(screen.getByRole("button", { name: /Yes/ })).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: /No/ })).toBeInTheDocument();
+ expect(screen.getByTestId("planning-option-other")).toBeInTheDocument();
+ expect(screen.queryByTestId("planning-other-input")).toBeNull();
+
+ const continueButton = screen.getByRole("button", { name: "Continue" });
+ fireEvent.click(screen.getByTestId("planning-option-other"));
+ expect(screen.getByTestId("planning-other-input")).toBeInTheDocument();
+ expect(continueButton).toBeDisabled();
+
+ fireEvent.change(screen.getByTestId("planning-other-input"), { target: { value: " " } });
+ expect(continueButton).toBeDisabled();
+
+ fireEvent.change(screen.getByTestId("planning-other-input"), {
+ target: { value: " Ask a different scoping question " },
+ });
+ expect(continueButton).toBeEnabled();
+ fireEvent.click(continueButton);
+
+ await waitFor(() => {
+ expect(mockRespondToPlanning).toHaveBeenCalledWith(
+ "session-123",
+ { _other: "Ask a different scoping question" },
+ undefined,
+ "tab-self",
+ );
+ });
+ });
+
+ it("clears confirm Other text when switching back to Yes or No", async () => {
+ window.sessionStorage.setItem("fusion-tab-id", "tab-self");
+ mockConnectPlanningStream.mockImplementationOnce((_sessionId: string, _projectId: string | undefined, handlers: any) => {
+ setTimeout(() => {
+ handlers.onQuestion?.({
+ id: "q-confirm-scope",
+ type: "confirm",
+ question: "Proceed with this scope?",
+ });
+ }, 10);
+ return {
+ close: vi.fn(),
+ isConnected: vi.fn().mockReturnValue(true),
+ };
+ });
+
+ render(
+ ,
+ );
+
+ fireEvent.change(screen.getByPlaceholderText(/e.g., Build a user authentication/), {
+ target: { value: "Build auth system" },
+ });
+ fireEvent.click(screen.getByText("Start Planning"));
+
+ await waitFor(() => {
+ expect(screen.getByText("Proceed with this scope?")).toBeDefined();
+ });
+
+ const continueButton = screen.getByRole("button", { name: "Continue" });
+ fireEvent.click(screen.getByTestId("planning-option-other"));
+ fireEvent.change(screen.getByTestId("planning-other-input"), {
+ target: { value: "Ask a different scoping question" },
+ });
+ fireEvent.change(screen.getByLabelText("Additional comments (optional)"), {
+ target: { value: "Keep the planner moving" },
+ });
+ expect(continueButton).toBeEnabled();
+
+ fireEvent.click(screen.getByRole("button", { name: /No/ }));
+ expect(screen.queryByTestId("planning-other-input")).toBeNull();
+ fireEvent.click(continueButton);
+
+ await waitFor(() => {
+ expect(mockRespondToPlanning).toHaveBeenCalledWith(
+ "session-123",
+ { "q-confirm-scope": false, _comment: "Keep the planner moving" },
+ undefined,
+ "tab-self",
+ );
+ });
+ });
+
it("shows stop action in loading and stops generation", async () => {
let streamHandlers: any;
const closeSpy = vi.fn();
diff --git a/packages/dashboard/src/__tests__/planning-interview-formatters.test.ts b/packages/dashboard/src/__tests__/planning-interview-formatters.test.ts
index d5668ae0e3..2b28486ac4 100644
--- a/packages/dashboard/src/__tests__/planning-interview-formatters.test.ts
+++ b/packages/dashboard/src/__tests__/planning-interview-formatters.test.ts
@@ -22,6 +22,12 @@ const multiSelectQuestion: PlanningQuestion = {
],
};
+const confirmQuestion: PlanningQuestion = {
+ id: "proceed",
+ type: "confirm",
+ question: "Proceed with this plan?",
+};
+
describe("planning interview formatter Other answers", () => {
it("formats Other-only single-select answers for the planning agent and Q&A history", () => {
const response = { _other: "Run discovery first" };
@@ -44,4 +50,25 @@ describe("planning interview formatter Other answers", () => {
"A: Speed, Keep humans in review (user's own answer)",
);
});
+
+ it("formats confirm Yes and No answers without changing boolean semantics", () => {
+ expect(formatResponseForAgent(confirmQuestion, { proceed: true })).toContain("Answer: Yes");
+ expect(formatInterviewQA([{ question: confirmQuestion, response: { proceed: true } }])).toContain("A: Yes");
+
+ expect(formatResponseForAgent(confirmQuestion, { proceed: false })).toContain("Answer: No");
+ expect(formatInterviewQA([{ question: confirmQuestion, response: { proceed: false } }])).toContain("A: No");
+ });
+
+ it("formats confirm Other answers and comments as first-class custom answers", () => {
+ const response = { _other: "Ask a different scoping question", _comment: "Need product input" };
+
+ expect(formatResponseForAgent(confirmQuestion, response)).toContain(
+ "Answer: Ask a different scoping question (user's own answer)",
+ );
+ expect(formatResponseForAgent(confirmQuestion, response)).toContain("Additional context: Need product input");
+ expect(formatInterviewQA([{ question: confirmQuestion, response }])).toContain(
+ "A: Ask a different scoping question (user's own answer)",
+ );
+ expect(formatInterviewQA([{ question: confirmQuestion, response }])).toContain("Comment: Need product input");
+ });
});
diff --git a/packages/dashboard/src/planning.ts b/packages/dashboard/src/planning.ts
index 41a41de05f..b6cf4b5d70 100644
--- a/packages/dashboard/src/planning.ts
+++ b/packages/dashboard/src/planning.ts
@@ -2478,7 +2478,13 @@ export function formatResponseForAgent(
break;
case "confirm":
- formatted = `Question: ${question.question}\n\nAnswer: ${responseValue === true ? "Yes" : "No"}`;
+ /*
+ FNXC:PlanningInterview 2026-07-01-00:00:
+ GitHub #1832 lets Planning Mode confirm questions submit `_other` instead of a boolean. Preserve that user-authored answer for the agent; otherwise history replay would turn missing confirm ids into an unintended "No".
+ */
+ formatted = other.length > 0
+ ? `Question: ${question.question}\n\nAnswer: ${other} (user's own answer)`
+ : `Question: ${question.question}\n\nAnswer: ${responseValue === true ? "Yes" : "No"}`;
break;
default:
@@ -2544,7 +2550,7 @@ function formatInterviewAnswer(question: PlanningQuestion, responseValue: unknow
}
case "confirm":
- return responseValue === true ? "Yes" : "No";
+ return other.length > 0 ? `${other} (user's own answer)` : responseValue === true ? "Yes" : "No";
default:
return JSON.stringify(responseValue);