From 2ea9500f734dccb590c7a4fff7ef69560afc5228 Mon Sep 17 00:00:00 2001 From: Will Chen Date: Wed, 25 Jun 2025 15:36:05 -0700 Subject: [PATCH] Configurable thinking budget (default to medium) (#494) --- ...--smart-context---auto-includes-only-1.txt | 3 +- ...ec.ts_manage-context---smart-context-1.txt | 3 +- ...ec.ts_manage-context---smart-context-3.txt | 3 +- ...ec.ts_manage-context---smart-context-4.txt | 3 +- ...r-auto-should-send-message-to-engine-1.txt | 3 +- ...ngine.spec.ts_send-message-to-engine-1.txt | 3 +- ...t-auto-should-send-message-to-engine-1.txt | 3 +- ...nking_budget.spec.ts_thinking-budget-1.txt | 25 ++ ...nking_budget.spec.ts_thinking-budget-2.txt | 385 +++++++++++++++++ ...nking_budget.spec.ts_thinking-budget-3.txt | 25 ++ ...nking_budget.spec.ts_thinking-budget-4.txt | 393 +++++++++++++++++ ...nking_budget.spec.ts_thinking-budget-5.txt | 25 ++ ...nking_budget.spec.ts_thinking-budget-6.txt | 401 ++++++++++++++++++ e2e-tests/thinking_budget.spec.ts | 34 ++ src/components/ThinkingBudgetSelector.tsx | 80 ++++ src/ipc/handlers/chat_stream_handlers.ts | 1 + src/ipc/utils/get_model_client.ts | 1 + src/ipc/utils/llm_engine_provider.ts | 7 +- src/ipc/utils/thinking_utils.ts | 21 + src/lib/schemas.ts | 1 + src/pages/settings.tsx | 5 + 21 files changed, 1417 insertions(+), 8 deletions(-) create mode 100644 e2e-tests/snapshots/thinking_budget.spec.ts_thinking-budget-1.txt create mode 100644 e2e-tests/snapshots/thinking_budget.spec.ts_thinking-budget-2.txt create mode 100644 e2e-tests/snapshots/thinking_budget.spec.ts_thinking-budget-3.txt create mode 100644 e2e-tests/snapshots/thinking_budget.spec.ts_thinking-budget-4.txt create mode 100644 e2e-tests/snapshots/thinking_budget.spec.ts_thinking-budget-5.txt create mode 100644 e2e-tests/snapshots/thinking_budget.spec.ts_thinking-budget-6.txt create mode 100644 e2e-tests/thinking_budget.spec.ts create mode 100644 src/components/ThinkingBudgetSelector.tsx diff --git a/e2e-tests/snapshots/context_manage.spec.ts_manage-context---smart-context---auto-includes-only-1.txt b/e2e-tests/snapshots/context_manage.spec.ts_manage-context---smart-context---auto-includes-only-1.txt index e144756..32e11ab 100644 --- a/e2e-tests/snapshots/context_manage.spec.ts_manage-context---smart-context---auto-includes-only-1.txt +++ b/e2e-tests/snapshots/context_manage.spec.ts_manage-context---smart-context---auto-includes-only-1.txt @@ -16,7 +16,8 @@ "stream": true, "thinking": { "type": "enabled", - "include_thoughts": true + "include_thoughts": true, + "budget_tokens": 4000 }, "dyad_options": { "files": [ diff --git a/e2e-tests/snapshots/context_manage.spec.ts_manage-context---smart-context-1.txt b/e2e-tests/snapshots/context_manage.spec.ts_manage-context---smart-context-1.txt index fd5650c..e2d70f3 100644 --- a/e2e-tests/snapshots/context_manage.spec.ts_manage-context---smart-context-1.txt +++ b/e2e-tests/snapshots/context_manage.spec.ts_manage-context---smart-context-1.txt @@ -16,7 +16,8 @@ "stream": true, "thinking": { "type": "enabled", - "include_thoughts": true + "include_thoughts": true, + "budget_tokens": 4000 }, "dyad_options": { "files": [ diff --git a/e2e-tests/snapshots/context_manage.spec.ts_manage-context---smart-context-3.txt b/e2e-tests/snapshots/context_manage.spec.ts_manage-context---smart-context-3.txt index 74ab41b..081c964 100644 --- a/e2e-tests/snapshots/context_manage.spec.ts_manage-context---smart-context-3.txt +++ b/e2e-tests/snapshots/context_manage.spec.ts_manage-context---smart-context-3.txt @@ -24,7 +24,8 @@ "stream": true, "thinking": { "type": "enabled", - "include_thoughts": true + "include_thoughts": true, + "budget_tokens": 4000 }, "dyad_options": { "files": [ diff --git a/e2e-tests/snapshots/context_manage.spec.ts_manage-context---smart-context-4.txt b/e2e-tests/snapshots/context_manage.spec.ts_manage-context---smart-context-4.txt index fba0955..505c0f0 100644 --- a/e2e-tests/snapshots/context_manage.spec.ts_manage-context---smart-context-4.txt +++ b/e2e-tests/snapshots/context_manage.spec.ts_manage-context---smart-context-4.txt @@ -32,7 +32,8 @@ "stream": true, "thinking": { "type": "enabled", - "include_thoughts": true + "include_thoughts": true, + "budget_tokens": 4000 }, "dyad_options": { "files": [ diff --git a/e2e-tests/snapshots/engine.spec.ts_regular-auto-should-send-message-to-engine-1.txt b/e2e-tests/snapshots/engine.spec.ts_regular-auto-should-send-message-to-engine-1.txt index 4917551..797fb4b 100644 --- a/e2e-tests/snapshots/engine.spec.ts_regular-auto-should-send-message-to-engine-1.txt +++ b/e2e-tests/snapshots/engine.spec.ts_regular-auto-should-send-message-to-engine-1.txt @@ -16,7 +16,8 @@ "stream": true, "thinking": { "type": "enabled", - "include_thoughts": true + "include_thoughts": true, + "budget_tokens": 4000 }, "dyad_options": { "files": [ diff --git a/e2e-tests/snapshots/engine.spec.ts_send-message-to-engine-1.txt b/e2e-tests/snapshots/engine.spec.ts_send-message-to-engine-1.txt index ac2eec7..45c2062 100644 --- a/e2e-tests/snapshots/engine.spec.ts_send-message-to-engine-1.txt +++ b/e2e-tests/snapshots/engine.spec.ts_send-message-to-engine-1.txt @@ -16,7 +16,8 @@ "stream": true, "thinking": { "type": "enabled", - "include_thoughts": true + "include_thoughts": true, + "budget_tokens": 4000 }, "dyad_options": { "files": [ diff --git a/e2e-tests/snapshots/engine.spec.ts_smart-auto-should-send-message-to-engine-1.txt b/e2e-tests/snapshots/engine.spec.ts_smart-auto-should-send-message-to-engine-1.txt index 9b7d0a6..7b76a11 100644 --- a/e2e-tests/snapshots/engine.spec.ts_smart-auto-should-send-message-to-engine-1.txt +++ b/e2e-tests/snapshots/engine.spec.ts_smart-auto-should-send-message-to-engine-1.txt @@ -16,7 +16,8 @@ "stream": true, "thinking": { "type": "enabled", - "include_thoughts": true + "include_thoughts": true, + "budget_tokens": 4000 }, "dyad_options": { "files": [ diff --git a/e2e-tests/snapshots/thinking_budget.spec.ts_thinking-budget-1.txt b/e2e-tests/snapshots/thinking_budget.spec.ts_thinking-budget-1.txt new file mode 100644 index 0000000..1b01e1d --- /dev/null +++ b/e2e-tests/snapshots/thinking_budget.spec.ts_thinking-budget-1.txt @@ -0,0 +1,25 @@ +{ + "selectedModel": { + "name": "gemini-2.5-pro", + "provider": "google" + }, + "providerSettings": { + "auto": { + "apiKey": { + "value": "testdyadkey", + "encryptionType": "plaintext" + } + } + }, + "telemetryConsent": "unset", + "telemetryUserId": "[UUID]", + "hasRunBefore": true, + "enableDyadPro": true, + "experiments": {}, + "lastShownReleaseNotesVersion": "[scrubbed]", + "thinkingBudget": "low", + "enableProLazyEditsMode": true, + "enableProSmartFilesContextMode": true, + "selectedChatMode": "build", + "isTestMode": true +} \ No newline at end of file diff --git a/e2e-tests/snapshots/thinking_budget.spec.ts_thinking-budget-2.txt b/e2e-tests/snapshots/thinking_budget.spec.ts_thinking-budget-2.txt new file mode 100644 index 0000000..1b01573 --- /dev/null +++ b/e2e-tests/snapshots/thinking_budget.spec.ts_thinking-budget-2.txt @@ -0,0 +1,385 @@ +{ + "body": { + "model": "gemini/gemini-2.5-pro", + "max_tokens": 65535, + "temperature": 0, + "messages": [ + { + "role": "system", + "content": "[[SYSTEM_MESSAGE]]" + }, + { + "role": "user", + "content": "tc=1" + }, + { + "role": "assistant", + "content": "Error: Test case file not found: 1.md" + }, + { + "role": "user", + "content": "[dump] hi" + } + ], + "stream": true, + "thinking": { + "type": "enabled", + "include_thoughts": true, + "budget_tokens": 1000 + }, + "dyad_options": { + "files": [ + { + "path": "AI_RULES.md", + "content": "# Tech Stack\n\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\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", + "force": false + }, + { + "path": "eslint.config.js", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "index.html", + "content": "\n\n \n \n \n dyad-generated-app\n \n\n \n
\n \n \n\n", + "force": false + }, + { + "path": "package.json", + "content": "{\n \"dependencies\": {\n \"@hookform/resolvers\": \"^3.9.0\",\n \"@radix-ui/react-accordion\": \"^1.2.0\",\n \"@radix-ui/react-alert-dialog\": \"^1.1.1\",\n \"@radix-ui/react-aspect-ratio\": \"^1.1.0\",\n \"@radix-ui/react-avatar\": \"^1.1.0\",\n \"@radix-ui/react-checkbox\": \"^1.1.1\",\n \"@radix-ui/react-collapsible\": \"^1.1.0\",\n \"@radix-ui/react-context-menu\": \"^2.2.1\",\n \"@radix-ui/react-dialog\": \"^1.1.2\",\n \"@radix-ui/react-dropdown-menu\": \"^2.1.1\",\n \"@radix-ui/react-hover-card\": \"^1.1.1\",\n \"@radix-ui/react-label\": \"^2.1.0\",\n \"@radix-ui/react-menubar\": \"^1.1.1\",\n \"@radix-ui/react-navigation-menu\": \"^1.2.0\",\n \"@radix-ui/react-popover\": \"^1.1.1\",\n \"@radix-ui/react-progress\": \"^1.1.0\",\n \"@radix-ui/react-radio-group\": \"^1.2.0\",\n \"@radix-ui/react-scroll-area\": \"^1.1.0\",\n \"@radix-ui/react-select\": \"^2.1.1\",\n \"@radix-ui/react-separator\": \"^1.1.0\",\n \"@radix-ui/react-slider\": \"^1.2.0\",\n \"@radix-ui/react-slot\": \"^1.1.0\",\n \"@radix-ui/react-switch\": \"^1.1.0\",\n \"@radix-ui/react-tabs\": \"^1.1.0\",\n \"@radix-ui/react-toast\": \"^1.2.1\",\n \"@radix-ui/react-toggle\": \"^1.1.0\",\n \"@radix-ui/react-toggle-group\": \"^1.1.0\",\n \"@radix-ui/react-tooltip\": \"^1.1.4\",\n \"@tanstack/react-query\": \"^5.56.2\",\n \"class-variance-authority\": \"^0.7.1\",\n \"clsx\": \"^2.1.1\",\n \"cmdk\": \"^1.0.0\",\n \"date-fns\": \"^3.6.0\",\n \"embla-carousel-react\": \"^8.3.0\",\n \"input-otp\": \"^1.2.4\",\n \"lucide-react\": \"^0.462.0\",\n \"next-themes\": \"^0.3.0\",\n \"react\": \"^18.3.1\",\n \"react-day-picker\": \"^8.10.1\",\n \"react-dom\": \"^18.3.1\",\n \"react-hook-form\": \"^7.53.0\",\n \"react-resizable-panels\": \"^2.1.3\",\n \"react-router-dom\": \"^6.26.2\",\n \"recharts\": \"^2.12.7\",\n \"sonner\": \"^1.5.0\",\n \"tailwind-merge\": \"^2.5.2\",\n \"tailwindcss-animate\": \"^1.0.7\",\n \"vaul\": \"^0.9.3\",\n \"zod\": \"^3.23.8\"\n },\n \"devDependencies\": {\n \"@dyad-sh/react-vite-component-tagger\": \"^0.8.0\",\n \"@eslint/js\": \"^9.9.0\",\n \"@tailwindcss/typography\": \"^0.5.15\",\n \"@types/node\": \"^22.5.5\",\n \"@types/react\": \"^18.3.3\",\n \"@types/react-dom\": \"^18.3.0\",\n \"@vitejs/plugin-react-swc\": \"^3.9.0\",\n \"autoprefixer\": \"^10.4.20\",\n \"eslint\": \"^9.9.0\",\n \"eslint-plugin-react-hooks\": \"^5.1.0-rc.0\",\n \"eslint-plugin-react-refresh\": \"^0.4.9\",\n \"globals\": \"^15.9.0\",\n \"postcss\": \"^8.4.47\",\n \"tailwindcss\": \"^3.4.11\",\n \"typescript\": \"^5.5.3\",\n \"typescript-eslint\": \"^8.0.1\",\n \"vite\": \"^6.3.4\"\n }\n}", + "force": false + }, + { + "path": "postcss.config.js", + "content": "export default {\n plugins: {\n tailwindcss: {},\n autoprefixer: {},\n },\n};\n", + "force": false + }, + { + "path": "README.md", + "content": "# Welcome to your Dyad app\n", + "force": false + }, + { + "path": "src/App.css", + "content": "#root {\n max-width: 1280px;\n margin: 0 auto;\n padding: 2rem;\n text-align: center;\n}\n\n.logo {\n height: 6em;\n padding: 1.5em;\n will-change: filter;\n transition: filter 300ms;\n}\n.logo:hover {\n filter: drop-shadow(0 0 2em #646cffaa);\n}\n.logo.react:hover {\n filter: drop-shadow(0 0 2em #61dafbaa);\n}\n\n@keyframes logo-spin {\n from {\n transform: rotate(0deg);\n }\n to {\n transform: rotate(360deg);\n }\n}\n\n@media (prefers-reduced-motion: no-preference) {\n a:nth-of-type(2) .logo {\n animation: logo-spin infinite 20s linear;\n }\n}\n\n.card {\n padding: 2em;\n}\n\n.read-the-docs {\n color: #888;\n}\n", + "force": false + }, + { + "path": "src/App.tsx", + "content": "import { Toaster } from \"@/components/ui/toaster\";\nimport { Toaster as Sonner } from \"@/components/ui/sonner\";\nimport { TooltipProvider } from \"@/components/ui/tooltip\";\nimport { QueryClient, QueryClientProvider } from \"@tanstack/react-query\";\nimport { BrowserRouter, Routes, Route } from \"react-router-dom\";\nimport Index from \"./pages/Index\";\nimport NotFound from \"./pages/NotFound\";\n\nconst queryClient = new QueryClient();\n\nconst App = () => (\n \n \n \n \n \n \n } />\n {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL \"*\" ROUTE */}\n } />\n \n \n \n \n);\n\nexport default App;\n", + "force": false + }, + { + "path": "src/components/made-with-dyad.tsx", + "content": "export const MadeWithDyad = () => {\n return (\n
\n \n Made with Dyad\n \n
\n );\n};\n", + "force": false + }, + { + "path": "src/components/ui/accordion.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/alert-dialog.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/alert.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/aspect-ratio.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/avatar.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/badge.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/breadcrumb.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/button.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/calendar.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/card.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/carousel.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/chart.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/checkbox.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/collapsible.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/command.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/context-menu.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/dialog.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/drawer.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/dropdown-menu.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/form.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/hover-card.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/input-otp.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/input.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/label.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/menubar.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/navigation-menu.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/pagination.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/popover.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/progress.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/radio-group.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/resizable.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/scroll-area.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/select.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/separator.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/sheet.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/sidebar.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/skeleton.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/slider.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/sonner.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/switch.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/table.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/tabs.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/textarea.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/toast.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/toaster.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/toggle-group.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/toggle.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/tooltip.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/use-toast.ts", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/globals.css", + "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@layer base {\n :root {\n --background: 0 0% 100%;\n --foreground: 222.2 84% 4.9%;\n\n --card: 0 0% 100%;\n --card-foreground: 222.2 84% 4.9%;\n\n --popover: 0 0% 100%;\n --popover-foreground: 222.2 84% 4.9%;\n\n --primary: 222.2 47.4% 11.2%;\n --primary-foreground: 210 40% 98%;\n\n --secondary: 210 40% 96.1%;\n --secondary-foreground: 222.2 47.4% 11.2%;\n\n --muted: 210 40% 96.1%;\n --muted-foreground: 215.4 16.3% 46.9%;\n\n --accent: 210 40% 96.1%;\n --accent-foreground: 222.2 47.4% 11.2%;\n\n --destructive: 0 84.2% 60.2%;\n --destructive-foreground: 210 40% 98%;\n\n --border: 214.3 31.8% 91.4%;\n --input: 214.3 31.8% 91.4%;\n --ring: 222.2 84% 4.9%;\n\n --radius: 0.5rem;\n\n --sidebar-background: 0 0% 98%;\n\n --sidebar-foreground: 240 5.3% 26.1%;\n\n --sidebar-primary: 240 5.9% 10%;\n\n --sidebar-primary-foreground: 0 0% 98%;\n\n --sidebar-accent: 240 4.8% 95.9%;\n\n --sidebar-accent-foreground: 240 5.9% 10%;\n\n --sidebar-border: 220 13% 91%;\n\n --sidebar-ring: 217.2 91.2% 59.8%;\n }\n\n .dark {\n --background: 222.2 84% 4.9%;\n --foreground: 210 40% 98%;\n\n --card: 222.2 84% 4.9%;\n --card-foreground: 210 40% 98%;\n\n --popover: 222.2 84% 4.9%;\n --popover-foreground: 210 40% 98%;\n\n --primary: 210 40% 98%;\n --primary-foreground: 222.2 47.4% 11.2%;\n\n --secondary: 217.2 32.6% 17.5%;\n --secondary-foreground: 210 40% 98%;\n\n --muted: 217.2 32.6% 17.5%;\n --muted-foreground: 215 20.2% 65.1%;\n\n --accent: 217.2 32.6% 17.5%;\n --accent-foreground: 210 40% 98%;\n\n --destructive: 0 62.8% 30.6%;\n --destructive-foreground: 210 40% 98%;\n\n --border: 217.2 32.6% 17.5%;\n --input: 217.2 32.6% 17.5%;\n --ring: 212.7 26.8% 83.9%;\n --sidebar-background: 240 5.9% 10%;\n --sidebar-foreground: 240 4.8% 95.9%;\n --sidebar-primary: 224.3 76.3% 48%;\n --sidebar-primary-foreground: 0 0% 100%;\n --sidebar-accent: 240 3.7% 15.9%;\n --sidebar-accent-foreground: 240 4.8% 95.9%;\n --sidebar-border: 240 3.7% 15.9%;\n --sidebar-ring: 217.2 91.2% 59.8%;\n }\n}\n\n@layer base {\n * {\n @apply border-border;\n }\n\n body {\n @apply bg-background text-foreground;\n }\n}\n", + "force": false + }, + { + "path": "src/hooks/use-mobile.tsx", + "content": "import * as React from \"react\";\n\nconst MOBILE_BREAKPOINT = 768;\n\nexport function useIsMobile() {\n const [isMobile, setIsMobile] = React.useState(\n undefined,\n );\n\n React.useEffect(() => {\n const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);\n const onChange = () => {\n setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);\n };\n mql.addEventListener(\"change\", onChange);\n setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);\n return () => mql.removeEventListener(\"change\", onChange);\n }, []);\n\n return !!isMobile;\n}\n", + "force": false + }, + { + "path": "src/hooks/use-toast.ts", + "content": "import * as React from \"react\";\n\nimport type { ToastActionElement, ToastProps } from \"@/components/ui/toast\";\n\nconst TOAST_LIMIT = 1;\nconst TOAST_REMOVE_DELAY = 1000000;\n\ntype ToasterToast = ToastProps & {\n id: string;\n title?: React.ReactNode;\n description?: React.ReactNode;\n action?: ToastActionElement;\n};\n\nconst _actionTypes = {\n ADD_TOAST: \"ADD_TOAST\",\n UPDATE_TOAST: \"UPDATE_TOAST\",\n DISMISS_TOAST: \"DISMISS_TOAST\",\n REMOVE_TOAST: \"REMOVE_TOAST\",\n} as const;\n\nlet count = 0;\n\nfunction genId() {\n count = (count + 1) % Number.MAX_SAFE_INTEGER;\n return count.toString();\n}\n\ntype ActionType = typeof _actionTypes;\n\ntype Action =\n | {\n type: ActionType[\"ADD_TOAST\"];\n toast: ToasterToast;\n }\n | {\n type: ActionType[\"UPDATE_TOAST\"];\n toast: Partial;\n }\n | {\n type: ActionType[\"DISMISS_TOAST\"];\n toastId?: ToasterToast[\"id\"];\n }\n | {\n type: ActionType[\"REMOVE_TOAST\"];\n toastId?: ToasterToast[\"id\"];\n };\n\ninterface State {\n toasts: ToasterToast[];\n}\n\nconst toastTimeouts = new Map>();\n\nconst addToRemoveQueue = (toastId: string) => {\n if (toastTimeouts.has(toastId)) {\n return;\n }\n\n const timeout = setTimeout(() => {\n toastTimeouts.delete(toastId);\n dispatch({\n type: \"REMOVE_TOAST\",\n toastId: toastId,\n });\n }, TOAST_REMOVE_DELAY);\n\n toastTimeouts.set(toastId, timeout);\n};\n\nexport const reducer = (state: State, action: Action): State => {\n switch (action.type) {\n case \"ADD_TOAST\":\n return {\n ...state,\n toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),\n };\n\n case \"UPDATE_TOAST\":\n return {\n ...state,\n toasts: state.toasts.map((t) =>\n t.id === action.toast.id ? { ...t, ...action.toast } : t,\n ),\n };\n\n case \"DISMISS_TOAST\": {\n const { toastId } = action;\n\n // ! Side effects ! - This could be extracted into a dismissToast() action,\n // but I'll keep it here for simplicity\n if (toastId) {\n addToRemoveQueue(toastId);\n } else {\n state.toasts.forEach((toast) => {\n addToRemoveQueue(toast.id);\n });\n }\n\n return {\n ...state,\n toasts: state.toasts.map((t) =>\n t.id === toastId || toastId === undefined\n ? {\n ...t,\n open: false,\n }\n : t,\n ),\n };\n }\n case \"REMOVE_TOAST\":\n if (action.toastId === undefined) {\n return {\n ...state,\n toasts: [],\n };\n }\n return {\n ...state,\n toasts: state.toasts.filter((t) => t.id !== action.toastId),\n };\n }\n};\n\nconst listeners: Array<(state: State) => void> = [];\n\nlet memoryState: State = { toasts: [] };\n\nfunction dispatch(action: Action) {\n memoryState = reducer(memoryState, action);\n listeners.forEach((listener) => {\n listener(memoryState);\n });\n}\n\ntype Toast = Omit;\n\nfunction toast({ ...props }: Toast) {\n const id = genId();\n\n const update = (props: ToasterToast) =>\n dispatch({\n type: \"UPDATE_TOAST\",\n toast: { ...props, id },\n });\n const dismiss = () => dispatch({ type: \"DISMISS_TOAST\", toastId: id });\n\n dispatch({\n type: \"ADD_TOAST\",\n toast: {\n ...props,\n id,\n open: true,\n onOpenChange: (open) => {\n if (!open) dismiss();\n },\n },\n });\n\n return {\n id: id,\n dismiss,\n update,\n };\n}\n\nfunction useToast() {\n const [state, setState] = React.useState(memoryState);\n\n React.useEffect(() => {\n listeners.push(setState);\n return () => {\n const index = listeners.indexOf(setState);\n if (index > -1) {\n listeners.splice(index, 1);\n }\n };\n }, [state]);\n\n return {\n ...state,\n toast,\n dismiss: (toastId?: string) => dispatch({ type: \"DISMISS_TOAST\", toastId }),\n };\n}\n\nexport { useToast, toast };\n", + "force": false + }, + { + "path": "src/lib/utils.ts", + "content": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n", + "force": false + }, + { + "path": "src/main.tsx", + "content": "import { createRoot } from \"react-dom/client\";\nimport App from \"./App.tsx\";\nimport \"./globals.css\";\n\ncreateRoot(document.getElementById(\"root\")!).render();\n", + "force": false + }, + { + "path": "src/pages/Index.tsx", + "content": "// Update this page (the content is just a fallback if you fail to update the page)\n\nimport { MadeWithDyad } from \"@/components/made-with-dyad\";\n\nconst Index = () => {\n return (\n
\n
\n

Welcome to Your Blank App

\n

\n Start building your amazing project here!\n

\n
\n \n
\n );\n};\n\nexport default Index;\n", + "force": false + }, + { + "path": "src/pages/NotFound.tsx", + "content": "import { useLocation } from \"react-router-dom\";\nimport { useEffect } from \"react\";\n\nconst NotFound = () => {\n const location = useLocation();\n\n useEffect(() => {\n console.error(\n \"404 Error: User attempted to access non-existent route:\",\n location.pathname,\n );\n }, [location.pathname]);\n\n return (\n
\n
\n

