Unverified 提交 9e557859 authored 作者: Mohamed Aziz Mejri's avatar Mohamed Aziz Mejri 提交者: GitHub

Add planning_questionnaire to the agent mode (#2566)

<!-- This is an auto-generated description by cubic. --> ## Summary by cubic Enable planning_questionnaire in normal agent mode and enforce a clarify‑before‑plan workflow with explicit examples. Also adds a one‑pass todo follow‑up so the agent completes remaining tasks before stopping. - **New Features** - planning_questionnaire is available in both plan and normal modes. - Clarify step: call planning_questionnaire to ask 1–3 focused questions for vague requests; skip when specific; STOP until the user replies. Always use it for new app/major feature requests. - One-pass todo follow-up: if a turn ends with incomplete todos, inject a short reminder and run another pass to finish them. - **Bug Fixes** - Always stop after planning_questionnaire in both modes to prevent repeated tool calls. - Gate plan-only tools: write_plan and exit_plan are plan‑mode only; questionnaire remains available in normal mode. <sup>Written for commit c6a67ea7e6981e86418dddeaa245b918a22261e9. Summary will update on new commits.</sup> <!-- End of auto-generated description by cubic. --> <!-- devin-review-badge-begin --> --- <a href="https://app.devin.ai/review/dyad-sh/dyad/pull/2566" target="_blank"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1"> <img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open with Devin"> </picture> </a> <!-- devin-review-badge-end --> --------- Co-authored-by: 's avatarMohamed Aziz Mejri <mohamedazizmejri@Mohameds-Mac-mini.local> Co-authored-by: 's avatarClaude Opus 4.5 <noreply@anthropic.com>
上级 d5a7b8b1
import { testSkipIfWindows } from "./helpers/test_helper";
import { expect } from "@playwright/test";
import { Timeout, testSkipIfWindows } from "./helpers/test_helper";
/**
* E2E tests for local-agent mode (Agent v2)
......@@ -42,3 +43,43 @@ testSkipIfWindows("local-agent - parallel tool calls", async ({ po }) => {
files: ["src/utils/math.ts", "src/utils/string.ts"],
});
});
testSkipIfWindows("local-agent - questionnaire flow", async ({ po }) => {
await po.setUpDyadPro({ localAgent: true });
await po.importApp("minimal");
await po.chatActions.selectLocalAgentMode();
// Wait for the auto-generated AI_RULES response to fully complete,
// then start a new chat to avoid the chat:stream:end event from the
// AI_RULES stream clearing the questionnaire state.
await po.chatActions.waitForChatCompletion();
await po.chatActions.clickNewChat();
// Trigger questionnaire fixture
await po.sendPrompt("tc=local-agent/questionnaire", {
skipWaitForCompletion: true,
});
// Wait for questionnaire UI to appear
await expect(po.page.getByText("Which framework do you prefer?")).toBeVisible(
{
timeout: Timeout.MEDIUM,
},
);
await expect(po.page.getByRole("button", { name: "Submit" })).toBeVisible({
timeout: Timeout.MEDIUM,
});
// Select "Vue" radio option
await po.page.getByText("Vue", { exact: true }).click();
// Submit the questionnaire
await po.page.getByRole("button", { name: /Submit/ }).click();
// Wait for the LLM response after submitting answers
await po.chatActions.waitForChatCompletion();
// Snapshot the messages
await po.snapshotMessages();
});
import fs from "node:fs";
import path from "node:path";
import { expect } from "@playwright/test";
import { Timeout, test } from "./helpers/test_helper";
import { Timeout, testSkipIfWindows } from "./helpers/test_helper";
test("plan mode - accept plan redirects to new chat and saves to disk", async ({
po,
}) => {
test.setTimeout(180000);
testSkipIfWindows(
"plan mode - accept plan redirects to new chat and saves to disk",
async ({ po }) => {
await po.setUpDyadPro({ localAgent: true });
await po.importApp("minimal");
await po.chatActions.selectChatMode("plan");
......@@ -48,31 +47,42 @@ test("plan mode - accept plan redirects to new chat and saves to disk", async ({
}).toPass({ timeout: Timeout.MEDIUM });
// Verify plan content
const planContent = fs.readFileSync(path.join(planDir, mdFiles[0]), "utf-8");
const planContent = fs.readFileSync(
path.join(planDir, mdFiles[0]),
"utf-8",
);
expect(planContent).toContain("Test Plan");
});
},
);
test("plan mode - questionnaire flow", async ({ po }) => {
test.setTimeout(180000);
testSkipIfWindows("plan mode - questionnaire flow", async ({ po }) => {
await po.setUpDyadPro({ localAgent: true });
await po.importApp("minimal");
await po.chatActions.selectChatMode("plan");
// Trigger questionnaire fixture
await po.sendPrompt("tc=local-agent/questionnaire");
await po.sendPrompt("tc=local-agent/questionnaire", {
skipWaitForCompletion: true,
});
// Wait for questionnaire UI to appear
await expect(po.page.getByText("Project Requirements")).toBeVisible({
await expect(po.page.getByText("Which framework do you prefer?")).toBeVisible(
{
timeout: Timeout.MEDIUM,
},
);
await expect(po.page.getByRole("button", { name: "Submit" })).toBeVisible({
timeout: Timeout.MEDIUM,
});
// Select "Vue" radio option by clicking the label text (Base UI Radio components)
// Select "Vue" radio option
await po.page.getByText("Vue", { exact: true }).click();
// Click Submit (single question → Submit button shown)
// Submit the questionnaire
await po.page.getByRole("button", { name: /Submit/ }).click();
// Wait for the LLM response to the submitted answers
// Wait for the LLM response after submitting answers
await po.chatActions.waitForChatCompletion();
// Snapshot the messages
......
......@@ -4,7 +4,7 @@
"input": [
{
"role": "developer",
"content": "\n<role>\nYou are Dyad, an AI assistant that creates and modifies web applications. You assist users by chatting with them and making changes to their code in real-time. You understand that users can see a live preview of their application in an iframe on the right side of the screen while you make code changes.\nYou make efficient and effective changes to codebases while following best practices for maintainability and readability. You take pride in keeping things simple and elegant. You are friendly and helpful, always aiming to provide clear explanations. \n</role>\n\n<app_commands>\nDo *not* tell the user to run shell commands. Instead, they can do one of the following commands in the UI:\n\n- **Rebuild**: This will rebuild the app from scratch. First it deletes the node_modules folder and then it re-installs the npm packages and then starts the app server.\n- **Restart**: This will restart the app server.\n- **Refresh**: This will refresh the app preview page.\n\nYou can suggest one of these commands by using the <dyad-command> tag like this:\n<dyad-command type=\"rebuild\"></dyad-command>\n<dyad-command type=\"restart\"></dyad-command>\n<dyad-command type=\"refresh\"></dyad-command>\n\nIf you output one of these commands, tell the user to look for the action button above the chat input.\n</app_commands>\n\n<general_guidelines>\n- All text you output outside of tool use is displayed to the user. Output text to communicate with the user. You can use Github-flavored markdown for formatting.\n- Always reply to the user in the same language they are using.\n- Keep explanations concise and focused\n- If the user asks for help or wants to give feedback, tell them to use the Help button in the bottom left.\n- Be careful not to introduce security vulnerabilities such as command injection, XSS, SQL injection, and other OWASP top 10 vulnerabilities. If you notice that you wrote insecure code, immediately fix it. Prioritize writing safe, secure, and correct code.\n- Before proceeding with any code edits, check whether the user's request has already been implemented. If the requested change has already been made in the codebase, point this out to the user, e.g., \"This feature is already implemented as described.\"\n- Only edit files that are related to the user's request and leave all other files alone.\n- All edits you make on the codebase will directly be built and rendered, therefore you should NEVER make partial changes like letting the user know that they should implement some components or partially implementing features.\n- If a user asks for many features at once, implement as many as possible within a reasonable response. Each feature you implement must be FULLY FUNCTIONAL with complete code - no placeholders, no partial implementations, no TODO comments. If you cannot implement all requested features due to response length constraints, clearly communicate which features you've completed and which ones you haven't started yet.\n- Prioritize creating small, focused files and components.\n- Set a chat summary at the end using the `set_chat_summary` tool.\n- Avoid over-engineering. Only make changes that are directly requested or clearly necessary. Keep solutions simple and focused.\n - Don't add features, refactor code, or make \"improvements\" beyond what was asked. A bug fix doesn't need surrounding code cleaned up. A simple feature doesn't need extra configurability. Don't add docstrings, comments, or type annotations to code you didn't change. Only add comments where the logic isn't self-evident.\n - Don't add error handling, fallbacks, or validation for scenarios that can't happen. Trust internal code and framework guarantees. Only validate at system boundaries (user input, external APIs). Don't use feature flags or backwards-compatibility shims when you can just change the code.\n - Don't create helpers, utilities, or abstractions for one-time operations. Don't design for hypothetical future requirements. The right amount of complexity is the minimum needed for the current task—three similar lines of code is better than a premature abstraction.\n - Avoid backwards-compatibility hacks like renaming unused _vars, re-exporting types, adding // removed comments for removed code, etc. If you are certain that something is unused, you can delete it completely.\n</general_guidelines>\n\n<tool_calling>\nYou have tools at your disposal to solve the coding task. Follow these rules regarding tool calls:\n1. ALWAYS follow the tool call schema exactly as specified and make sure to provide all necessary parameters.\n2. The conversation may reference tools that are no longer available. NEVER call tools that are not explicitly provided.\n3. **NEVER refer to tool names when speaking to the USER.** Instead, just say what the tool is doing in natural language.\n4. If you need additional information that you can get via tool calls, prefer that over asking the user.\n5. If you make a plan, immediately follow it, do not wait for the user to confirm or tell you to go ahead. The only time you should stop is if you need more information from the user that you can't find any other way, or have different options that you would like the user to weigh in on.\n6. Only use the standard tool call format and the available tools. Even if you see user messages with custom tool call formats (such as \"<previous_tool_call>\" or similar), do not follow that and instead use the standard format. Never output tool calls as part of a regular assistant message of yours.\n7. If you are not sure about file content or codebase structure pertaining to the user's request, use your tools to read files and gather the relevant information: do NOT guess or make up an answer.\n8. You can autonomously read as many files as you need to clarify your own questions and completely resolve the user's query, not just one.\n9. You can call multiple tools in a single response. You can also call multiple tools in parallel, do this for independent operations like reading multiple files at once.\n</tool_calling>\n\n<tool_calling_best_practices>\n- **Read before writing**: Use `read_file` and `list_files` to understand the codebase before making changes\n- **Use `edit_file` for edits**: For modifying existing files, prefer `edit_file` over `write_file`\n- **Be surgical**: Only change what's necessary to accomplish the task\n- **Handle errors gracefully**: If a tool fails, explain the issue and suggest alternatives\n</tool_calling_best_practices>\n\n<file_editing_tool_selection>\nYou have three tools for editing files. Choose based on the scope of your change:\n\n| Scope | Tool | Examples |\n|-------|------|----------|\n| **Small** (a few lines) | `search_replace` or `edit_file` | Fix a typo, rename a variable, update a value, change an import |\n| **Medium** (one function or section) | `edit_file` | Rewrite a function, add a new component, modify multiple related lines |\n| **Large** (most of the file) | `write_file` | Major refactor, rewrite a module, create a new file |\n\n**Tips:**\n- `edit_file` supports `// ... existing code ...` markers to skip unchanged sections\n- When in doubt, prefer `search_replace` for precision or `write_file` for simplicity\n\n**Post-edit verification (REQUIRED):**\nAfter every edit, read the file to verify changes applied correctly. If something went wrong, try a different tool and verify again.\n</file_editing_tool_selection>\n\n<development_workflow>\n1. **Understand:** Think about the user's request and the relevant codebase context. Use `grep` and `code_search` search tools extensively (in parallel if independent) to understand file structures, existing code patterns, and conventions. Use `read_file` to understand context and validate any assumptions you may have. If you need to read multiple files, you should make multiple parallel calls to `read_file`.\n2. **Plan:** Build a coherent and grounded (based on the understanding in step 1) plan for how you intend to resolve the user's task. For complex tasks, break them down into smaller, manageable subtasks and use the `update_todos` tool to track your progress. Share an extremely concise yet clear plan with the user if it would help the user understand your thought process.\n3. **Implement:** Use the available tools (e.g., `edit_file`, `write_file`, ...) to act on the plan, strictly adhering to the project's established conventions. When debugging, add targeted console.log statements to trace data flow and identify root causes. **Important:** After adding logs, you must ask the user to interact with the application (e.g., click a button, submit a form, navigate to a page) to trigger the code paths where logs were added—the logs will only be available once that code actually executes.\n4. **Verify:** After making code changes, use `run_type_checks` to verify that the changes are correct and read the file contents to ensure the changes are what you intended.\n5. **Finalize:** After all verification passes, consider the task complete and briefly summarize the changes you made.\n</development_workflow>\n\n# Tech Stack\n- You are building a React application.\n- Use TypeScript.\n- Use React Router. KEEP the routes in src/App.tsx\n- Always put source code in the src folder.\n- Put pages into src/pages/\n- Put components into src/components/\n- The main page (default page) is src/pages/Index.tsx\n- UPDATE the main page to include the new components. OTHERWISE, the user can NOT see any components!\n- ALWAYS try to use the shadcn/ui library.\n- Tailwind CSS: always use Tailwind CSS for styling components. Utilize Tailwind classes extensively for layout, spacing, colors, and other design aspects.\n\nAvailable packages and libraries:\n- The lucide-react package is installed for icons.\n- You ALREADY have ALL the shadcn/ui components and their dependencies installed. So you don't need to install them again.\n- You have ALL the necessary Radix UI components installed.\n- Use prebuilt components from the shadcn/ui library after importing them. Note that these files shouldn't be edited, so make new components if you need to change them.\n\n"
"content": "\n<role>\nYou are Dyad, an AI assistant that creates and modifies web applications. You assist users by chatting with them and making changes to their code in real-time. You understand that users can see a live preview of their application in an iframe on the right side of the screen while you make code changes.\nYou make efficient and effective changes to codebases while following best practices for maintainability and readability. You take pride in keeping things simple and elegant. You are friendly and helpful, always aiming to provide clear explanations. \n</role>\n\n<app_commands>\nDo *not* tell the user to run shell commands. Instead, they can do one of the following commands in the UI:\n\n- **Rebuild**: This will rebuild the app from scratch. First it deletes the node_modules folder and then it re-installs the npm packages and then starts the app server.\n- **Restart**: This will restart the app server.\n- **Refresh**: This will refresh the app preview page.\n\nYou can suggest one of these commands by using the <dyad-command> tag like this:\n<dyad-command type=\"rebuild\"></dyad-command>\n<dyad-command type=\"restart\"></dyad-command>\n<dyad-command type=\"refresh\"></dyad-command>\n\nIf you output one of these commands, tell the user to look for the action button above the chat input.\n</app_commands>\n\n<general_guidelines>\n- All text you output outside of tool use is displayed to the user. Output text to communicate with the user. You can use Github-flavored markdown for formatting.\n- Always reply to the user in the same language they are using.\n- Keep explanations concise and focused\n- If the user asks for help or wants to give feedback, tell them to use the Help button in the bottom left.\n- Be careful not to introduce security vulnerabilities such as command injection, XSS, SQL injection, and other OWASP top 10 vulnerabilities. If you notice that you wrote insecure code, immediately fix it. Prioritize writing safe, secure, and correct code.\n- Before proceeding with any code edits, check whether the user's request has already been implemented. If the requested change has already been made in the codebase, point this out to the user, e.g., \"This feature is already implemented as described.\"\n- Only edit files that are related to the user's request and leave all other files alone.\n- All edits you make on the codebase will directly be built and rendered, therefore you should NEVER make partial changes like letting the user know that they should implement some components or partially implementing features.\n- If a user asks for many features at once, implement as many as possible within a reasonable response. Each feature you implement must be FULLY FUNCTIONAL with complete code - no placeholders, no partial implementations, no TODO comments. If you cannot implement all requested features due to response length constraints, clearly communicate which features you've completed and which ones you haven't started yet.\n- Prioritize creating small, focused files and components.\n- Set a chat summary at the end using the `set_chat_summary` tool.\n- Avoid over-engineering. Only make changes that are directly requested or clearly necessary. Keep solutions simple and focused.\n - Don't add features, refactor code, or make \"improvements\" beyond what was asked. A bug fix doesn't need surrounding code cleaned up. A simple feature doesn't need extra configurability. Don't add docstrings, comments, or type annotations to code you didn't change. Only add comments where the logic isn't self-evident.\n - Don't add error handling, fallbacks, or validation for scenarios that can't happen. Trust internal code and framework guarantees. Only validate at system boundaries (user input, external APIs). Don't use feature flags or backwards-compatibility shims when you can just change the code.\n - Don't create helpers, utilities, or abstractions for one-time operations. Don't design for hypothetical future requirements. The right amount of complexity is the minimum needed for the current task—three similar lines of code is better than a premature abstraction.\n - Avoid backwards-compatibility hacks like renaming unused _vars, re-exporting types, adding // removed comments for removed code, etc. If you are certain that something is unused, you can delete it completely.\n</general_guidelines>\n\n<tool_calling>\nYou have tools at your disposal to solve the coding task. Follow these rules regarding tool calls:\n1. ALWAYS follow the tool call schema exactly as specified and make sure to provide all necessary parameters.\n2. The conversation may reference tools that are no longer available. NEVER call tools that are not explicitly provided.\n3. **NEVER refer to tool names when speaking to the USER.** Instead, just say what the tool is doing in natural language.\n4. If you need additional information that you can get via tool calls, prefer that over asking the user.\n5. If you make a plan, immediately follow it, do not wait for the user to confirm or tell you to go ahead. The only time you should stop is if you need more information from the user that you can't find any other way, or have different options that you would like the user to weigh in on.\n6. Only use the standard tool call format and the available tools. Even if you see user messages with custom tool call formats (such as \"<previous_tool_call>\" or similar), do not follow that and instead use the standard format. Never output tool calls as part of a regular assistant message of yours.\n7. If you are not sure about file content or codebase structure pertaining to the user's request, use your tools to read files and gather the relevant information: do NOT guess or make up an answer.\n8. You can autonomously read as many files as you need to clarify your own questions and completely resolve the user's query, not just one.\n9. You can call multiple tools in a single response. You can also call multiple tools in parallel, do this for independent operations like reading multiple files at once.\n</tool_calling>\n\n<tool_calling_best_practices>\n- **Read before writing**: Use `read_file` and `list_files` to understand the codebase before making changes\n- **Use `edit_file` for edits**: For modifying existing files, prefer `edit_file` over `write_file`\n- **Be surgical**: Only change what's necessary to accomplish the task\n- **Handle errors gracefully**: If a tool fails, explain the issue and suggest alternatives\n</tool_calling_best_practices>\n\n<file_editing_tool_selection>\nYou have three tools for editing files. Choose based on the scope of your change:\n\n| Scope | Tool | Examples |\n|-------|------|----------|\n| **Small** (a few lines) | `search_replace` or `edit_file` | Fix a typo, rename a variable, update a value, change an import |\n| **Medium** (one function or section) | `edit_file` | Rewrite a function, add a new component, modify multiple related lines |\n| **Large** (most of the file) | `write_file` | Major refactor, rewrite a module, create a new file |\n\n**Tips:**\n- `edit_file` supports `// ... existing code ...` markers to skip unchanged sections\n- When in doubt, prefer `search_replace` for precision or `write_file` for simplicity\n\n**Post-edit verification (REQUIRED):**\nAfter every edit, read the file to verify changes applied correctly. If something went wrong, try a different tool and verify again.\n</file_editing_tool_selection>\n\n<development_workflow>\n1. **Understand:** Think about the user's request and the relevant codebase context. Use `grep` and `code_search` search tools extensively (in parallel if independent) to understand file structures, existing code patterns, and conventions. Use `read_file` to understand context and validate any assumptions you may have. If you need to read multiple files, you should make multiple parallel calls to `read_file`.\n2. **Clarify (when needed):** Use `planning_questionnaire` to ask 1-3 focused questions when details are missing. Choose text (open-ended), radio (pick one), or checkbox (pick many) for each question, with 2-3 likely options for radio/checkbox.\n **Use when:** creating a new app/project, the request is vague (e.g. \"Add authentication\"), or there are multiple reasonable interpretations.\n **Skip when:** the request is specific and concrete (e.g. \"Fix the login button\", \"Change color from blue to green\").\n The tool accepts ONLY a `questions` array (no empty objects). It returns the user's answers as the tool result.\n3. **Plan:** Build a coherent and grounded (based on the understanding in steps 1-2) plan for how you intend to resolve the user's task. For complex tasks, break them down into smaller, manageable subtasks and use the `update_todos` tool to track your progress. Share an extremely concise yet clear plan with the user if it would help the user understand your thought process.\n4. **Implement:** Use the available tools (e.g., `edit_file`, `write_file`, ...) to act on the plan, strictly adhering to the project's established conventions. When debugging, add targeted console.log statements to trace data flow and identify root causes. **Important:** After adding logs, you must ask the user to interact with the application (e.g., click a button, submit a form, navigate to a page) to trigger the code paths where logs were added—the logs will only be available once that code actually executes.\n5. **Verify:** After making code changes, use `run_type_checks` to verify that the changes are correct and read the file contents to ensure the changes are what you intended.\n6. **Finalize:** After all verification passes, consider the task complete and briefly summarize the changes you made.\n</development_workflow>\n\n# Tech Stack\n- You are building a React application.\n- Use TypeScript.\n- Use React Router. KEEP the routes in src/App.tsx\n- Always put source code in the src folder.\n- Put pages into src/pages/\n- Put components into src/components/\n- The main page (default page) is src/pages/Index.tsx\n- UPDATE the main page to include the new components. OTHERWISE, the user can NOT see any components!\n- ALWAYS try to use the shadcn/ui library.\n- Tailwind CSS: always use Tailwind CSS for styling components. Utilize Tailwind classes extensively for layout, spacing, colors, and other design aspects.\n\nAvailable packages and libraries:\n- The lucide-react package is installed for icons.\n- You ALREADY have ALL the shadcn/ui components and their dependencies installed. So you don't need to install them again.\n- You have ALL the necessary Radix UI components installed.\n- Use prebuilt components from the shadcn/ui library after importing them. Note that these files shouldn't be edited, so make new components if you need to change them.\n\n"
},
{
"role": "user",
......@@ -491,6 +491,71 @@
},
"additionalProperties": false
}
},
{
"type": "function",
"name": "planning_questionnaire",
"description": "Present a structured questionnaire to gather requirements from the user. The tool displays questions in the UI and waits for the user's responses, returning them as the tool result.\n\n<when_to_use>\nUse this tool when:\n- The user wants to create a NEW app or project\n- The request is vague or open-ended\n- There are multiple reasonable interpretations\nSkip when the request is a specific, concrete change.\n</when_to_use>\n\n<input_schema>\nThe tool accepts ONLY a \"questions\" array.\n\nEach question object has these fields:\n- \"question\" (string, REQUIRED): The question text shown to the user\n- \"type\" (string, REQUIRED): One of \"text\", \"radio\", or \"checkbox\"\n- \"options\" (string array, REQUIRED for radio/checkbox, OMIT for text): 1-3 predefined choices\n- \"id\" (string, optional): Unique identifier, auto-generated if omitted\n- \"required\" (boolean, optional): Defaults to true\n- \"placeholder\" (string, optional): Placeholder for text inputs\n</input_schema>\n\n<correct_example>\nReasoning: The user asked to \"build me a todo app\". I need to clarify the tech stack and key features. I'll use radio for single-choice and checkbox for multi-choice.\n\n{\n \"questions\": [\n {\n \"type\": \"radio\",\n \"question\": \"What visual style do you prefer?\",\n \"options\": [\"Minimal & clean\", \"Colorful & playful\", \"Dark & modern\"]\n },\n {\n \"type\": \"checkbox\",\n \"question\": \"Which features do you want?\",\n \"options\": [\"Due dates\", \"Categories/tags\", \"Priority levels\"]\n }\n ]\n}\n</correct_example>\n\n<incorrect_examples>\nWRONG — Empty questions array:\n{ \"questions\": [] }\n\nWRONG — options on text type:\n{ \"type\": \"text\", \"question\": \"...\", \"options\": [\"a\"] }\n\nWRONG — Empty options array:\n{ \"type\": \"radio\", \"question\": \"...\", \"options\": [] }\n\nWRONG — Missing options for radio:\n{ \"type\": \"radio\", \"question\": \"...\" }\n\nWRONG — More than 3 questions or more than 3 options\n\nWRONG — Array with empty object (missing required \"question\" and \"type\" fields):\n{ \"questions\": [{}] }\n</incorrect_examples>",
"parameters": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"questions": {
"minItems": 1,
"maxItems": 3,
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"description": "Unique identifier for this question (auto-generated if omitted)",
"type": "string"
},
"question": {
"type": "string",
"description": "The question text to display to the user"
},
"type": {
"type": "string",
"enum": [
"text",
"radio",
"checkbox"
],
"description": "text for free-form input, radio for single choice, checkbox for multiple choice"
},
"options": {
"description": "Options for radio/checkbox questions. Keep to max 3 — users can always provide a custom answer via the free-form text input. Omit for text questions.",
"minItems": 1,
"maxItems": 3,
"type": "array",
"items": {
"type": "string"
}
},
"required": {
"description": "Whether this question requires an answer (defaults to true)",
"type": "boolean"
},
"placeholder": {
"description": "Placeholder text for text inputs",
"type": "string"
}
},
"required": [
"question",
"type"
],
"additionalProperties": false
},
"description": "A non empty array of 1-3 questions to present to the user"
}
},
"required": [
"questions"
],
"additionalProperties": false
}
}
],
"tool_choice": "auto",
......
......@@ -507,6 +507,73 @@
"additionalProperties": false
}
}
},
{
"type": "function",
"function": {
"name": "planning_questionnaire",
"description": "Present a structured questionnaire to gather requirements from the user. The tool displays questions in the UI and waits for the user's responses, returning them as the tool result.\n\n<when_to_use>\nUse this tool when:\n- The user wants to create a NEW app or project\n- The request is vague or open-ended\n- There are multiple reasonable interpretations\nSkip when the request is a specific, concrete change.\n</when_to_use>\n\n<input_schema>\nThe tool accepts ONLY a \"questions\" array.\n\nEach question object has these fields:\n- \"question\" (string, REQUIRED): The question text shown to the user\n- \"type\" (string, REQUIRED): One of \"text\", \"radio\", or \"checkbox\"\n- \"options\" (string array, REQUIRED for radio/checkbox, OMIT for text): 1-3 predefined choices\n- \"id\" (string, optional): Unique identifier, auto-generated if omitted\n- \"required\" (boolean, optional): Defaults to true\n- \"placeholder\" (string, optional): Placeholder for text inputs\n</input_schema>\n\n<correct_example>\nReasoning: The user asked to \"build me a todo app\". I need to clarify the tech stack and key features. I'll use radio for single-choice and checkbox for multi-choice.\n\n{\n \"questions\": [\n {\n \"type\": \"radio\",\n \"question\": \"What visual style do you prefer?\",\n \"options\": [\"Minimal & clean\", \"Colorful & playful\", \"Dark & modern\"]\n },\n {\n \"type\": \"checkbox\",\n \"question\": \"Which features do you want?\",\n \"options\": [\"Due dates\", \"Categories/tags\", \"Priority levels\"]\n }\n ]\n}\n</correct_example>\n\n<incorrect_examples>\nWRONG — Empty questions array:\n{ \"questions\": [] }\n\nWRONG — options on text type:\n{ \"type\": \"text\", \"question\": \"...\", \"options\": [\"a\"] }\n\nWRONG — Empty options array:\n{ \"type\": \"radio\", \"question\": \"...\", \"options\": [] }\n\nWRONG — Missing options for radio:\n{ \"type\": \"radio\", \"question\": \"...\" }\n\nWRONG — More than 3 questions or more than 3 options\n\nWRONG — Array with empty object (missing required \"question\" and \"type\" fields):\n{ \"questions\": [{}] }\n</incorrect_examples>",
"parameters": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"questions": {
"minItems": 1,
"maxItems": 3,
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"description": "Unique identifier for this question (auto-generated if omitted)",
"type": "string"
},
"question": {
"type": "string",
"description": "The question text to display to the user"
},
"type": {
"type": "string",
"enum": [
"text",
"radio",
"checkbox"
],
"description": "text for free-form input, radio for single choice, checkbox for multiple choice"
},
"options": {
"description": "Options for radio/checkbox questions. Keep to max 3 — users can always provide a custom answer via the free-form text input. Omit for text questions.",
"minItems": 1,
"maxItems": 3,
"type": "array",
"items": {
"type": "string"
}
},
"required": {
"description": "Whether this question requires an answer (defaults to true)",
"type": "boolean"
},
"placeholder": {
"description": "Placeholder text for text inputs",
"type": "string"
}
},
"required": [
"question",
"type"
],
"additionalProperties": false
},
"description": "A non empty array of 1-3 questions to present to the user"
}
},
"required": [
"questions"
],
"additionalProperties": false
}
}
}
],
"tool_choice": "auto",
......
- paragraph: tc=local-agent/questionnaire
- paragraph: Let me ask you a few questions to understand your requirements.
- img
- text: Questionnaire Responses 1 answered
- img
- img
- text: Single choice
- paragraph: Which framework do you prefer?
- paragraph: Vue
- paragraph: Thanks, I'll wait for your responses before proceeding.
- button "Copy":
- img
- img
- text: claude-opus-4-5
- img
- text: less than a minute ago
- button "Copy Request ID":
- img
- text: ""
- button "Undo":
- img
- text: ""
- button "Retry":
- img
- text: ""
\ No newline at end of file
......@@ -3,32 +3,15 @@
- button "Copy":
- img
- img
- text: Approved
- img
- text: claude-opus-4-5
- img
- text: less than a minute ago
- button "Copy Request ID":
- img
- text: Request ID
- paragraph: "Here are my responses to the questionnaire:"
- paragraph:
- strong: Which framework do you prefer?
- text: Vue
- button "file1.txt file1.txt Edit"
- paragraph: More EOM
- button "Copy":
- img
- img
- text: claude-opus-4-5
- img
- text: less than a minute ago
- button "Copy Request ID":
- img
- text: Request ID
- text: ""
- button "Undo":
- img
- text: Undo
- text: ""
- button "Retry":
- img
- text: Retry
- text: ""
\ No newline at end of file
{
"name": "dyad",
"version": "0.37.0",
"version": "0.37.0-beta.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "dyad",
"version": "0.37.0",
"version": "0.37.0-beta.2",
"license": "MIT",
"dependencies": {
"@ai-sdk/amazon-bedrock": "^4.0.46",
......
......@@ -80,10 +80,14 @@ After every edit, read the file to verify changes applied correctly. If somethin
<development_workflow>
1. **Understand:** Think about the user's request and the relevant codebase context. Use \`grep\` and \`code_search\` search tools extensively (in parallel if independent) to understand file structures, existing code patterns, and conventions. Use \`read_file\` to understand context and validate any assumptions you may have. If you need to read multiple files, you should make multiple parallel calls to \`read_file\`.
2. **Plan:** Build a coherent and grounded (based on the understanding in step 1) plan for how you intend to resolve the user's task. For complex tasks, break them down into smaller, manageable subtasks and use the \`update_todos\` tool to track your progress. Share an extremely concise yet clear plan with the user if it would help the user understand your thought process.
3. **Implement:** Use the available tools (e.g., \`edit_file\`, \`write_file\`, ...) to act on the plan, strictly adhering to the project's established conventions. When debugging, add targeted console.log statements to trace data flow and identify root causes. **Important:** After adding logs, you must ask the user to interact with the application (e.g., click a button, submit a form, navigate to a page) to trigger the code paths where logs were added—the logs will only be available once that code actually executes.
4. **Verify:** After making code changes, use \`run_type_checks\` to verify that the changes are correct and read the file contents to ensure the changes are what you intended.
5. **Finalize:** After all verification passes, consider the task complete and briefly summarize the changes you made.
2. **Clarify (when needed):** Use \`planning_questionnaire\` to ask 1-3 focused questions when details are missing. Choose text (open-ended), radio (pick one), or checkbox (pick many) for each question, with 2-3 likely options for radio/checkbox.
**Use when:** creating a new app/project, the request is vague (e.g. "Add authentication"), or there are multiple reasonable interpretations.
**Skip when:** the request is specific and concrete (e.g. "Fix the login button", "Change color from blue to green").
The tool accepts ONLY a \`questions\` array (no empty objects). It returns the user's answers as the tool result.
3. **Plan:** Build a coherent and grounded (based on the understanding in steps 1-2) plan for how you intend to resolve the user's task. For complex tasks, break them down into smaller, manageable subtasks and use the \`update_todos\` tool to track your progress. Share an extremely concise yet clear plan with the user if it would help the user understand your thought process.
4. **Implement:** Use the available tools (e.g., \`edit_file\`, \`write_file\`, ...) to act on the plan, strictly adhering to the project's established conventions. When debugging, add targeted console.log statements to trace data flow and identify root causes. **Important:** After adding logs, you must ask the user to interact with the application (e.g., click a button, submit a form, navigate to a page) to trigger the code paths where logs were added—the logs will only be available once that code actually executes.
5. **Verify:** After making code changes, use \`run_type_checks\` to verify that the changes are correct and read the file contents to ensure the changes are what you intended.
6. **Finalize:** After all verification passes, consider the task complete and briefly summarize the changes you made.
</development_workflow>
# Tech Stack
......@@ -249,10 +253,14 @@ After every edit, read the file to verify changes applied correctly. If somethin
<development_workflow>
1. **Understand:** Think about the user's request and the relevant codebase context. Use \`grep\` to search for text patterns and \`list_files\` to understand file structures. Use \`read_file\` to understand context and validate any assumptions you may have. If you need to read multiple files, you should make multiple parallel calls to \`read_file\`.
2. **Plan:** Build a coherent and grounded (based on the understanding in step 1) plan for how you intend to resolve the user's task. For complex tasks, break them down into smaller, manageable subtasks and use the \`update_todos\` tool to track your progress. Share an extremely concise yet clear plan with the user if it would help the user understand your thought process.
3. **Implement:** Use the available tools (e.g., \`search_replace\`, \`write_file\`, ...) to act on the plan, strictly adhering to the project's established conventions. When debugging, add targeted console.log statements to trace data flow and identify root causes. **Important:** After adding logs, you must ask the user to interact with the application (e.g., click a button, submit a form, navigate to a page) to trigger the code paths where logs were added—the logs will only be available once that code actually executes.
4. **Verify:** After making code changes, use \`run_type_checks\` to verify that the changes are correct and read the file contents to ensure the changes are what you intended.
5. **Finalize:** After all verification passes, consider the task complete and briefly summarize the changes you made.
2. **Clarify (when needed):** Use \`planning_questionnaire\` to ask 1-3 focused questions when details are missing. Choose text (open-ended), radio (pick one), or checkbox (pick many) for each question, with 2-3 likely options for radio/checkbox.
**Use when:** creating a new app/project, the request is vague (e.g. "Add authentication"), or there are multiple reasonable interpretations.
**Skip when:** the request is specific and concrete (e.g. "Fix the login button", "Change color from blue to green").
The tool accepts ONLY a \`questions\` array (no empty objects). It returns the user's answers as the tool result.
3. **Plan:** Build a coherent and grounded (based on the understanding in steps 1-2) plan for how you intend to resolve the user's task. For complex tasks, break them down into smaller, manageable subtasks and use the \`update_todos\` tool to track your progress. Share an extremely concise yet clear plan with the user if it would help the user understand your thought process.
4. **Implement:** Use the available tools (e.g., \`search_replace\`, \`write_file\`, ...) to act on the plan, strictly adhering to the project's established conventions. When debugging, add targeted console.log statements to trace data flow and identify root causes. **Important:** After adding logs, you must ask the user to interact with the application (e.g., click a button, submit a form, navigate to a page) to trigger the code paths where logs were added—the logs will only be available once that code actually executes.
5. **Verify:** After making code changes, use \`run_type_checks\` to verify that the changes are correct and read the file contents to ensure the changes are what you intended.
6. **Finalize:** After all verification passes, consider the task complete and briefly summarize the changes you made.
</development_workflow>
# Tech Stack
......
......@@ -269,6 +269,7 @@ vi.mock("@/pro/main/ipc/handlers/local_agent/tool_definitions", () => ({
buildAgentToolSet: vi.fn(() => ({})),
requireAgentToolConsent: vi.fn(async () => true),
clearPendingConsentsForChat: vi.fn(),
clearPendingQuestionnairesForChat: vi.fn(),
}));
vi.mock(
......@@ -928,6 +929,134 @@ describe("handleLocalAgentStream", () => {
});
});
describe("Synthetic planning_questionnaire reflection", () => {
it("injects a non-persisted reflection message after invalid planning_questionnaire input", async () => {
// Arrange
const { event } = createFakeEvent();
mockSettings = buildTestSettings({ enableDyadPro: true });
mockChatData = buildTestChat({
messages: [{ id: 1, role: "user", content: "Help me plan this app" }],
});
const invalidQuestionnaireInput = {
title: "Project Requirements",
questions: [{}],
};
let secondStepPreparedMessages: any[] | undefined;
mockStreamTextImpl = (options) => {
const firstStepMessages = [
{ role: "user", content: "Help me plan this app" },
];
return {
fullStream: (async function* () {
await options.prepareStep?.({
messages: firstStepMessages,
stepNumber: 0,
steps: [],
model: {},
experimental_context: undefined,
});
await options.onStepFinish?.({
content: [
{
type: "tool-error",
toolName: "planning_questionnaire",
toolCallId: "call_plan_q",
input: invalidQuestionnaireInput,
error:
"Invalid input for tool planning_questionnaire: questions[0].question is required",
},
],
usage: { totalTokens: 1234 },
toolCalls: [
{
type: "tool-call",
toolName: "planning_questionnaire",
toolCallId: "call_plan_q",
input: invalidQuestionnaireInput,
},
],
});
const secondStepMessages = [
...firstStepMessages,
{ role: "assistant", content: "retrying questionnaire call" },
];
const preparedSecondStep = (await options.prepareStep?.({
messages: secondStepMessages,
stepNumber: 1,
steps: [],
model: {},
experimental_context: undefined,
})) ?? { messages: secondStepMessages };
secondStepPreparedMessages = preparedSecondStep.messages;
yield {
type: "text-delta",
text: "I fixed the questionnaire call.",
};
})(),
response: Promise.resolve({
messages: [
{
role: "assistant",
content: [
{ type: "text", text: "I fixed the questionnaire call." },
],
},
],
}),
steps: Promise.resolve([{ toolCalls: [{}] }, { toolCalls: [] }]),
};
};
// Act
await handleLocalAgentStream(
event,
{ chatId: 1, prompt: "test" },
new AbortController(),
{
placeholderMessageId: 10,
systemPrompt: "You are helpful",
dyadRequestId,
},
);
// Assert
expect(secondStepPreparedMessages).toBeDefined();
const reflectionMessage = (secondStepPreparedMessages ?? []).find(
(message: any) =>
message.role === "user" &&
Array.isArray(message.content) &&
message.content.some(
(part: any) =>
part.type === "text" &&
typeof part.text === "string" &&
part.text.includes(
"planning_questionnaire tool call had a format error",
),
),
);
expect(reflectionMessage).toBeDefined();
const aiMessagesUpdate = dbOperations.updates.find(
(u) => u.data.aiMessagesJson !== undefined,
);
expect(aiMessagesUpdate).toBeDefined();
const persistedAiMessages = JSON.stringify(
(aiMessagesUpdate!.data.aiMessagesJson as { messages: unknown[] })
.messages,
);
expect(persistedAiMessages).not.toContain(
"planning_questionnaire tool call had a format error",
);
});
});
describe("Abort handling", () => {
it("should stop processing stream chunks when abort signal is triggered", async () => {
// Arrange
......
......@@ -28,6 +28,12 @@ export interface PendingPlanImplementation {
export const pendingPlanImplementationAtom =
atom<PendingPlanImplementation | null>(null);
export const pendingQuestionnaireAtom = atom<PlanQuestionnairePayload | null>(
null,
);
export const pendingQuestionnaireAtom = atom<
Map<number, PlanQuestionnairePayload>
>(new Map());
// Transient flag: chatIds that just had a questionnaire submitted (for brief confirmation)
// "visible" = showing, "fading" = fade-out in progress
export const questionnaireSubmittedChatIdsAtom = atom<
Map<number, "visible" | "fading">
>(new Map());
......@@ -37,6 +37,7 @@ import { DyadStatus } from "./DyadStatus";
import { DyadCompaction } from "./DyadCompaction";
import { DyadWritePlan } from "./DyadWritePlan";
import { DyadExitPlan } from "./DyadExitPlan";
import { DyadQuestionnaire } from "./DyadQuestionnaire";
import { mapActionToButton } from "./ChatInput";
import { SuggestedAction } from "@/lib/schemas";
import { FixAllErrorsButton } from "./FixAllErrorsButton";
......@@ -76,6 +77,7 @@ const DYAD_CUSTOM_TAGS = [
// Plan mode tags
"dyad-write-plan",
"dyad-exit-plan",
"dyad-questionnaire",
];
interface DyadMarkdownParserProps {
......@@ -762,6 +764,9 @@ function renderCustomTag(
/>
);
case "dyad-questionnaire":
return <DyadQuestionnaire>{content}</DyadQuestionnaire>;
default:
return null;
}
......
import React, { useMemo, useState } from "react";
import {
ChevronLeft,
ChevronRight,
ClipboardList,
CheckCircle2,
MessageSquareText,
CircleDot,
ListChecks,
} from "lucide-react";
import { unescapeXmlAttr, unescapeXmlContent } from "../../../shared/xmlEscape";
interface QAEntry {
question: string;
type: string;
answer: string;
}
interface DyadQuestionnaireProps {
children?: React.ReactNode;
}
function parseQAEntries(content: string): QAEntry[] {
const entries: QAEntry[] = [];
const pattern = /<qa\s+question="([^"]*)"\s+type="([^"]*)">([\s\S]*?)<\/qa>/g;
let match;
while ((match = pattern.exec(content)) !== null) {
entries.push({
question: unescapeXmlAttr(match[1]),
type: unescapeXmlAttr(match[2]),
answer: unescapeXmlContent(match[3].trim()),
});
}
return entries;
}
const TYPE_META: Record<string, { icon: React.ReactNode; label: string }> = {
text: {
icon: <MessageSquareText size={12} />,
label: "Free text",
},
radio: {
icon: <CircleDot size={12} />,
label: "Single choice",
},
checkbox: {
icon: <ListChecks size={12} />,
label: "Multiple choice",
},
};
export function DyadQuestionnaire({ children }: DyadQuestionnaireProps) {
const [currentIndex, setCurrentIndex] = useState(0);
const entries = useMemo(
() => parseQAEntries(typeof children === "string" ? children : ""),
[children],
);
if (entries.length === 0) return null;
const current = entries[currentIndex];
const hasPrev = currentIndex > 0;
const hasNext = currentIndex < entries.length - 1;
const meta = TYPE_META[current.type];
return (
<div className="my-4 border rounded-lg overflow-hidden border-primary/20 bg-primary/5">
{/* Header */}
<div className="px-4 py-3 flex items-center justify-between">
<div className="flex items-center gap-2">
<ClipboardList className="text-primary" size={20} />
<span className="font-semibold text-foreground">
Questionnaire Responses
</span>
<span className="flex items-center text-xs text-primary px-2 py-0.5 bg-primary/10 rounded-md font-medium">
{entries.length} answered
</span>
</div>
<CheckCircle2 className="size-4 text-green-600 dark:text-green-500 shrink-0" />
</div>
{/* Question/Answer content */}
<div className="px-4 pb-4">
<div className="rounded-lg bg-(--background-lightest) dark:bg-zinc-900/60 border border-border/40 overflow-hidden">
{/* Question */}
<div className="px-3.5 pt-3 pb-2.5 bg-muted/40">
<div className="flex items-center gap-1.5 mb-1.5">
{meta && (
<span className="inline-flex items-center gap-1 text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
{meta.icon}
{meta.label}
</span>
)}
</div>
<p className="text-sm font-medium text-foreground leading-relaxed">
{current.question}
</p>
</div>
<div className="h-px bg-border" />
{/* Answer */}
<div className="px-3.5 pt-2.5 pb-3">
<p className="text-xs font-medium uppercase tracking-wider text-muted-foreground mb-1">
Answer
</p>
<p className="text-sm text-foreground/90 leading-relaxed">
{current.answer}
</p>
</div>
</div>
{/* Navigation */}
{entries.length > 1 && (
<div className="flex items-center justify-between mt-3">
{/* Dot indicators */}
<div className="flex items-center gap-1.5">
{entries.map((_, i) => (
<button
key={i}
onClick={() => setCurrentIndex(i)}
className={`rounded-full transition-all duration-200 ${
i === currentIndex
? "w-5 h-1.5 bg-primary"
: "w-1.5 h-1.5 bg-primary/25 hover:bg-primary/40"
}`}
aria-label={`Go to question ${i + 1}`}
/>
))}
</div>
{/* Arrow buttons */}
<div className="flex items-center gap-1">
<button
onClick={() => setCurrentIndex((i) => i - 1)}
disabled={!hasPrev}
className="p-1 rounded-md hover:bg-primary/10 disabled:opacity-25 disabled:cursor-not-allowed transition-colors"
aria-label="Previous question"
>
<ChevronLeft size={16} className="text-muted-foreground" />
</button>
<span className="text-xs text-muted-foreground tabular-nums min-w-[3ch] text-center">
{currentIndex + 1}/{entries.length}
</span>
<button
onClick={() => setCurrentIndex((i) => i + 1)}
disabled={!hasNext}
className="p-1 rounded-md hover:bg-primary/10 disabled:opacity-25 disabled:cursor-not-allowed transition-colors"
aria-label="Next question"
>
<ChevronRight size={16} className="text-muted-foreground" />
</button>
</div>
</div>
)}
</div>
</div>
);
}
......@@ -7,8 +7,9 @@ import { OpenRouterSetupBanner, SetupBanner } from "../SetupBanner";
import { useStreamChat } from "@/hooks/useStreamChat";
import { selectedChatIdAtom } from "@/atoms/chatAtoms";
import { questionnaireSubmittedChatIdsAtom } from "@/atoms/planAtoms";
import { useAtomValue, useSetAtom } from "jotai";
import { Loader2, RefreshCw, Undo } from "lucide-react";
import { CheckCircle2, Loader2, RefreshCw, Undo } from "lucide-react";
import { Button } from "@/components/ui/button";
import { useVersions } from "@/hooks/useVersions";
import { selectedAppIdAtom } from "@/atoms/appAtoms";
......@@ -51,6 +52,7 @@ interface FooterContext {
// Footer component for Virtuoso - receives context via props
function FooterComponent({ context }: { context?: FooterContext }) {
const submittedChatIds = useAtomValue(questionnaireSubmittedChatIdsAtom);
if (!context) return null;
const {
......@@ -72,6 +74,9 @@ function FooterComponent({ context }: { context?: FooterContext }) {
renderSetupBanner,
} = context;
const questionnaireState =
selectedChatId != null ? submittedChatIds.get(selectedChatId) : undefined;
return (
<>
{!isStreaming && (
......@@ -220,6 +225,18 @@ function FooterComponent({ context }: { context?: FooterContext }) {
</div>
)}
{questionnaireState && (
<div
className={`flex justify-start px-4 duration-300 ${questionnaireState === "fading" ? "animate-out fade-out-0 slide-out-to-bottom-2" : "animate-in fade-in-0 slide-in-from-bottom-2"}`}
>
<div className="max-w-3xl w-full mx-auto">
<div className="flex items-center gap-1.5 text-sm text-muted-foreground py-2">
<CheckCircle2 className="h-4 w-4 text-green-500" />
Answers submitted
</div>
</div>
</div>
)}
{isStreaming &&
!settings?.enableDyadPro &&
!userBudget &&
......
import React, { useState, useEffect } from "react";
import { useAtom, useAtomValue } from "jotai";
import { pendingQuestionnaireAtom } from "@/atoms/planAtoms";
import { useStreamChat } from "@/hooks/useStreamChat";
import { useAtom, useAtomValue, useSetAtom } from "jotai";
import {
pendingQuestionnaireAtom,
questionnaireSubmittedChatIdsAtom,
} from "@/atoms/planAtoms";
import { planClient } from "@/ipc/types/plan";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
......@@ -15,15 +18,20 @@ import {
ChevronDown,
ChevronUp,
Circle,
X,
} from "lucide-react";
import { selectedChatIdAtom } from "@/atoms/chatAtoms";
const MAX_DISPLAYED_OPTIONS = 3;
export function QuestionnaireInput() {
const [questionnaire, setQuestionnaire] = useAtom(pendingQuestionnaireAtom);
const [questionnaireMap, setQuestionnaireMap] = useAtom(
pendingQuestionnaireAtom,
);
const setSubmittedChatIds = useSetAtom(questionnaireSubmittedChatIdsAtom);
const chatId = useAtomValue(selectedChatIdAtom);
const { streamMessage, isStreaming } = useStreamChat();
const questionnaire =
chatId != null ? questionnaireMap.get(chatId) : undefined;
// Track current question index
const [currentIndex, setCurrentIndex] = useState(0);
......@@ -57,11 +65,46 @@ export function QuestionnaireInput() {
setIsExpanded(true);
}, [
questionnaire?.chatId,
questionnaire?.title,
questionnaire?.requestId,
questionnaire?.questions?.length,
]);
if (!questionnaire || questionnaire.chatId !== chatId) return null;
const clearQuestionnaire = () => {
if (chatId == null) return;
setQuestionnaireMap((prev) => {
const next = new Map(prev);
next.delete(chatId);
return next;
});
};
const handleDismiss = () => {
if (!questionnaire) return;
planClient.respondToQuestionnaire({
requestId: questionnaire.requestId,
answers: null,
});
clearQuestionnaire();
};
// Auto-dismiss after 5 minutes to match the backend timeout
useEffect(() => {
if (!questionnaire) return;
const timeout = setTimeout(
() => {
planClient.respondToQuestionnaire({
requestId: questionnaire.requestId,
answers: null,
});
clearQuestionnaire();
},
5 * 60 * 1000,
);
return () => clearTimeout(timeout);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [questionnaire?.requestId, chatId]);
if (!questionnaire) return null;
const currentQuestion = questionnaire.questions[currentIndex];
......@@ -76,6 +119,13 @@ export function QuestionnaireInput() {
// Sentinel value for the custom free-form radio option
const CUSTOM_OPTION = "__custom__";
// Fall back to text input if a radio/checkbox question has no options
const effectiveType =
(currentQuestion.type === "radio" || currentQuestion.type === "checkbox") &&
(!currentQuestion.options || currentQuestion.options.length === 0)
? "text"
: currentQuestion.type;
// Get the final response value (combining selected option with additional text)
const getFinalResponse = (questionId: string): string => {
const response = responses[questionId];
......@@ -135,27 +185,36 @@ export function QuestionnaireInput() {
};
const handleSubmit = () => {
if (!chatId) return;
const formattedResponses = questionnaire.questions
.map((q) => {
const answer = getFinalResponse(q.id);
return `**${q.question}**\n${answer}`;
})
.join("\n\n");
streamMessage({
chatId,
prompt: `Here are my responses to the questionnaire:\n\n${formattedResponses}`,
if (!questionnaire || chatId == null) return;
const answers: Record<string, string> = {};
for (const q of questionnaire.questions) {
answers[q.id] = getFinalResponse(q.id);
}
planClient.respondToQuestionnaire({
requestId: questionnaire.requestId,
answers,
});
// Clear questionnaire after submission
setQuestionnaire(null);
clearQuestionnaire();
// Show brief confirmation in message list
setSubmittedChatIds((prev) => new Map([...prev, [chatId, "visible"]]));
setTimeout(() => {
setSubmittedChatIds((prev) => new Map([...prev, [chatId, "fading"]]));
setTimeout(() => {
setSubmittedChatIds((prev) => {
const next = new Map(prev);
next.delete(chatId);
return next;
});
}, 300);
}, 1700);
};
// Helper to determine if Next button should be disabled
const isNextDisabled = () => {
if (isStreaming && isLastQuestion) return true;
if (currentQuestion.required === false) return false;
return !hasValidAnswer();
};
......@@ -171,16 +230,23 @@ export function QuestionnaireInput() {
return (
<div className="border-b border-border bg-muted/30">
<div className="flex items-center">
<button
type="button"
onClick={() => setIsExpanded(!isExpanded)}
className="w-full flex items-center justify-between px-3 py-2 hover:bg-muted/50 transition-colors"
className="flex-1 flex items-center justify-between px-3 py-2 hover:bg-muted/50 transition-colors"
aria-expanded={isExpanded}
aria-label={
isExpanded
? "Collapse questionnaire"
: `Expand questionnaire: ${currentQuestion.question}`
}
>
<div className="flex items-center gap-2.5 min-w-0 flex-1">
{isExpanded ? (
<>
<ClipboardList className="w-4 h-4 text-muted-foreground flex-shrink-0" />
<span className="text-sm">{questionnaire.title}</span>
<span className="text-sm">Questions</span>
</>
) : (
<>
......@@ -205,6 +271,16 @@ export function QuestionnaireInput() {
)}
</div>
</button>
<Button
onClick={handleDismiss}
variant="ghost"
size="icon"
className="h-7 w-7 text-muted-foreground flex-shrink-0 mr-1.5"
aria-label="Dismiss questionnaire"
>
<X size={14} />
</Button>
</div>
{isExpanded && (
<div className="px-3 pb-3">
......@@ -217,17 +293,13 @@ export function QuestionnaireInput() {
<span className="text-red-500 ml-1">*</span>
)}
</Label>
{currentQuestion.placeholder && (
<p className="text-xs text-muted-foreground">
{currentQuestion.placeholder}
</p>
)}
<div className="mt-2">
{currentQuestion.type === "text" && (
{effectiveType === "text" && (
<Input
autoFocus
placeholder="Type your answer..."
placeholder={
currentQuestion.placeholder || "Type your answer..."
}
value={(responses[currentQuestion.id] as string) || ""}
onChange={(e) =>
setResponses((prev) => ({
......@@ -239,11 +311,10 @@ export function QuestionnaireInput() {
/>
)}
{currentQuestion.type === "radio" &&
currentQuestion.options && (
{effectiveType === "radio" && currentQuestion.options && (
<RadioGroup
value={(responses[currentQuestion.id] as string) || ""}
onValueChange={(value) => {
onValueChange={(value: string) => {
setResponses((prev) => ({
...prev,
[currentQuestion.id]: value,
......@@ -311,8 +382,7 @@ export function QuestionnaireInput() {
</RadioGroup>
)}
{currentQuestion.type === "checkbox" &&
currentQuestion.options && (
{effectiveType === "checkbox" && currentQuestion.options && (
<div className="space-y-0.5">
{currentQuestion.options
.slice(0, MAX_DISPLAYED_OPTIONS)
......@@ -337,10 +407,7 @@ export function QuestionnaireInput() {
if (checked) {
return {
...prev,
[currentQuestion.id]: [
...current,
option,
],
[currentQuestion.id]: [...current, option],
};
}
return {
......
......@@ -172,7 +172,11 @@ export function usePlanEvents() {
// Handle questionnaire events
const unsubscribeQuestionnaire = planEventClient.onQuestionnaire(
(payload: PlanQuestionnairePayload) => {
setPendingQuestionnaire(payload);
setPendingQuestionnaire((prev) => {
const next = new Map(prev);
next.set(payload.chatId, payload);
return next;
});
},
);
......
......@@ -7,6 +7,7 @@ import { getDyadAppPath } from "../../paths/paths";
import log from "electron-log";
import { createTypedHandler } from "./base";
import { planContracts } from "../types/plan";
import { resolveQuestionnaireResponse } from "../../pro/main/ipc/handlers/local_agent/tool_definitions";
import {
slugify,
buildFrontmatter,
......@@ -150,4 +151,11 @@ export function registerPlanHandlers() {
}
logger.info("Deleted plan:", planId);
});
createTypedHandler(
planContracts.respondToQuestionnaire,
async (_, params) => {
resolveQuestionnaireResponse(params.requestId, params.answers);
},
);
}
......@@ -23,39 +23,39 @@ export const PlanExitSchema = z.object({
export type PlanExitPayload = z.infer<typeof PlanExitSchema>;
const TextQuestionSchema = z.object({
export const QuestionSchema = z
.object({
id: z.string(),
type: z.literal("text"),
type: z.enum(["text", "radio", "checkbox"]),
question: z.string(),
options: z.array(z.string()).min(1).optional(),
required: z.boolean().optional(),
placeholder: z.string().optional(),
});
const MultipleChoiceQuestionSchema = z.object({
id: z.string(),
type: z.enum(["radio", "checkbox"]),
question: z.string(),
options: z.array(z.string()).min(1),
required: z.boolean().optional(),
placeholder: z.string().optional(),
});
export const QuestionSchema = z.union([
TextQuestionSchema,
MultipleChoiceQuestionSchema,
]);
})
.refine((q) => q.type === "text" || (q.options && q.options.length >= 1), {
message: "options are required for radio and checkbox questions",
path: ["options"],
});
export type Question = z.infer<typeof QuestionSchema>;
export const PlanQuestionnaireSchema = z.object({
chatId: z.number(),
title: z.string(),
description: z.string().optional(),
requestId: z.string(),
questions: z.array(QuestionSchema),
});
export type PlanQuestionnairePayload = z.infer<typeof PlanQuestionnaireSchema>;
export const QuestionnaireResponseSchema = z.object({
requestId: z.string(),
answers: z.record(z.string(), z.string()).nullable(),
});
export type QuestionnaireResponsePayload = z.infer<
typeof QuestionnaireResponseSchema
>;
export const PlanSchema = z.object({
id: z.string(),
appId: z.number(),
......@@ -140,6 +140,12 @@ export const planContracts = {
input: z.object({ appId: z.number(), planId: z.string() }),
output: z.void(),
}),
respondToQuestionnaire: defineContract({
channel: "plan:questionnaire-response",
input: QuestionnaireResponseSchema,
output: z.void(),
}),
} as const;
// Plan Clients
......
......@@ -31,6 +31,7 @@ import {
buildAgentToolSet,
requireAgentToolConsent,
clearPendingConsentsForChat,
clearPendingQuestionnairesForChat,
} from "./tool_definitions";
import {
deployAllFunctionsIfNeeded,
......@@ -64,7 +65,6 @@ import {
} from "@/ipc/utils/ai_messages_utils";
import { parseMcpToolKey, sanitizeMcpName } from "@/ipc/utils/mcp_tool_utils";
import { addIntegrationTool } from "./tools/add_integration";
import { planningQuestionnaireTool } from "./tools/planning_questionnaire";
import { writePlanTool } from "./tools/write_plan";
import { exitPlanTool } from "./tools/exit_plan";
import {
......@@ -75,6 +75,7 @@ import {
import { getPostCompactionMessages } from "@/ipc/handlers/compaction/compaction_utils";
const logger = log.scope("local_agent_handler");
const PLANNING_QUESTIONNAIRE_TOOL_NAME = "planning_questionnaire";
// ============================================================================
// Tool Streaming State Management
......@@ -488,7 +489,11 @@ export async function handleLocalAgentStream(
// In read-only mode, only include read-only tools and skip MCP tools
// (since we can't determine if MCP tools modify state)
// In plan mode, only include planning tools (read + questionnaire/plan tools)
const agentTools = buildAgentToolSet(ctx, { readOnly, planModeOnly });
const agentTools = buildAgentToolSet(ctx, {
readOnly,
planModeOnly,
basicAgentMode: !readOnly && !planModeOnly && isBasicAgentMode(settings),
});
const mcpTools =
readOnly || planModeOnly ? {} : await getMcpTools(event, ctx);
const allTools: ToolSet = { ...agentTools, ...mcpTools };
......@@ -514,6 +519,7 @@ export async function handleLocalAgentStream(
// there are still incomplete todos, we append a reminder and do another pass.
const maxTodoFollowUpLoops = 1;
let todoFollowUpLoops = 0;
let hasInjectedPlanningQuestionnaireReflection = false;
let currentMessageHistory = messageHistory;
const accumulatedAiMessages: ModelMessage[] = [];
......@@ -551,17 +557,9 @@ export async function handleLocalAgentStream(
stopWhen: [
stepCountIs(25),
hasToolCall(addIntegrationTool.name),
// In plan mode, stop immediately after presenting a questionnaire,
// writing a plan, or exiting plan mode so the agent yields control
// back to the user. Without this, some models (e.g. Gemini Pro 3)
// ignore the prompt-level "STOP" instruction and keep calling tools
// in a loop.
// In plan mode, also stop after writing a plan or exiting plan mode.
...(planModeOnly
? [
hasToolCall(planningQuestionnaireTool.name),
hasToolCall(writePlanTool.name),
hasToolCall(exitPlanTool.name),
]
? [hasToolCall(writePlanTool.name), hasToolCall(exitPlanTool.name)]
: []),
],
abortSignal: abortController.signal,
......@@ -627,13 +625,32 @@ export async function handleLocalAgentStream(
// injections/cleanups to apply. If we already replaced the base
// message history (e.g., after mid-turn compaction), we still need
// to return the updated options.
if (preparedStep) {
return preparedStep;
}
let result =
preparedStep ?? (stepOptions === options ? undefined : stepOptions);
return stepOptions === options ? undefined : stepOptions;
return result;
},
onStepFinish: async (step) => {
if (!hasInjectedPlanningQuestionnaireReflection) {
const questionnaireError =
getPlanningQuestionnaireErrorFromStep(step);
if (questionnaireError) {
pendingUserMessages.push([
{
type: "text",
text: buildPlanningQuestionnaireReflectionMessage(
questionnaireError,
planModeOnly,
),
},
]);
hasInjectedPlanningQuestionnaireReflection = true;
logger.info(
`Injected synthetic planning_questionnaire reflection message for chat ${req.chatId}`,
);
}
}
if (
settings.enableContextCompaction === false ||
compactedMidTurn ||
......@@ -700,8 +717,9 @@ export async function handleLocalAgentStream(
for await (const part of streamResult.fullStream) {
if (abortController.signal.aborted) {
logger.log(`Stream aborted for chat ${req.chatId}`);
// Clean up pending consent requests to prevent stale UI banners
// Clean up pending consent/questionnaire requests to prevent stale UI banners
clearPendingConsentsForChat(req.chatId);
clearPendingQuestionnairesForChat(req.chatId);
break;
}
......@@ -960,9 +978,10 @@ export async function handleLocalAgentStream(
return true; // Success
} catch (error) {
// Clean up any pending consent requests for this chat to prevent
// Clean up any pending consent/questionnaire requests for this chat to prevent
// stale UI banners and orphaned promises
clearPendingConsentsForChat(req.chatId);
clearPendingQuestionnairesForChat(req.chatId);
if (abortController.signal.aborted) {
// Handle cancellation
......@@ -1018,6 +1037,50 @@ function sendResponseChunk(
});
}
function getPlanningQuestionnaireErrorFromStep(step: {
content?: unknown;
}): string | null {
if (!Array.isArray(step.content)) {
return null;
}
for (const part of step.content) {
if (!isRecord(part) || part.toolName !== PLANNING_QUESTIONNAIRE_TOOL_NAME) {
continue;
}
if (part.type === "tool-error") {
return typeof part.error === "string" ? part.error : "Unknown tool error";
}
if (
part.type === "tool-result" &&
typeof part.output === "string" &&
part.output.startsWith("Error:")
) {
return part.output;
}
}
return null;
}
function buildPlanningQuestionnaireReflectionMessage(
errorDetail?: string,
planModeOnly?: boolean,
): string {
const base = "Your planning_questionnaire tool call had a format error.";
const detail = errorDetail ? ` The error was: ${errorDetail}` : "";
if (planModeOnly) {
return `[System]${base}${detail} Review the tool's input schema, fix the issue, and re-call planning_questionnaire with correct arguments.`;
}
return `[System]${base}${detail} Skip the questionnaire step and proceed directly to the planning phase.`;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}
function shouldRunTodoFollowUpPass(params: {
readOnly: boolean;
planModeOnly: boolean;
......
......@@ -121,6 +121,69 @@ export function clearPendingConsentsForChat(chatId: number): void {
}
}
// ============================================================================
// Questionnaire Response Management
// ============================================================================
interface PendingQuestionnaireEntry {
chatId: number;
resolve: (answers: Record<string, string> | null) => void;
}
const pendingQuestionnaireResolvers = new Map<
string,
PendingQuestionnaireEntry
>();
const QUESTIONNAIRE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
export function waitForQuestionnaireResponse(
requestId: string,
chatId: number,
): Promise<Record<string, string> | null> {
return new Promise((resolve) => {
const timeout = setTimeout(() => {
const entry = pendingQuestionnaireResolvers.get(requestId);
if (entry) {
pendingQuestionnaireResolvers.delete(requestId);
entry.resolve(null);
}
}, QUESTIONNAIRE_TIMEOUT_MS);
pendingQuestionnaireResolvers.set(requestId, {
chatId,
resolve: (answers) => {
clearTimeout(timeout);
resolve(answers);
},
});
});
}
export function resolveQuestionnaireResponse(
requestId: string,
answers: Record<string, string> | null,
) {
const entry = pendingQuestionnaireResolvers.get(requestId);
if (entry) {
pendingQuestionnaireResolvers.delete(requestId);
entry.resolve(answers);
}
}
/**
* Clean up all pending questionnaire requests for a given chat.
* Called when a stream is cancelled/aborted to prevent orphaned promises.
*/
export function clearPendingQuestionnairesForChat(chatId: number): void {
for (const [requestId, entry] of pendingQuestionnaireResolvers) {
if (entry.chatId === chatId) {
pendingQuestionnaireResolvers.delete(requestId);
entry.resolve(null);
}
}
}
export function getDefaultConsent(toolName: AgentToolName): AgentToolConsent {
const tool = TOOL_DEFINITIONS.find((t) => t.name === toolName);
return tool?.defaultConsent ?? "ask";
......@@ -275,6 +338,11 @@ export interface BuildAgentToolSetOptions {
* Plan mode has access to read-only tools plus planning-specific tools.
*/
planModeOnly?: boolean;
/**
* If true, exclude Pro-only tools.
* Used for basic agent mode where some tools may not be available.
*/
basicAgentMode?: boolean;
}
const FILE_EDIT_TOOLS: Set<FileEditToolName> = new Set(FILE_EDIT_TOOL_NAMES);
......@@ -305,15 +373,26 @@ function trackFileEditTool(
}
/**
* Planning-specific tools that are only available in plan mode.
* In plan mode, all non-state-modifying tools are also included automatically.
* Tools that should ONLY be available in plan mode (excluded from normal agent mode).
* Note: planning_questionnaire is intentionally omitted so it's available in pro agent mode too.
*/
const PLAN_MODE_ONLY_TOOLS = new Set(["write_plan", "exit_plan"]);
/**
* Planning-specific tools that are allowed in plan mode despite modifying state.
* Superset of PLAN_MODE_ONLY_TOOLS plus tools that participate in planning
* but are also available in normal (pro) agent mode.
*/
const PLANNING_SPECIFIC_TOOLS = new Set([
...PLAN_MODE_ONLY_TOOLS,
"planning_questionnaire",
"write_plan",
"exit_plan",
]);
/**
* Tools only available in Pro agent mode (excluded from basic agent mode).
*/
const PRO_AGENT_ONLY_TOOLS = new Set<string>();
/**
* Build ToolSet for AI SDK from tool definitions
*/
......@@ -338,8 +417,13 @@ export function buildAgentToolSet(
continue;
}
// Skip planning-specific tools when NOT in plan mode
if (!options.planModeOnly && PLANNING_SPECIFIC_TOOLS.has(tool.name)) {
// Skip plan-mode-only tools when NOT in plan mode
if (!options.planModeOnly && PLAN_MODE_ONLY_TOOLS.has(tool.name)) {
continue;
}
// Skip Pro-only tools in basic agent mode
if (options.basicAgentMode && PRO_AGENT_ONLY_TOOLS.has(tool.name)) {
continue;
}
......
import { z } from "zod";
import crypto from "node:crypto";
import log from "electron-log";
import { ToolDefinition, AgentContext } from "./types";
import { safeSend } from "@/ipc/utils/safe_sender";
import { waitForQuestionnaireResponse } from "../tool_definitions";
import {
escapeXmlAttr,
escapeXmlContent,
} from "../../../../../../../shared/xmlEscape";
const logger = log.scope("planning_questionnaire");
const BaseQuestionFields = {
id: z.string().describe("Unique identifier for this question"),
question: z.string().describe("The question text to display to the user"),
required: z
.boolean()
.optional()
.describe("Whether this question requires an answer (defaults to true)"),
placeholder: z
const QuestionSchema = z
.object({
id: z
.string()
.optional()
.describe("Placeholder text for text inputs"),
};
const TextQuestionSchema = z.object({
...BaseQuestionFields,
type: z.literal("text"),
});
const MultipleChoiceQuestionSchema = z.object({
...BaseQuestionFields,
.describe(
"Unique identifier for this question (auto-generated if omitted)",
),
question: z.string().describe("The question text to display to the user"),
type: z
.enum(["radio", "checkbox"])
.describe("radio for single choice, checkbox for multiple choice"),
.enum(["text", "radio", "checkbox"])
.describe(
"text for free-form input, radio for single choice, checkbox for multiple choice",
),
options: z
.array(z.string())
.min(1)
.max(3)
.optional()
.describe(
"Options for the question. Keep to max 3 — users can always provide a custom answer via the free-form text input.",
"Options for radio/checkbox questions. Keep to max 3 — users can always provide a custom answer via the free-form text input. Omit for text questions.",
),
});
const QuestionSchema = z.union([
TextQuestionSchema,
MultipleChoiceQuestionSchema,
]);
const planningQuestionnaireSchema = z.object({
title: z.string().describe("Title of this questionnaire section"),
description: z
required: z
.boolean()
.optional()
.describe("Whether this question requires an answer (defaults to true)"),
placeholder: z
.string()
.optional()
.describe(
"Brief description or context for why these questions are being asked",
),
.describe("Placeholder text for text inputs"),
})
.refine((q) => q.type === "text" || (q.options && q.options.length >= 1), {
message: "options are required for radio and checkbox questions",
path: ["options"],
});
const planningQuestionnaireSchema = z.object({
questions: z
.array(QuestionSchema)
.min(1)
.max(3)
.describe("Array of 1-3 questions to present to the user"),
.min(1, "questions array must not be empty")
.max(3, "questions array must have at most 3 questions")
.describe("A non empty array of 1-3 questions to present to the user"),
});
const DESCRIPTION = `
Present a structured questionnaire to gather requirements from the user during the planning phase.
**CRITICAL**: After calling this tool, you MUST STOP and wait for the user's responses before proceeding. Do NOT create a plan or take further action until the user has answered all questions. The user's responses will be sent as a follow-up message.
const DESCRIPTION = `Present a structured questionnaire to gather requirements from the user. The tool displays questions in the UI and waits for the user's responses, returning them as the tool result.
Use this tool to collect specific information about:
- Feature requirements and expected behavior
- Technology preferences or constraints
- Design and UX choices
- Priority decisions
- Edge cases and error handling expectations
<when_to_use>
Use this tool when:
- The user wants to create a NEW app or project
- The request is vague or open-ended
- There are multiple reasonable interpretations
Skip when the request is a specific, concrete change.
</when_to_use>
Question Types:
- \`text\`: Free-form text input for open-ended questions
- \`radio\`: Single choice from multiple options (with additional free-form text input)
- \`checkbox\`: Multiple choice (with additional free-form text input)
<input_schema>
The tool accepts ONLY a "questions" array.
**NOTE**: All question types (except pure text) include a free-form text input where users can provide custom answers or additional details. This ensures users are never limited to just the predefined options.
Each question object has these fields:
- "question" (string, REQUIRED): The question text shown to the user
- "type" (string, REQUIRED): One of "text", "radio", or "checkbox"
- "options" (string array, REQUIRED for radio/checkbox, OMIT for text): 1-3 predefined choices
- "id" (string, optional): Unique identifier, auto-generated if omitted
- "required" (boolean, optional): Defaults to true
- "placeholder" (string, optional): Placeholder for text inputs
</input_schema>
Best Practices:
- Ask 1-3 focused questions at a time
- Keep options to a maximum of 3 per question — users can always type a custom answer
- Users can always type a custom answer, so you don't need to cover every possible option
- Group related questions together
- Provide clear options when using radio/checkbox
- Explain why you're asking if it's not obvious
<correct_example>
Reasoning: The user asked to "build me a todo app". I need to clarify the tech stack and key features. I'll use radio for single-choice and checkbox for multi-choice.
Example:
{
"title": "Authentication Preferences",
"description": "Help me understand your authentication requirements",
"questions": [
{
"id": "auth_method",
"type": "radio",
"question": "Which authentication method would you prefer?",
"options": ["Email/Password", "OAuth (Google, GitHub)", "Magic Link"],
"required": true
"question": "What visual style do you prefer?",
"options": ["Minimal & clean", "Colorful & playful", "Dark & modern"]
},
{
"type": "checkbox",
"question": "Which features do you want?",
"options": ["Due dates", "Categories/tags", "Priority levels"]
}
]
}
`;
</correct_example>
<incorrect_examples>
WRONG — Empty questions array:
{ "questions": [] }
WRONG — options on text type:
{ "type": "text", "question": "...", "options": ["a"] }
WRONG — Empty options array:
{ "type": "radio", "question": "...", "options": [] }
WRONG — Missing options for radio:
{ "type": "radio", "question": "..." }
WRONG — More than 3 questions or more than 3 options
WRONG — Array with empty object (missing required "question" and "type" fields):
{ "questions": [{}] }
</incorrect_examples>`;
export const planningQuestionnaireTool: ToolDefinition<
z.infer<typeof planningQuestionnaireSchema>
......@@ -110,20 +125,52 @@ export const planningQuestionnaireTool: ToolDefinition<
modifiesState: true,
getConsentPreview: (args) =>
`Questionnaire: ${args.title} (${args.questions.length} questions)`,
`Questionnaire (${args.questions.length} questions)`,
execute: async (args, ctx: AgentContext) => {
const requestId = `questionnaire:${crypto.randomUUID()}`;
// Auto-generate missing IDs
const questions = args.questions.map((q) => ({
...q,
id: q.id || `q_${crypto.randomUUID().slice(0, 8)}`,
}));
logger.log(
`Presenting questionnaire: ${args.title} (${args.questions.length} questions)`,
`Presenting questionnaire (${questions.length} questions), requestId: ${requestId}`,
);
safeSend(ctx.event.sender, "plan:questionnaire", {
chatId: ctx.chatId,
title: args.title,
description: args.description,
questions: args.questions,
requestId,
questions,
});
return `Questionnaire "${args.title}" presented to the user. STOP HERE and wait for the user to respond. Do NOT create a plan or continue until you receive the user's answers in a follow-up message.`;
const answers = await waitForQuestionnaireResponse(requestId, ctx.chatId);
if (!answers) {
return "The user dismissed the questionnaire without answering. Ask them how they'd like to proceed, or try asking questions in regular chat text.";
}
const formattedAnswers = questions
.map((q) => {
const answer = answers[q.id] || "(no answer)";
return `**${q.question}**\n${answer}`;
})
.join("\n\n");
// Build XML with questions and answers for the chat UI
const qaEntries = questions
.map((q) => {
const answer = answers[q.id] || "(no answer)";
return `<qa question="${escapeXmlAttr(q.question)}" type="${escapeXmlAttr(q.type)}">${escapeXmlContent(answer)}</qa>`;
})
.join("\n");
ctx.onXmlComplete(
`<dyad-questionnaire count="${questions.length}">\n${qaEntries}\n</dyad-questionnaire>`,
);
return `User responses:\n\n${formattedAnswers}`;
},
};
......@@ -92,10 +92,14 @@ After every edit, read the file to verify changes applied correctly. If somethin
const PRO_DEVELOPMENT_WORKFLOW_BLOCK = `<development_workflow>
1. **Understand:** Think about the user's request and the relevant codebase context. Use \`grep\` and \`code_search\` search tools extensively (in parallel if independent) to understand file structures, existing code patterns, and conventions. Use \`read_file\` to understand context and validate any assumptions you may have. If you need to read multiple files, you should make multiple parallel calls to \`read_file\`.
2. **Plan:** Build a coherent and grounded (based on the understanding in step 1) plan for how you intend to resolve the user's task. For complex tasks, break them down into smaller, manageable subtasks and use the \`update_todos\` tool to track your progress. Share an extremely concise yet clear plan with the user if it would help the user understand your thought process.
3. **Implement:** Use the available tools (e.g., \`edit_file\`, \`write_file\`, ...) to act on the plan, strictly adhering to the project's established conventions. When debugging, add targeted console.log statements to trace data flow and identify root causes. **Important:** After adding logs, you must ask the user to interact with the application (e.g., click a button, submit a form, navigate to a page) to trigger the code paths where logs were added—the logs will only be available once that code actually executes.
4. **Verify:** After making code changes, use \`run_type_checks\` to verify that the changes are correct and read the file contents to ensure the changes are what you intended.
5. **Finalize:** After all verification passes, consider the task complete and briefly summarize the changes you made.
2. **Clarify (when needed):** Use \`planning_questionnaire\` to ask 1-3 focused questions when details are missing. Choose text (open-ended), radio (pick one), or checkbox (pick many) for each question, with 2-3 likely options for radio/checkbox.
**Use when:** creating a new app/project, the request is vague (e.g. "Add authentication"), or there are multiple reasonable interpretations.
**Skip when:** the request is specific and concrete (e.g. "Fix the login button", "Change color from blue to green").
The tool accepts ONLY a \`questions\` array (no empty objects). It returns the user's answers as the tool result.
3. **Plan:** Build a coherent and grounded (based on the understanding in steps 1-2) plan for how you intend to resolve the user's task. For complex tasks, break them down into smaller, manageable subtasks and use the \`update_todos\` tool to track your progress. Share an extremely concise yet clear plan with the user if it would help the user understand your thought process.
4. **Implement:** Use the available tools (e.g., \`edit_file\`, \`write_file\`, ...) to act on the plan, strictly adhering to the project's established conventions. When debugging, add targeted console.log statements to trace data flow and identify root causes. **Important:** After adding logs, you must ask the user to interact with the application (e.g., click a button, submit a form, navigate to a page) to trigger the code paths where logs were added—the logs will only be available once that code actually executes.
5. **Verify:** After making code changes, use \`run_type_checks\` to verify that the changes are correct and read the file contents to ensure the changes are what you intended.
6. **Finalize:** After all verification passes, consider the task complete and briefly summarize the changes you made.
</development_workflow>`;
// ============================================================================
......@@ -126,10 +130,14 @@ After every edit, read the file to verify changes applied correctly. If somethin
const BASIC_DEVELOPMENT_WORKFLOW_BLOCK = `<development_workflow>
1. **Understand:** Think about the user's request and the relevant codebase context. Use \`grep\` to search for text patterns and \`list_files\` to understand file structures. Use \`read_file\` to understand context and validate any assumptions you may have. If you need to read multiple files, you should make multiple parallel calls to \`read_file\`.
2. **Plan:** Build a coherent and grounded (based on the understanding in step 1) plan for how you intend to resolve the user's task. For complex tasks, break them down into smaller, manageable subtasks and use the \`update_todos\` tool to track your progress. Share an extremely concise yet clear plan with the user if it would help the user understand your thought process.
3. **Implement:** Use the available tools (e.g., \`search_replace\`, \`write_file\`, ...) to act on the plan, strictly adhering to the project's established conventions. When debugging, add targeted console.log statements to trace data flow and identify root causes. **Important:** After adding logs, you must ask the user to interact with the application (e.g., click a button, submit a form, navigate to a page) to trigger the code paths where logs were added—the logs will only be available once that code actually executes.
4. **Verify:** After making code changes, use \`run_type_checks\` to verify that the changes are correct and read the file contents to ensure the changes are what you intended.
5. **Finalize:** After all verification passes, consider the task complete and briefly summarize the changes you made.
2. **Clarify (when needed):** Use \`planning_questionnaire\` to ask 1-3 focused questions when details are missing. Choose text (open-ended), radio (pick one), or checkbox (pick many) for each question, with 2-3 likely options for radio/checkbox.
**Use when:** creating a new app/project, the request is vague (e.g. "Add authentication"), or there are multiple reasonable interpretations.
**Skip when:** the request is specific and concrete (e.g. "Fix the login button", "Change color from blue to green").
The tool accepts ONLY a \`questions\` array (no empty objects). It returns the user's answers as the tool result.
3. **Plan:** Build a coherent and grounded (based on the understanding in steps 1-2) plan for how you intend to resolve the user's task. For complex tasks, break them down into smaller, manageable subtasks and use the \`update_todos\` tool to track your progress. Share an extremely concise yet clear plan with the user if it would help the user understand your thought process.
4. **Implement:** Use the available tools (e.g., \`search_replace\`, \`write_file\`, ...) to act on the plan, strictly adhering to the project's established conventions. When debugging, add targeted console.log statements to trace data flow and identify root causes. **Important:** After adding logs, you must ask the user to interact with the application (e.g., click a button, submit a form, navigate to a page) to trigger the code paths where logs were added—the logs will only be available once that code actually executes.
5. **Verify:** After making code changes, use \`run_type_checks\` to verify that the changes are correct and read the file contents to ensure the changes are what you intended.
6. **Finalize:** After all verification passes, consider the task complete and briefly summarize the changes you made.
</development_workflow>`;
// ============================================================================
......
......@@ -15,7 +15,11 @@ Your goal is to have a thoughtful brainstorming session with the user to fully u
2. **Explore the Codebase**: Use read-only tools (read_file, list_files, grep, code_search) to examine the existing codebase structure, patterns, and relevant files.
3. **Ask Clarifying Questions**: Use the \`planning_questionnaire\` tool to ask targeted questions about:
3. **Ask Clarifying Questions**: Use the \`planning_questionnaire\` tool to ask targeted questions. The tool accepts only a \`questions\` array and returns the user's responses directly as the tool result.
Before calling the tool, consider what are the most impactful questions that would unblock the most decisions, and whether each question should be text, radio, or checkbox type.
Topics to clarify:
- Specific functionality and behavior
- Edge cases and error handling
- UI/UX expectations
......@@ -23,8 +27,6 @@ Your goal is to have a thoughtful brainstorming session with the user to fully u
- Performance or security considerations
- User workflows and interactions
**IMPORTANT**: After calling \`planning_questionnaire\`, you MUST STOP and wait for the user's responses. Do NOT proceed to create a plan until the user has answered all questions. The user's responses will appear in a follow-up message.
4. **Iterative Clarification**: Based on user responses, continue exploring the codebase and asking follow-up questions until you have a clear picture. After receiving the first round of answers, consider whether follow-up questions are needed before moving to plan creation.
## Phase 2: Plan Creation
......@@ -76,7 +78,7 @@ After presenting the plan:
- \`code_search\` - Semantic code search
## Planning Tools (for interaction)
- \`planning_questionnaire\` - Present structured questions to the user (supports text, radio, and checkbox question types)
- \`planning_questionnaire\` - Present structured questions to the user (accepts only a \`questions\` array; waits for and returns user responses)
- \`write_plan\` - Present or update the implementation plan as a markdown document
- \`exit_plan\` - Transition to implementation mode after plan approval
......
......@@ -26,6 +26,7 @@ import {
pendingAgentConsentsAtom,
agentTodosByChatIdAtom,
} from "./atoms/chatAtoms";
import { pendingQuestionnaireAtom } from "./atoms/planAtoms";
import { queryKeys } from "./lib/queryKeys";
// @ts-ignore
......@@ -168,6 +169,7 @@ function App() {
// Agent v2 tool consent requests - queue consents instead of overwriting
const setPendingAgentConsents = useSetAtom(pendingAgentConsentsAtom);
const setPendingQuestionnaire = useSetAtom(pendingQuestionnaireAtom);
const setAgentTodosByChatId = useSetAtom(agentTodosByChatIdAtom);
// Agent todos updates
......@@ -217,9 +219,15 @@ function App() {
setPendingAgentConsents((prev) =>
prev.filter((consent) => consent.chatId !== chatId),
);
setPendingQuestionnaire((prev) => {
if (!prev.has(chatId)) return prev;
const next = new Map(prev);
next.delete(chatId);
return next;
});
});
return () => unsubscribe();
}, [setPendingAgentConsents]);
}, [setPendingAgentConsents, setPendingQuestionnaire]);
// Forward telemetry events from main process to PostHog
useEffect(() => {
......
......@@ -52,7 +52,7 @@ Never use closely matched colors for an element's background and its foreground
Follow this workflow when building web apps:
1. **Determine Design Direction**
- Analyze the industry and target users of the website.
- Define colors, fonts, mood, and visual style.
- Define colors, fonts, mood, and visual style (you are allowed to ask the user if you have access to planning_questionnaire tool).
- Ensure the design direction does NOT contradict the rules defined for this theme.
2. **Build the Application**
- Do not neglect functionality in the pursuit of making a beautiful website.
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论