404

\n

Oops! Page not found

\n \n Return to Home\n \n
\n
\n );\n};\n\nexport default NotFound;\n", + "force": false + }, + { + "path": "src/utils/toast.ts", + "content": "import { toast } from \"sonner\";\n\nexport const showSuccess = (message: string) => {\n toast.success(message);\n};\n\nexport const showError = (message: string) => {\n toast.error(message);\n};\n\nexport const showLoading = (message: string) => {\n return toast.loading(message);\n};\n\nexport const dismissToast = (toastId: string) => {\n toast.dismiss(toastId);\n};\n", + "force": false + }, + { + "path": "src/vite-env.d.ts", + "content": "/// \n", + "force": false + }, + { + "path": "tailwind.config.ts", + "content": "import type { Config } from \"tailwindcss\";\n\nexport default {\n darkMode: [\"class\"],\n content: [\n \"./pages/**/*.{ts,tsx}\",\n \"./components/**/*.{ts,tsx}\",\n \"./app/**/*.{ts,tsx}\",\n \"./src/**/*.{ts,tsx}\",\n ],\n prefix: \"\",\n theme: {\n container: {\n center: true,\n padding: \"2rem\",\n screens: {\n \"2xl\": \"1400px\",\n },\n },\n extend: {\n colors: {\n border: \"hsl(var(--border))\",\n input: \"hsl(var(--input))\",\n ring: \"hsl(var(--ring))\",\n background: \"hsl(var(--background))\",\n foreground: \"hsl(var(--foreground))\",\n primary: {\n DEFAULT: \"hsl(var(--primary))\",\n foreground: \"hsl(var(--primary-foreground))\",\n },\n secondary: {\n DEFAULT: \"hsl(var(--secondary))\",\n foreground: \"hsl(var(--secondary-foreground))\",\n },\n destructive: {\n DEFAULT: \"hsl(var(--destructive))\",\n foreground: \"hsl(var(--destructive-foreground))\",\n },\n muted: {\n DEFAULT: \"hsl(var(--muted))\",\n foreground: \"hsl(var(--muted-foreground))\",\n },\n accent: {\n DEFAULT: \"hsl(var(--accent))\",\n foreground: \"hsl(var(--accent-foreground))\",\n },\n popover: {\n DEFAULT: \"hsl(var(--popover))\",\n foreground: \"hsl(var(--popover-foreground))\",\n },\n card: {\n DEFAULT: \"hsl(var(--card))\",\n foreground: \"hsl(var(--card-foreground))\",\n },\n sidebar: {\n DEFAULT: \"hsl(var(--sidebar-background))\",\n foreground: \"hsl(var(--sidebar-foreground))\",\n primary: \"hsl(var(--sidebar-primary))\",\n \"primary-foreground\": \"hsl(var(--sidebar-primary-foreground))\",\n accent: \"hsl(var(--sidebar-accent))\",\n \"accent-foreground\": \"hsl(var(--sidebar-accent-foreground))\",\n border: \"hsl(var(--sidebar-border))\",\n ring: \"hsl(var(--sidebar-ring))\",\n },\n },\n borderRadius: {\n lg: \"var(--radius)\",\n md: \"calc(var(--radius) - 2px)\",\n sm: \"calc(var(--radius) - 4px)\",\n },\n keyframes: {\n \"accordion-down\": {\n from: {\n height: \"0\",\n },\n to: {\n height: \"var(--radix-accordion-content-height)\",\n },\n },\n \"accordion-up\": {\n from: {\n height: \"var(--radix-accordion-content-height)\",\n },\n to: {\n height: \"0\",\n },\n },\n },\n animation: {\n \"accordion-down\": \"accordion-down 0.2s ease-out\",\n \"accordion-up\": \"accordion-up 0.2s ease-out\",\n },\n },\n },\n plugins: [require(\"tailwindcss-animate\")],\n} satisfies Config;\n", + "force": false + }, + { + "path": "vite.config.ts", + "content": "import { defineConfig } from \"vite\";\nimport dyadComponentTagger from \"@dyad-sh/react-vite-component-tagger\";\nimport react from \"@vitejs/plugin-react-swc\";\nimport path from \"path\";\n\nexport default defineConfig(() => ({\n server: {\n host: \"::\",\n port: 8080,\n },\n plugins: [dyadComponentTagger(), react()],\n resolve: {\n alias: {\n \"@\": path.resolve(__dirname, \"./src\"),\n },\n },\n}));\n", + "force": false + } + ], + "enable_lazy_edits": true, + "enable_smart_files_context": true + } + }, + "headers": { + "authorization": "Bearer testdyadkey" + } +} \ No newline at end of file diff --git a/e2e-tests/snapshots/thinking_budget.spec.ts_thinking-budget-3.txt b/e2e-tests/snapshots/thinking_budget.spec.ts_thinking-budget-3.txt new file mode 100644 index 0000000..16ea61d --- /dev/null +++ b/e2e-tests/snapshots/thinking_budget.spec.ts_thinking-budget-3.txt @@ -0,0 +1,25 @@ +{ + "selectedModel": { + "name": "gemini-2.5-pro", + "provider": "google" + }, + "providerSettings": { + "auto": { + "apiKey": { + "value": "testdyadkey", + "encryptionType": "plaintext" + } + } + }, + "telemetryConsent": "unset", + "telemetryUserId": "[UUID]", + "hasRunBefore": true, + "enableDyadPro": true, + "experiments": {}, + "lastShownReleaseNotesVersion": "[scrubbed]", + "thinkingBudget": "medium", + "enableProLazyEditsMode": true, + "enableProSmartFilesContextMode": true, + "selectedChatMode": "build", + "isTestMode": true +} \ No newline at end of file diff --git a/e2e-tests/snapshots/thinking_budget.spec.ts_thinking-budget-4.txt b/e2e-tests/snapshots/thinking_budget.spec.ts_thinking-budget-4.txt new file mode 100644 index 0000000..a9bd207 --- /dev/null +++ b/e2e-tests/snapshots/thinking_budget.spec.ts_thinking-budget-4.txt @@ -0,0 +1,393 @@ +{ + "body": { + "model": "gemini/gemini-2.5-pro", + "max_tokens": 65535, + "temperature": 0, + "messages": [ + { + "role": "system", + "content": "[[SYSTEM_MESSAGE]]" + }, + { + "role": "user", + "content": "tc=1" + }, + { + "role": "assistant", + "content": "Error: Test case file not found: 1.md" + }, + { + "role": "user", + "content": "[dump] hi" + }, + { + "role": "assistant", + "content": "[[dyad-dump-path=*]]" + }, + { + "role": "user", + "content": "[dump] hi" + } + ], + "stream": true, + "thinking": { + "type": "enabled", + "include_thoughts": true, + "budget_tokens": 4000 + }, + "dyad_options": { + "files": [ + { + "path": "AI_RULES.md", + "content": "# Tech Stack\n\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\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", + "force": false + }, + { + "path": "eslint.config.js", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "index.html", + "content": "\n\n \n \n \n dyad-generated-app\n \n\n \n
\n \n \n\n", + "force": false + }, + { + "path": "package.json", + "content": "{\n \"dependencies\": {\n \"@hookform/resolvers\": \"^3.9.0\",\n \"@radix-ui/react-accordion\": \"^1.2.0\",\n \"@radix-ui/react-alert-dialog\": \"^1.1.1\",\n \"@radix-ui/react-aspect-ratio\": \"^1.1.0\",\n \"@radix-ui/react-avatar\": \"^1.1.0\",\n \"@radix-ui/react-checkbox\": \"^1.1.1\",\n \"@radix-ui/react-collapsible\": \"^1.1.0\",\n \"@radix-ui/react-context-menu\": \"^2.2.1\",\n \"@radix-ui/react-dialog\": \"^1.1.2\",\n \"@radix-ui/react-dropdown-menu\": \"^2.1.1\",\n \"@radix-ui/react-hover-card\": \"^1.1.1\",\n \"@radix-ui/react-label\": \"^2.1.0\",\n \"@radix-ui/react-menubar\": \"^1.1.1\",\n \"@radix-ui/react-navigation-menu\": \"^1.2.0\",\n \"@radix-ui/react-popover\": \"^1.1.1\",\n \"@radix-ui/react-progress\": \"^1.1.0\",\n \"@radix-ui/react-radio-group\": \"^1.2.0\",\n \"@radix-ui/react-scroll-area\": \"^1.1.0\",\n \"@radix-ui/react-select\": \"^2.1.1\",\n \"@radix-ui/react-separator\": \"^1.1.0\",\n \"@radix-ui/react-slider\": \"^1.2.0\",\n \"@radix-ui/react-slot\": \"^1.1.0\",\n \"@radix-ui/react-switch\": \"^1.1.0\",\n \"@radix-ui/react-tabs\": \"^1.1.0\",\n \"@radix-ui/react-toast\": \"^1.2.1\",\n \"@radix-ui/react-toggle\": \"^1.1.0\",\n \"@radix-ui/react-toggle-group\": \"^1.1.0\",\n \"@radix-ui/react-tooltip\": \"^1.1.4\",\n \"@tanstack/react-query\": \"^5.56.2\",\n \"class-variance-authority\": \"^0.7.1\",\n \"clsx\": \"^2.1.1\",\n \"cmdk\": \"^1.0.0\",\n \"date-fns\": \"^3.6.0\",\n \"embla-carousel-react\": \"^8.3.0\",\n \"input-otp\": \"^1.2.4\",\n \"lucide-react\": \"^0.462.0\",\n \"next-themes\": \"^0.3.0\",\n \"react\": \"^18.3.1\",\n \"react-day-picker\": \"^8.10.1\",\n \"react-dom\": \"^18.3.1\",\n \"react-hook-form\": \"^7.53.0\",\n \"react-resizable-panels\": \"^2.1.3\",\n \"react-router-dom\": \"^6.26.2\",\n \"recharts\": \"^2.12.7\",\n \"sonner\": \"^1.5.0\",\n \"tailwind-merge\": \"^2.5.2\",\n \"tailwindcss-animate\": \"^1.0.7\",\n \"vaul\": \"^0.9.3\",\n \"zod\": \"^3.23.8\"\n },\n \"devDependencies\": {\n \"@dyad-sh/react-vite-component-tagger\": \"^0.8.0\",\n \"@eslint/js\": \"^9.9.0\",\n \"@tailwindcss/typography\": \"^0.5.15\",\n \"@types/node\": \"^22.5.5\",\n \"@types/react\": \"^18.3.3\",\n \"@types/react-dom\": \"^18.3.0\",\n \"@vitejs/plugin-react-swc\": \"^3.9.0\",\n \"autoprefixer\": \"^10.4.20\",\n \"eslint\": \"^9.9.0\",\n \"eslint-plugin-react-hooks\": \"^5.1.0-rc.0\",\n \"eslint-plugin-react-refresh\": \"^0.4.9\",\n \"globals\": \"^15.9.0\",\n \"postcss\": \"^8.4.47\",\n \"tailwindcss\": \"^3.4.11\",\n \"typescript\": \"^5.5.3\",\n \"typescript-eslint\": \"^8.0.1\",\n \"vite\": \"^6.3.4\"\n }\n}", + "force": false + }, + { + "path": "postcss.config.js", + "content": "export default {\n plugins: {\n tailwindcss: {},\n autoprefixer: {},\n },\n};\n", + "force": false + }, + { + "path": "README.md", + "content": "# Welcome to your Dyad app\n", + "force": false + }, + { + "path": "src/App.css", + "content": "#root {\n max-width: 1280px;\n margin: 0 auto;\n padding: 2rem;\n text-align: center;\n}\n\n.logo {\n height: 6em;\n padding: 1.5em;\n will-change: filter;\n transition: filter 300ms;\n}\n.logo:hover {\n filter: drop-shadow(0 0 2em #646cffaa);\n}\n.logo.react:hover {\n filter: drop-shadow(0 0 2em #61dafbaa);\n}\n\n@keyframes logo-spin {\n from {\n transform: rotate(0deg);\n }\n to {\n transform: rotate(360deg);\n }\n}\n\n@media (prefers-reduced-motion: no-preference) {\n a:nth-of-type(2) .logo {\n animation: logo-spin infinite 20s linear;\n }\n}\n\n.card {\n padding: 2em;\n}\n\n.read-the-docs {\n color: #888;\n}\n", + "force": false + }, + { + "path": "src/App.tsx", + "content": "import { Toaster } from \"@/components/ui/toaster\";\nimport { Toaster as Sonner } from \"@/components/ui/sonner\";\nimport { TooltipProvider } from \"@/components/ui/tooltip\";\nimport { QueryClient, QueryClientProvider } from \"@tanstack/react-query\";\nimport { BrowserRouter, Routes, Route } from \"react-router-dom\";\nimport Index from \"./pages/Index\";\nimport NotFound from \"./pages/NotFound\";\n\nconst queryClient = new QueryClient();\n\nconst App = () => (\n \n \n \n \n \n \n } />\n {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL \"*\" ROUTE */}\n } />\n \n \n \n \n);\n\nexport default App;\n", + "force": false + }, + { + "path": "src/components/made-with-dyad.tsx", + "content": "export const MadeWithDyad = () => {\n return (\n
\n \n Made with Dyad\n \n
\n );\n};\n", + "force": false + }, + { + "path": "src/components/ui/accordion.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/alert-dialog.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/alert.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/aspect-ratio.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/avatar.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/badge.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/breadcrumb.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/button.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/calendar.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/card.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/carousel.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/chart.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/checkbox.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/collapsible.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/command.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/context-menu.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/dialog.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/drawer.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/dropdown-menu.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/form.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/hover-card.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/input-otp.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/input.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/label.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/menubar.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/navigation-menu.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/pagination.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/popover.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/progress.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/radio-group.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/resizable.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/scroll-area.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/select.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/separator.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/sheet.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/sidebar.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/skeleton.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/slider.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/sonner.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/switch.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/table.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/tabs.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/textarea.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/toast.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/toaster.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/toggle-group.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/toggle.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/tooltip.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/use-toast.ts", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/globals.css", + "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@layer base {\n :root {\n --background: 0 0% 100%;\n --foreground: 222.2 84% 4.9%;\n\n --card: 0 0% 100%;\n --card-foreground: 222.2 84% 4.9%;\n\n --popover: 0 0% 100%;\n --popover-foreground: 222.2 84% 4.9%;\n\n --primary: 222.2 47.4% 11.2%;\n --primary-foreground: 210 40% 98%;\n\n --secondary: 210 40% 96.1%;\n --secondary-foreground: 222.2 47.4% 11.2%;\n\n --muted: 210 40% 96.1%;\n --muted-foreground: 215.4 16.3% 46.9%;\n\n --accent: 210 40% 96.1%;\n --accent-foreground: 222.2 47.4% 11.2%;\n\n --destructive: 0 84.2% 60.2%;\n --destructive-foreground: 210 40% 98%;\n\n --border: 214.3 31.8% 91.4%;\n --input: 214.3 31.8% 91.4%;\n --ring: 222.2 84% 4.9%;\n\n --radius: 0.5rem;\n\n --sidebar-background: 0 0% 98%;\n\n --sidebar-foreground: 240 5.3% 26.1%;\n\n --sidebar-primary: 240 5.9% 10%;\n\n --sidebar-primary-foreground: 0 0% 98%;\n\n --sidebar-accent: 240 4.8% 95.9%;\n\n --sidebar-accent-foreground: 240 5.9% 10%;\n\n --sidebar-border: 220 13% 91%;\n\n --sidebar-ring: 217.2 91.2% 59.8%;\n }\n\n .dark {\n --background: 222.2 84% 4.9%;\n --foreground: 210 40% 98%;\n\n --card: 222.2 84% 4.9%;\n --card-foreground: 210 40% 98%;\n\n --popover: 222.2 84% 4.9%;\n --popover-foreground: 210 40% 98%;\n\n --primary: 210 40% 98%;\n --primary-foreground: 222.2 47.4% 11.2%;\n\n --secondary: 217.2 32.6% 17.5%;\n --secondary-foreground: 210 40% 98%;\n\n --muted: 217.2 32.6% 17.5%;\n --muted-foreground: 215 20.2% 65.1%;\n\n --accent: 217.2 32.6% 17.5%;\n --accent-foreground: 210 40% 98%;\n\n --destructive: 0 62.8% 30.6%;\n --destructive-foreground: 210 40% 98%;\n\n --border: 217.2 32.6% 17.5%;\n --input: 217.2 32.6% 17.5%;\n --ring: 212.7 26.8% 83.9%;\n --sidebar-background: 240 5.9% 10%;\n --sidebar-foreground: 240 4.8% 95.9%;\n --sidebar-primary: 224.3 76.3% 48%;\n --sidebar-primary-foreground: 0 0% 100%;\n --sidebar-accent: 240 3.7% 15.9%;\n --sidebar-accent-foreground: 240 4.8% 95.9%;\n --sidebar-border: 240 3.7% 15.9%;\n --sidebar-ring: 217.2 91.2% 59.8%;\n }\n}\n\n@layer base {\n * {\n @apply border-border;\n }\n\n body {\n @apply bg-background text-foreground;\n }\n}\n", + "force": false + }, + { + "path": "src/hooks/use-mobile.tsx", + "content": "import * as React from \"react\";\n\nconst MOBILE_BREAKPOINT = 768;\n\nexport function useIsMobile() {\n const [isMobile, setIsMobile] = React.useState(\n undefined,\n );\n\n React.useEffect(() => {\n const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);\n const onChange = () => {\n setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);\n };\n mql.addEventListener(\"change\", onChange);\n setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);\n return () => mql.removeEventListener(\"change\", onChange);\n }, []);\n\n return !!isMobile;\n}\n", + "force": false + }, + { + "path": "src/hooks/use-toast.ts", + "content": "import * as React from \"react\";\n\nimport type { ToastActionElement, ToastProps } from \"@/components/ui/toast\";\n\nconst TOAST_LIMIT = 1;\nconst TOAST_REMOVE_DELAY = 1000000;\n\ntype ToasterToast = ToastProps & {\n id: string;\n title?: React.ReactNode;\n description?: React.ReactNode;\n action?: ToastActionElement;\n};\n\nconst _actionTypes = {\n ADD_TOAST: \"ADD_TOAST\",\n UPDATE_TOAST: \"UPDATE_TOAST\",\n DISMISS_TOAST: \"DISMISS_TOAST\",\n REMOVE_TOAST: \"REMOVE_TOAST\",\n} as const;\n\nlet count = 0;\n\nfunction genId() {\n count = (count + 1) % Number.MAX_SAFE_INTEGER;\n return count.toString();\n}\n\ntype ActionType = typeof _actionTypes;\n\ntype Action =\n | {\n type: ActionType[\"ADD_TOAST\"];\n toast: ToasterToast;\n }\n | {\n type: ActionType[\"UPDATE_TOAST\"];\n toast: Partial;\n }\n | {\n type: ActionType[\"DISMISS_TOAST\"];\n toastId?: ToasterToast[\"id\"];\n }\n | {\n type: ActionType[\"REMOVE_TOAST\"];\n toastId?: ToasterToast[\"id\"];\n };\n\ninterface State {\n toasts: ToasterToast[];\n}\n\nconst toastTimeouts = new Map>();\n\nconst addToRemoveQueue = (toastId: string) => {\n if (toastTimeouts.has(toastId)) {\n return;\n }\n\n const timeout = setTimeout(() => {\n toastTimeouts.delete(toastId);\n dispatch({\n type: \"REMOVE_TOAST\",\n toastId: toastId,\n });\n }, TOAST_REMOVE_DELAY);\n\n toastTimeouts.set(toastId, timeout);\n};\n\nexport const reducer = (state: State, action: Action): State => {\n switch (action.type) {\n case \"ADD_TOAST\":\n return {\n ...state,\n toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),\n };\n\n case \"UPDATE_TOAST\":\n return {\n ...state,\n toasts: state.toasts.map((t) =>\n t.id === action.toast.id ? { ...t, ...action.toast } : t,\n ),\n };\n\n case \"DISMISS_TOAST\": {\n const { toastId } = action;\n\n // ! Side effects ! - This could be extracted into a dismissToast() action,\n // but I'll keep it here for simplicity\n if (toastId) {\n addToRemoveQueue(toastId);\n } else {\n state.toasts.forEach((toast) => {\n addToRemoveQueue(toast.id);\n });\n }\n\n return {\n ...state,\n toasts: state.toasts.map((t) =>\n t.id === toastId || toastId === undefined\n ? {\n ...t,\n open: false,\n }\n : t,\n ),\n };\n }\n case \"REMOVE_TOAST\":\n if (action.toastId === undefined) {\n return {\n ...state,\n toasts: [],\n };\n }\n return {\n ...state,\n toasts: state.toasts.filter((t) => t.id !== action.toastId),\n };\n }\n};\n\nconst listeners: Array<(state: State) => void> = [];\n\nlet memoryState: State = { toasts: [] };\n\nfunction dispatch(action: Action) {\n memoryState = reducer(memoryState, action);\n listeners.forEach((listener) => {\n listener(memoryState);\n });\n}\n\ntype Toast = Omit;\n\nfunction toast({ ...props }: Toast) {\n const id = genId();\n\n const update = (props: ToasterToast) =>\n dispatch({\n type: \"UPDATE_TOAST\",\n toast: { ...props, id },\n });\n const dismiss = () => dispatch({ type: \"DISMISS_TOAST\", toastId: id });\n\n dispatch({\n type: \"ADD_TOAST\",\n toast: {\n ...props,\n id,\n open: true,\n onOpenChange: (open) => {\n if (!open) dismiss();\n },\n },\n });\n\n return {\n id: id,\n dismiss,\n update,\n };\n}\n\nfunction useToast() {\n const [state, setState] = React.useState(memoryState);\n\n React.useEffect(() => {\n listeners.push(setState);\n return () => {\n const index = listeners.indexOf(setState);\n if (index > -1) {\n listeners.splice(index, 1);\n }\n };\n }, [state]);\n\n return {\n ...state,\n toast,\n dismiss: (toastId?: string) => dispatch({ type: \"DISMISS_TOAST\", toastId }),\n };\n}\n\nexport { useToast, toast };\n", + "force": false + }, + { + "path": "src/lib/utils.ts", + "content": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n", + "force": false + }, + { + "path": "src/main.tsx", + "content": "import { createRoot } from \"react-dom/client\";\nimport App from \"./App.tsx\";\nimport \"./globals.css\";\n\ncreateRoot(document.getElementById(\"root\")!).render();\n", + "force": false + }, + { + "path": "src/pages/Index.tsx", + "content": "// Update this page (the content is just a fallback if you fail to update the page)\n\nimport { MadeWithDyad } from \"@/components/made-with-dyad\";\n\nconst Index = () => {\n return (\n
\n
\n

Welcome to Your Blank App

\n

\n Start building your amazing project here!\n

\n
\n \n
\n );\n};\n\nexport default Index;\n", + "force": false + }, + { + "path": "src/pages/NotFound.tsx", + "content": "import { useLocation } from \"react-router-dom\";\nimport { useEffect } from \"react\";\n\nconst NotFound = () => {\n const location = useLocation();\n\n useEffect(() => {\n console.error(\n \"404 Error: User attempted to access non-existent route:\",\n location.pathname,\n );\n }, [location.pathname]);\n\n return (\n
\n
\n

404

\n

Oops! Page not found

\n \n Return to Home\n \n
\n
\n );\n};\n\nexport default NotFound;\n", + "force": false + }, + { + "path": "src/utils/toast.ts", + "content": "import { toast } from \"sonner\";\n\nexport const showSuccess = (message: string) => {\n toast.success(message);\n};\n\nexport const showError = (message: string) => {\n toast.error(message);\n};\n\nexport const showLoading = (message: string) => {\n return toast.loading(message);\n};\n\nexport const dismissToast = (toastId: string) => {\n toast.dismiss(toastId);\n};\n", + "force": false + }, + { + "path": "src/vite-env.d.ts", + "content": "/// \n", + "force": false + }, + { + "path": "tailwind.config.ts", + "content": "import type { Config } from \"tailwindcss\";\n\nexport default {\n darkMode: [\"class\"],\n content: [\n \"./pages/**/*.{ts,tsx}\",\n \"./components/**/*.{ts,tsx}\",\n \"./app/**/*.{ts,tsx}\",\n \"./src/**/*.{ts,tsx}\",\n ],\n prefix: \"\",\n theme: {\n container: {\n center: true,\n padding: \"2rem\",\n screens: {\n \"2xl\": \"1400px\",\n },\n },\n extend: {\n colors: {\n border: \"hsl(var(--border))\",\n input: \"hsl(var(--input))\",\n ring: \"hsl(var(--ring))\",\n background: \"hsl(var(--background))\",\n foreground: \"hsl(var(--foreground))\",\n primary: {\n DEFAULT: \"hsl(var(--primary))\",\n foreground: \"hsl(var(--primary-foreground))\",\n },\n secondary: {\n DEFAULT: \"hsl(var(--secondary))\",\n foreground: \"hsl(var(--secondary-foreground))\",\n },\n destructive: {\n DEFAULT: \"hsl(var(--destructive))\",\n foreground: \"hsl(var(--destructive-foreground))\",\n },\n muted: {\n DEFAULT: \"hsl(var(--muted))\",\n foreground: \"hsl(var(--muted-foreground))\",\n },\n accent: {\n DEFAULT: \"hsl(var(--accent))\",\n foreground: \"hsl(var(--accent-foreground))\",\n },\n popover: {\n DEFAULT: \"hsl(var(--popover))\",\n foreground: \"hsl(var(--popover-foreground))\",\n },\n card: {\n DEFAULT: \"hsl(var(--card))\",\n foreground: \"hsl(var(--card-foreground))\",\n },\n sidebar: {\n DEFAULT: \"hsl(var(--sidebar-background))\",\n foreground: \"hsl(var(--sidebar-foreground))\",\n primary: \"hsl(var(--sidebar-primary))\",\n \"primary-foreground\": \"hsl(var(--sidebar-primary-foreground))\",\n accent: \"hsl(var(--sidebar-accent))\",\n \"accent-foreground\": \"hsl(var(--sidebar-accent-foreground))\",\n border: \"hsl(var(--sidebar-border))\",\n ring: \"hsl(var(--sidebar-ring))\",\n },\n },\n borderRadius: {\n lg: \"var(--radius)\",\n md: \"calc(var(--radius) - 2px)\",\n sm: \"calc(var(--radius) - 4px)\",\n },\n keyframes: {\n \"accordion-down\": {\n from: {\n height: \"0\",\n },\n to: {\n height: \"var(--radix-accordion-content-height)\",\n },\n },\n \"accordion-up\": {\n from: {\n height: \"var(--radix-accordion-content-height)\",\n },\n to: {\n height: \"0\",\n },\n },\n },\n animation: {\n \"accordion-down\": \"accordion-down 0.2s ease-out\",\n \"accordion-up\": \"accordion-up 0.2s ease-out\",\n },\n },\n },\n plugins: [require(\"tailwindcss-animate\")],\n} satisfies Config;\n", + "force": false + }, + { + "path": "vite.config.ts", + "content": "import { defineConfig } from \"vite\";\nimport dyadComponentTagger from \"@dyad-sh/react-vite-component-tagger\";\nimport react from \"@vitejs/plugin-react-swc\";\nimport path from \"path\";\n\nexport default defineConfig(() => ({\n server: {\n host: \"::\",\n port: 8080,\n },\n plugins: [dyadComponentTagger(), react()],\n resolve: {\n alias: {\n \"@\": path.resolve(__dirname, \"./src\"),\n },\n },\n}));\n", + "force": false + } + ], + "enable_lazy_edits": true, + "enable_smart_files_context": true + } + }, + "headers": { + "authorization": "Bearer testdyadkey" + } +} \ No newline at end of file diff --git a/e2e-tests/snapshots/thinking_budget.spec.ts_thinking-budget-5.txt b/e2e-tests/snapshots/thinking_budget.spec.ts_thinking-budget-5.txt new file mode 100644 index 0000000..331cdb3 --- /dev/null +++ b/e2e-tests/snapshots/thinking_budget.spec.ts_thinking-budget-5.txt @@ -0,0 +1,25 @@ +{ + "selectedModel": { + "name": "gemini-2.5-pro", + "provider": "google" + }, + "providerSettings": { + "auto": { + "apiKey": { + "value": "testdyadkey", + "encryptionType": "plaintext" + } + } + }, + "telemetryConsent": "unset", + "telemetryUserId": "[UUID]", + "hasRunBefore": true, + "enableDyadPro": true, + "experiments": {}, + "lastShownReleaseNotesVersion": "[scrubbed]", + "thinkingBudget": "high", + "enableProLazyEditsMode": true, + "enableProSmartFilesContextMode": true, + "selectedChatMode": "build", + "isTestMode": true +} \ No newline at end of file diff --git a/e2e-tests/snapshots/thinking_budget.spec.ts_thinking-budget-6.txt b/e2e-tests/snapshots/thinking_budget.spec.ts_thinking-budget-6.txt new file mode 100644 index 0000000..cb7140d --- /dev/null +++ b/e2e-tests/snapshots/thinking_budget.spec.ts_thinking-budget-6.txt @@ -0,0 +1,401 @@ +{ + "body": { + "model": "gemini/gemini-2.5-pro", + "max_tokens": 65535, + "temperature": 0, + "messages": [ + { + "role": "system", + "content": "[[SYSTEM_MESSAGE]]" + }, + { + "role": "user", + "content": "tc=1" + }, + { + "role": "assistant", + "content": "Error: Test case file not found: 1.md" + }, + { + "role": "user", + "content": "[dump] hi" + }, + { + "role": "assistant", + "content": "[[dyad-dump-path=*]]" + }, + { + "role": "user", + "content": "[dump] hi" + }, + { + "role": "assistant", + "content": "[[dyad-dump-path=*]]" + }, + { + "role": "user", + "content": "[dump] hi" + } + ], + "stream": true, + "thinking": { + "type": "enabled", + "include_thoughts": true, + "budget_tokens": -1 + }, + "dyad_options": { + "files": [ + { + "path": "AI_RULES.md", + "content": "# Tech Stack\n\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\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", + "force": false + }, + { + "path": "eslint.config.js", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "index.html", + "content": "\n\n \n \n \n dyad-generated-app\n \n\n \n
\n \n \n\n", + "force": false + }, + { + "path": "package.json", + "content": "{\n \"dependencies\": {\n \"@hookform/resolvers\": \"^3.9.0\",\n \"@radix-ui/react-accordion\": \"^1.2.0\",\n \"@radix-ui/react-alert-dialog\": \"^1.1.1\",\n \"@radix-ui/react-aspect-ratio\": \"^1.1.0\",\n \"@radix-ui/react-avatar\": \"^1.1.0\",\n \"@radix-ui/react-checkbox\": \"^1.1.1\",\n \"@radix-ui/react-collapsible\": \"^1.1.0\",\n \"@radix-ui/react-context-menu\": \"^2.2.1\",\n \"@radix-ui/react-dialog\": \"^1.1.2\",\n \"@radix-ui/react-dropdown-menu\": \"^2.1.1\",\n \"@radix-ui/react-hover-card\": \"^1.1.1\",\n \"@radix-ui/react-label\": \"^2.1.0\",\n \"@radix-ui/react-menubar\": \"^1.1.1\",\n \"@radix-ui/react-navigation-menu\": \"^1.2.0\",\n \"@radix-ui/react-popover\": \"^1.1.1\",\n \"@radix-ui/react-progress\": \"^1.1.0\",\n \"@radix-ui/react-radio-group\": \"^1.2.0\",\n \"@radix-ui/react-scroll-area\": \"^1.1.0\",\n \"@radix-ui/react-select\": \"^2.1.1\",\n \"@radix-ui/react-separator\": \"^1.1.0\",\n \"@radix-ui/react-slider\": \"^1.2.0\",\n \"@radix-ui/react-slot\": \"^1.1.0\",\n \"@radix-ui/react-switch\": \"^1.1.0\",\n \"@radix-ui/react-tabs\": \"^1.1.0\",\n \"@radix-ui/react-toast\": \"^1.2.1\",\n \"@radix-ui/react-toggle\": \"^1.1.0\",\n \"@radix-ui/react-toggle-group\": \"^1.1.0\",\n \"@radix-ui/react-tooltip\": \"^1.1.4\",\n \"@tanstack/react-query\": \"^5.56.2\",\n \"class-variance-authority\": \"^0.7.1\",\n \"clsx\": \"^2.1.1\",\n \"cmdk\": \"^1.0.0\",\n \"date-fns\": \"^3.6.0\",\n \"embla-carousel-react\": \"^8.3.0\",\n \"input-otp\": \"^1.2.4\",\n \"lucide-react\": \"^0.462.0\",\n \"next-themes\": \"^0.3.0\",\n \"react\": \"^18.3.1\",\n \"react-day-picker\": \"^8.10.1\",\n \"react-dom\": \"^18.3.1\",\n \"react-hook-form\": \"^7.53.0\",\n \"react-resizable-panels\": \"^2.1.3\",\n \"react-router-dom\": \"^6.26.2\",\n \"recharts\": \"^2.12.7\",\n \"sonner\": \"^1.5.0\",\n \"tailwind-merge\": \"^2.5.2\",\n \"tailwindcss-animate\": \"^1.0.7\",\n \"vaul\": \"^0.9.3\",\n \"zod\": \"^3.23.8\"\n },\n \"devDependencies\": {\n \"@dyad-sh/react-vite-component-tagger\": \"^0.8.0\",\n \"@eslint/js\": \"^9.9.0\",\n \"@tailwindcss/typography\": \"^0.5.15\",\n \"@types/node\": \"^22.5.5\",\n \"@types/react\": \"^18.3.3\",\n \"@types/react-dom\": \"^18.3.0\",\n \"@vitejs/plugin-react-swc\": \"^3.9.0\",\n \"autoprefixer\": \"^10.4.20\",\n \"eslint\": \"^9.9.0\",\n \"eslint-plugin-react-hooks\": \"^5.1.0-rc.0\",\n \"eslint-plugin-react-refresh\": \"^0.4.9\",\n \"globals\": \"^15.9.0\",\n \"postcss\": \"^8.4.47\",\n \"tailwindcss\": \"^3.4.11\",\n \"typescript\": \"^5.5.3\",\n \"typescript-eslint\": \"^8.0.1\",\n \"vite\": \"^6.3.4\"\n }\n}", + "force": false + }, + { + "path": "postcss.config.js", + "content": "export default {\n plugins: {\n tailwindcss: {},\n autoprefixer: {},\n },\n};\n", + "force": false + }, + { + "path": "README.md", + "content": "# Welcome to your Dyad app\n", + "force": false + }, + { + "path": "src/App.css", + "content": "#root {\n max-width: 1280px;\n margin: 0 auto;\n padding: 2rem;\n text-align: center;\n}\n\n.logo {\n height: 6em;\n padding: 1.5em;\n will-change: filter;\n transition: filter 300ms;\n}\n.logo:hover {\n filter: drop-shadow(0 0 2em #646cffaa);\n}\n.logo.react:hover {\n filter: drop-shadow(0 0 2em #61dafbaa);\n}\n\n@keyframes logo-spin {\n from {\n transform: rotate(0deg);\n }\n to {\n transform: rotate(360deg);\n }\n}\n\n@media (prefers-reduced-motion: no-preference) {\n a:nth-of-type(2) .logo {\n animation: logo-spin infinite 20s linear;\n }\n}\n\n.card {\n padding: 2em;\n}\n\n.read-the-docs {\n color: #888;\n}\n", + "force": false + }, + { + "path": "src/App.tsx", + "content": "import { Toaster } from \"@/components/ui/toaster\";\nimport { Toaster as Sonner } from \"@/components/ui/sonner\";\nimport { TooltipProvider } from \"@/components/ui/tooltip\";\nimport { QueryClient, QueryClientProvider } from \"@tanstack/react-query\";\nimport { BrowserRouter, Routes, Route } from \"react-router-dom\";\nimport Index from \"./pages/Index\";\nimport NotFound from \"./pages/NotFound\";\n\nconst queryClient = new QueryClient();\n\nconst App = () => (\n \n \n \n \n \n \n } />\n {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL \"*\" ROUTE */}\n } />\n \n \n \n \n);\n\nexport default App;\n", + "force": false + }, + { + "path": "src/components/made-with-dyad.tsx", + "content": "export const MadeWithDyad = () => {\n return (\n
\n \n Made with Dyad\n \n
\n );\n};\n", + "force": false + }, + { + "path": "src/components/ui/accordion.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/alert-dialog.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/alert.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/aspect-ratio.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/avatar.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/badge.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/breadcrumb.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/button.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/calendar.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/card.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/carousel.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/chart.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/checkbox.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/collapsible.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/command.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/context-menu.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/dialog.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/drawer.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/dropdown-menu.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/form.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/hover-card.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/input-otp.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/input.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/label.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/menubar.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/navigation-menu.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/pagination.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/popover.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/progress.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/radio-group.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/resizable.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/scroll-area.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/select.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/separator.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/sheet.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/sidebar.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/skeleton.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/slider.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/sonner.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/switch.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/table.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/tabs.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/textarea.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/toast.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/toaster.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/toggle-group.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/toggle.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/tooltip.tsx", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/components/ui/use-toast.ts", + "content": "// Contents omitted for brevity", + "force": false + }, + { + "path": "src/globals.css", + "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@layer base {\n :root {\n --background: 0 0% 100%;\n --foreground: 222.2 84% 4.9%;\n\n --card: 0 0% 100%;\n --card-foreground: 222.2 84% 4.9%;\n\n --popover: 0 0% 100%;\n --popover-foreground: 222.2 84% 4.9%;\n\n --primary: 222.2 47.4% 11.2%;\n --primary-foreground: 210 40% 98%;\n\n --secondary: 210 40% 96.1%;\n --secondary-foreground: 222.2 47.4% 11.2%;\n\n --muted: 210 40% 96.1%;\n --muted-foreground: 215.4 16.3% 46.9%;\n\n --accent: 210 40% 96.1%;\n --accent-foreground: 222.2 47.4% 11.2%;\n\n --destructive: 0 84.2% 60.2%;\n --destructive-foreground: 210 40% 98%;\n\n --border: 214.3 31.8% 91.4%;\n --input: 214.3 31.8% 91.4%;\n --ring: 222.2 84% 4.9%;\n\n --radius: 0.5rem;\n\n --sidebar-background: 0 0% 98%;\n\n --sidebar-foreground: 240 5.3% 26.1%;\n\n --sidebar-primary: 240 5.9% 10%;\n\n --sidebar-primary-foreground: 0 0% 98%;\n\n --sidebar-accent: 240 4.8% 95.9%;\n\n --sidebar-accent-foreground: 240 5.9% 10%;\n\n --sidebar-border: 220 13% 91%;\n\n --sidebar-ring: 217.2 91.2% 59.8%;\n }\n\n .dark {\n --background: 222.2 84% 4.9%;\n --foreground: 210 40% 98%;\n\n --card: 222.2 84% 4.9%;\n --card-foreground: 210 40% 98%;\n\n --popover: 222.2 84% 4.9%;\n --popover-foreground: 210 40% 98%;\n\n --primary: 210 40% 98%;\n --primary-foreground: 222.2 47.4% 11.2%;\n\n --secondary: 217.2 32.6% 17.5%;\n --secondary-foreground: 210 40% 98%;\n\n --muted: 217.2 32.6% 17.5%;\n --muted-foreground: 215 20.2% 65.1%;\n\n --accent: 217.2 32.6% 17.5%;\n --accent-foreground: 210 40% 98%;\n\n --destructive: 0 62.8% 30.6%;\n --destructive-foreground: 210 40% 98%;\n\n --border: 217.2 32.6% 17.5%;\n --input: 217.2 32.6% 17.5%;\n --ring: 212.7 26.8% 83.9%;\n --sidebar-background: 240 5.9% 10%;\n --sidebar-foreground: 240 4.8% 95.9%;\n --sidebar-primary: 224.3 76.3% 48%;\n --sidebar-primary-foreground: 0 0% 100%;\n --sidebar-accent: 240 3.7% 15.9%;\n --sidebar-accent-foreground: 240 4.8% 95.9%;\n --sidebar-border: 240 3.7% 15.9%;\n --sidebar-ring: 217.2 91.2% 59.8%;\n }\n}\n\n@layer base {\n * {\n @apply border-border;\n }\n\n body {\n @apply bg-background text-foreground;\n }\n}\n", + "force": false + }, + { + "path": "src/hooks/use-mobile.tsx", + "content": "import * as React from \"react\";\n\nconst MOBILE_BREAKPOINT = 768;\n\nexport function useIsMobile() {\n const [isMobile, setIsMobile] = React.useState(\n undefined,\n );\n\n React.useEffect(() => {\n const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);\n const onChange = () => {\n setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);\n };\n mql.addEventListener(\"change\", onChange);\n setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);\n return () => mql.removeEventListener(\"change\", onChange);\n }, []);\n\n return !!isMobile;\n}\n", + "force": false + }, + { + "path": "src/hooks/use-toast.ts", + "content": "import * as React from \"react\";\n\nimport type { ToastActionElement, ToastProps } from \"@/components/ui/toast\";\n\nconst TOAST_LIMIT = 1;\nconst TOAST_REMOVE_DELAY = 1000000;\n\ntype ToasterToast = ToastProps & {\n id: string;\n title?: React.ReactNode;\n description?: React.ReactNode;\n action?: ToastActionElement;\n};\n\nconst _actionTypes = {\n ADD_TOAST: \"ADD_TOAST\",\n UPDATE_TOAST: \"UPDATE_TOAST\",\n DISMISS_TOAST: \"DISMISS_TOAST\",\n REMOVE_TOAST: \"REMOVE_TOAST\",\n} as const;\n\nlet count = 0;\n\nfunction genId() {\n count = (count + 1) % Number.MAX_SAFE_INTEGER;\n return count.toString();\n}\n\ntype ActionType = typeof _actionTypes;\n\ntype Action =\n | {\n type: ActionType[\"ADD_TOAST\"];\n toast: ToasterToast;\n }\n | {\n type: ActionType[\"UPDATE_TOAST\"];\n toast: Partial;\n }\n | {\n type: ActionType[\"DISMISS_TOAST\"];\n toastId?: ToasterToast[\"id\"];\n }\n | {\n type: ActionType[\"REMOVE_TOAST\"];\n toastId?: ToasterToast[\"id\"];\n };\n\ninterface State {\n toasts: ToasterToast[];\n}\n\nconst toastTimeouts = new Map>();\n\nconst addToRemoveQueue = (toastId: string) => {\n if (toastTimeouts.has(toastId)) {\n return;\n }\n\n const timeout = setTimeout(() => {\n toastTimeouts.delete(toastId);\n dispatch({\n type: \"REMOVE_TOAST\",\n toastId: toastId,\n });\n }, TOAST_REMOVE_DELAY);\n\n toastTimeouts.set(toastId, timeout);\n};\n\nexport const reducer = (state: State, action: Action): State => {\n switch (action.type) {\n case \"ADD_TOAST\":\n return {\n ...state,\n toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),\n };\n\n case \"UPDATE_TOAST\":\n return {\n ...state,\n toasts: state.toasts.map((t) =>\n t.id === action.toast.id ? { ...t, ...action.toast } : t,\n ),\n };\n\n case \"DISMISS_TOAST\": {\n const { toastId } = action;\n\n // ! Side effects ! - This could be extracted into a dismissToast() action,\n // but I'll keep it here for simplicity\n if (toastId) {\n addToRemoveQueue(toastId);\n } else {\n state.toasts.forEach((toast) => {\n addToRemoveQueue(toast.id);\n });\n }\n\n return {\n ...state,\n toasts: state.toasts.map((t) =>\n t.id === toastId || toastId === undefined\n ? {\n ...t,\n open: false,\n }\n : t,\n ),\n };\n }\n case \"REMOVE_TOAST\":\n if (action.toastId === undefined) {\n return {\n ...state,\n toasts: [],\n };\n }\n return {\n ...state,\n toasts: state.toasts.filter((t) => t.id !== action.toastId),\n };\n }\n};\n\nconst listeners: Array<(state: State) => void> = [];\n\nlet memoryState: State = { toasts: [] };\n\nfunction dispatch(action: Action) {\n memoryState = reducer(memoryState, action);\n listeners.forEach((listener) => {\n listener(memoryState);\n });\n}\n\ntype Toast = Omit;\n\nfunction toast({ ...props }: Toast) {\n const id = genId();\n\n const update = (props: ToasterToast) =>\n dispatch({\n type: \"UPDATE_TOAST\",\n toast: { ...props, id },\n });\n const dismiss = () => dispatch({ type: \"DISMISS_TOAST\", toastId: id });\n\n dispatch({\n type: \"ADD_TOAST\",\n toast: {\n ...props,\n id,\n open: true,\n onOpenChange: (open) => {\n if (!open) dismiss();\n },\n },\n });\n\n return {\n id: id,\n dismiss,\n update,\n };\n}\n\nfunction useToast() {\n const [state, setState] = React.useState(memoryState);\n\n React.useEffect(() => {\n listeners.push(setState);\n return () => {\n const index = listeners.indexOf(setState);\n if (index > -1) {\n listeners.splice(index, 1);\n }\n };\n }, [state]);\n\n return {\n ...state,\n toast,\n dismiss: (toastId?: string) => dispatch({ type: \"DISMISS_TOAST\", toastId }),\n };\n}\n\nexport { useToast, toast };\n", + "force": false + }, + { + "path": "src/lib/utils.ts", + "content": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n", + "force": false + }, + { + "path": "src/main.tsx", + "content": "import { createRoot } from \"react-dom/client\";\nimport App from \"./App.tsx\";\nimport \"./globals.css\";\n\ncreateRoot(document.getElementById(\"root\")!).render();\n", + "force": false + }, + { + "path": "src/pages/Index.tsx", + "content": "// Update this page (the content is just a fallback if you fail to update the page)\n\nimport { MadeWithDyad } from \"@/components/made-with-dyad\";\n\nconst Index = () => {\n return (\n
\n
\n

Welcome to Your Blank App

\n

\n Start building your amazing project here!\n

\n
\n \n
\n );\n};\n\nexport default Index;\n", + "force": false + }, + { + "path": "src/pages/NotFound.tsx", + "content": "import { useLocation } from \"react-router-dom\";\nimport { useEffect } from \"react\";\n\nconst NotFound = () => {\n const location = useLocation();\n\n useEffect(() => {\n console.error(\n \"404 Error: User attempted to access non-existent route:\",\n location.pathname,\n );\n }, [location.pathname]);\n\n return (\n
\n
\n

404

\n

Oops! Page not found

\n \n Return to Home\n \n
\n
\n );\n};\n\nexport default NotFound;\n", + "force": false + }, + { + "path": "src/utils/toast.ts", + "content": "import { toast } from \"sonner\";\n\nexport const showSuccess = (message: string) => {\n toast.success(message);\n};\n\nexport const showError = (message: string) => {\n toast.error(message);\n};\n\nexport const showLoading = (message: string) => {\n return toast.loading(message);\n};\n\nexport const dismissToast = (toastId: string) => {\n toast.dismiss(toastId);\n};\n", + "force": false + }, + { + "path": "src/vite-env.d.ts", + "content": "/// \n", + "force": false + }, + { + "path": "tailwind.config.ts", + "content": "import type { Config } from \"tailwindcss\";\n\nexport default {\n darkMode: [\"class\"],\n content: [\n \"./pages/**/*.{ts,tsx}\",\n \"./components/**/*.{ts,tsx}\",\n \"./app/**/*.{ts,tsx}\",\n \"./src/**/*.{ts,tsx}\",\n ],\n prefix: \"\",\n theme: {\n container: {\n center: true,\n padding: \"2rem\",\n screens: {\n \"2xl\": \"1400px\",\n },\n },\n extend: {\n colors: {\n border: \"hsl(var(--border))\",\n input: \"hsl(var(--input))\",\n ring: \"hsl(var(--ring))\",\n background: \"hsl(var(--background))\",\n foreground: \"hsl(var(--foreground))\",\n primary: {\n DEFAULT: \"hsl(var(--primary))\",\n foreground: \"hsl(var(--primary-foreground))\",\n },\n secondary: {\n DEFAULT: \"hsl(var(--secondary))\",\n foreground: \"hsl(var(--secondary-foreground))\",\n },\n destructive: {\n DEFAULT: \"hsl(var(--destructive))\",\n foreground: \"hsl(var(--destructive-foreground))\",\n },\n muted: {\n DEFAULT: \"hsl(var(--muted))\",\n foreground: \"hsl(var(--muted-foreground))\",\n },\n accent: {\n DEFAULT: \"hsl(var(--accent))\",\n foreground: \"hsl(var(--accent-foreground))\",\n },\n popover: {\n DEFAULT: \"hsl(var(--popover))\",\n foreground: \"hsl(var(--popover-foreground))\",\n },\n card: {\n DEFAULT: \"hsl(var(--card))\",\n foreground: \"hsl(var(--card-foreground))\",\n },\n sidebar: {\n DEFAULT: \"hsl(var(--sidebar-background))\",\n foreground: \"hsl(var(--sidebar-foreground))\",\n primary: \"hsl(var(--sidebar-primary))\",\n \"primary-foreground\": \"hsl(var(--sidebar-primary-foreground))\",\n accent: \"hsl(var(--sidebar-accent))\",\n \"accent-foreground\": \"hsl(var(--sidebar-accent-foreground))\",\n border: \"hsl(var(--sidebar-border))\",\n ring: \"hsl(var(--sidebar-ring))\",\n },\n },\n borderRadius: {\n lg: \"var(--radius)\",\n md: \"calc(var(--radius) - 2px)\",\n sm: \"calc(var(--radius) - 4px)\",\n },\n keyframes: {\n \"accordion-down\": {\n from: {\n height: \"0\",\n },\n to: {\n height: \"var(--radix-accordion-content-height)\",\n },\n },\n \"accordion-up\": {\n from: {\n height: \"var(--radix-accordion-content-height)\",\n },\n to: {\n height: \"0\",\n },\n },\n },\n animation: {\n \"accordion-down\": \"accordion-down 0.2s ease-out\",\n \"accordion-up\": \"accordion-up 0.2s ease-out\",\n },\n },\n },\n plugins: [require(\"tailwindcss-animate\")],\n} satisfies Config;\n", + "force": false + }, + { + "path": "vite.config.ts", + "content": "import { defineConfig } from \"vite\";\nimport dyadComponentTagger from \"@dyad-sh/react-vite-component-tagger\";\nimport react from \"@vitejs/plugin-react-swc\";\nimport path from \"path\";\n\nexport default defineConfig(() => ({\n server: {\n host: \"::\",\n port: 8080,\n },\n plugins: [dyadComponentTagger(), react()],\n resolve: {\n alias: {\n \"@\": path.resolve(__dirname, \"./src\"),\n },\n },\n}));\n", + "force": false + } + ], + "enable_lazy_edits": true, + "enable_smart_files_context": true + } + }, + "headers": { + "authorization": "Bearer testdyadkey" + } +} \ No newline at end of file diff --git a/e2e-tests/thinking_budget.spec.ts b/e2e-tests/thinking_budget.spec.ts new file mode 100644 index 0000000..169b6b7 --- /dev/null +++ b/e2e-tests/thinking_budget.spec.ts @@ -0,0 +1,34 @@ +import { testSkipIfWindows } from "./helpers/test_helper"; + +testSkipIfWindows("thinking budget", async ({ po }) => { + await po.setUpDyadPro(); + await po.selectModel({ provider: "Google", model: "Gemini 2.5 Pro" }); + await po.sendPrompt("tc=1"); + + // Low + await po.goToSettingsTab(); + await po.page.getByRole("combobox", { name: "Thinking Budget" }).click(); + await po.page.getByRole("option", { name: "Low" }).click(); + await po.snapshotSettings(); + await po.page.getByText("Go Back").click(); + await po.sendPrompt("[dump] hi"); + await po.snapshotServerDump("request"); + + // Medium + await po.goToSettingsTab(); + await po.page.getByRole("combobox", { name: "Thinking Budget" }).click(); + await po.page.getByRole("option", { name: "Medium (default)" }).click(); + await po.snapshotSettings(); + await po.page.getByText("Go Back").click(); + await po.sendPrompt("[dump] hi"); + await po.snapshotServerDump("request"); + + // High + await po.goToSettingsTab(); + await po.page.getByRole("combobox", { name: "Thinking Budget" }).click(); + await po.page.getByRole("option", { name: "High" }).click(); + await po.snapshotSettings(); + await po.page.getByText("Go Back").click(); + await po.sendPrompt("[dump] hi"); + await po.snapshotServerDump("request"); +}); diff --git a/src/components/ThinkingBudgetSelector.tsx b/src/components/ThinkingBudgetSelector.tsx new file mode 100644 index 0000000..3e9e8ed --- /dev/null +++ b/src/components/ThinkingBudgetSelector.tsx @@ -0,0 +1,80 @@ +import React from "react"; +import { useSettings } from "@/hooks/useSettings"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; + +interface OptionInfo { + value: string; + label: string; + description: string; +} + +const defaultValue = "medium"; + +const options: OptionInfo[] = [ + { + value: "low", + label: "Low", + description: + "Minimal thinking tokens for faster responses and lower costs.", + }, + { + value: defaultValue, + label: "Medium (default)", + description: "Balanced thinking for most conversations.", + }, + { + value: "high", + label: "High", + description: + "Extended thinking for complex problems requiring deep analysis.", + }, +]; + +export const ThinkingBudgetSelector: React.FC = () => { + const { settings, updateSettings } = useSettings(); + + const handleValueChange = (value: string) => { + updateSettings({ thinkingBudget: value as "low" | "medium" | "high" }); + }; + + // Determine the current value + const currentValue = settings?.thinkingBudget || defaultValue; + + // Find the current option to display its description + const currentOption = + options.find((opt) => opt.value === currentValue) || options[1]; + + return ( +
+
+ + +
+
+ {currentOption.description} +
+
+ ); +}; diff --git a/src/ipc/handlers/chat_stream_handlers.ts b/src/ipc/handlers/chat_stream_handlers.ts index a2620cd..0883c5b 100644 --- a/src/ipc/handlers/chat_stream_handlers.ts +++ b/src/ipc/handlers/chat_stream_handlers.ts @@ -464,6 +464,7 @@ This conversation includes one or more image attachments. When the user uploads providerOptions: { "dyad-gateway": getExtraProviderOptions( modelClient.builtinProviderId, + settings, ), google: { thinkingConfig: { diff --git a/src/ipc/utils/get_model_client.ts b/src/ipc/utils/get_model_client.ts index 8a94d39..9caa614 100644 --- a/src/ipc/utils/get_model_client.ts +++ b/src/ipc/utils/get_model_client.ts @@ -84,6 +84,7 @@ export async function getModelClient( : settings.enableProLazyEditsMode, enableSmartFilesContext: settings.enableProSmartFilesContextMode, }, + settings, }) : createOpenAICompatible({ name: "dyad-gateway", diff --git a/src/ipc/utils/llm_engine_provider.ts b/src/ipc/utils/llm_engine_provider.ts index 33e1ec5..e3ad6b6 100644 --- a/src/ipc/utils/llm_engine_provider.ts +++ b/src/ipc/utils/llm_engine_provider.ts @@ -12,6 +12,7 @@ import { import { OpenAICompatibleChatSettings } from "@ai-sdk/openai-compatible"; import log from "electron-log"; import { getExtraProviderOptions } from "./thinking_utils"; +import type { UserSettings } from "../../lib/schemas"; const logger = log.scope("llm_engine_provider"); @@ -48,6 +49,7 @@ or to provide a custom fetch implementation for e.g. testing. enableLazyEdits?: boolean; enableSmartFilesContext?: boolean; }; + settings: UserSettings; } export interface DyadEngineProvider { @@ -125,7 +127,10 @@ export function createDyadEngine( // Parse the request body to manipulate it const parsedBody = { ...JSON.parse(init.body), - ...getExtraProviderOptions(options.originalProviderId), + ...getExtraProviderOptions( + options.originalProviderId, + options.settings, + ), }; // Add files to the request if they exist diff --git a/src/ipc/utils/thinking_utils.ts b/src/ipc/utils/thinking_utils.ts index b5ee058..31be916 100644 --- a/src/ipc/utils/thinking_utils.ts +++ b/src/ipc/utils/thinking_utils.ts @@ -1,16 +1,37 @@ import { PROVIDERS_THAT_SUPPORT_THINKING } from "../shared/language_model_helpers"; +import type { UserSettings } from "../../lib/schemas"; + +function getThinkingBudgetTokens( + thinkingBudget?: "low" | "medium" | "high", +): number { + switch (thinkingBudget) { + case "low": + return 1_000; + case "medium": + return 4_000; + case "high": + return -1; + default: + return 4_000; // Default to medium + } +} export function getExtraProviderOptions( providerId: string | undefined, + settings: UserSettings, ): Record { if (!providerId) { return {}; } if (PROVIDERS_THAT_SUPPORT_THINKING.includes(providerId)) { + const budgetTokens = getThinkingBudgetTokens(settings?.thinkingBudget); return { thinking: { type: "enabled", include_thoughts: true, + // -1 means dynamic thinking where model determines. + // budget_tokens: 128, // minimum for Gemini Pro is 128 + budget_tokens: budgetTokens, }, }; } diff --git a/src/lib/schemas.ts b/src/lib/schemas.ts index 9ec2f6d..01957a3 100644 --- a/src/lib/schemas.ts +++ b/src/lib/schemas.ts @@ -142,6 +142,7 @@ export const UserSettingsSchema = z.object({ experiments: ExperimentsSchema.optional(), lastShownReleaseNotesVersion: z.string().optional(), maxChatTurnsInContext: z.number().optional(), + thinkingBudget: z.enum(["low", "medium", "high"]).optional(), enableProLazyEditsMode: z.boolean().optional(), enableProSmartFilesContextMode: z.boolean().optional(), selectedTemplateId: z.string().optional(), diff --git a/src/pages/settings.tsx b/src/pages/settings.tsx index 441c5af..22cefab 100644 --- a/src/pages/settings.tsx +++ b/src/pages/settings.tsx @@ -7,6 +7,7 @@ import { showSuccess, showError } from "@/lib/toast"; import { AutoApproveSwitch } from "@/components/AutoApproveSwitch"; import { TelemetrySwitch } from "@/components/TelemetrySwitch"; import { MaxChatTurnsSelector } from "@/components/MaxChatTurnsSelector"; +import { ThinkingBudgetSelector } from "@/components/ThinkingBudgetSelector"; import { useSettings } from "@/hooks/useSettings"; import { useAppVersion } from "@/hooks/useAppVersion"; import { Button } from "@/components/ui/button"; @@ -138,6 +139,10 @@ export default function SettingsPage() { +
+ +
+