Unverified 提交 89f95ff6 authored 作者: Mohamed Aziz Mejri's avatar Mohamed Aziz Mejri 提交者: GitHub

Adding nitro server layer support for vite apps (#3251)

上级 b058b83f
import type { LocalAgentFixture } from "../../../../testing/fake-llm-server/localAgentTypes";
export const fixture: LocalAgentFixture = {
description: "Enable the Nitro server layer on a Vite app",
turns: [
{
text: "I'll add a Nitro server layer so the app can run server-side code.",
toolCalls: [
{
name: "enable_nitro",
args: {
reason: "User asked to store form submissions in Postgres.",
},
},
],
},
{
text: "Server layer added. Now update vite.config.ts and write the API route.",
},
],
};
......@@ -18,8 +18,8 @@
"@types/node": "^22.5.5",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react-swc": "^3.9.0",
"@vitejs/plugin-react-swc": "^4.3.0",
"typescript": "^5.5.3",
"vite": "^6.3.4"
"vite": "^7.3.2"
}
}
import path from "path";
import { testSkipIfWindows } from "./helpers/test_helper";
import { testSkipIfWindows, Timeout } from "./helpers/test_helper";
/**
* Test for security review in local-agent mode
......@@ -75,3 +75,21 @@ testSkipIfWindows("local-agent - mcp tool call", async ({ po }) => {
await po.snapshotMessages();
});
/**
* Test for enable_nitro tool in local-agent mode on a Vite app.
*/
testSkipIfWindows("local-agent - enable nitro", async ({ po }) => {
await po.setUpDyadPro({ localAgent: true });
await po.importApp("minimal");
await po.chatActions.selectLocalAgentMode();
await po.sendPrompt("tc=local-agent/enable-nitro", {
skipWaitForCompletion: true,
});
// Install of `nitro` goes through socket firewall, which can be slow on first run.
await po.chatActions.waitForChatCompletion({ timeout: Timeout.LONG });
await po.snapshotMessages();
});
......@@ -186,13 +186,13 @@ A file (2)
"zod": "^3.23.8"
},
"devDependencies": {
"@dyad-sh/react-vite-component-tagger": "^0.8.0",
"@dyad-sh/react-vite-component-tagger": "^0.9.0",
"@eslint/js": "^9.9.0",
"@tailwindcss/typography": "^0.5.15",
"@types/node": "^22.5.5",
"@types/react": "^19.2.8",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react-swc": "^3.9.0",
"@vitejs/plugin-react-swc": "^4.3.0",
"autoprefixer": "^10.4.20",
"eslint": "^9.9.0",
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
......@@ -202,7 +202,7 @@ A file (2)
"tailwindcss": "^3.4.11",
"typescript": "^5.5.3",
"typescript-eslint": "^8.0.1",
"vite": "^6.3.4"
"vite": "^8.0.0"
},
"packageManager": "<scrubbed>"
}
......
......@@ -65,9 +65,9 @@ A file (2)
"@types/node": "^22.5.5",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react-swc": "^3.9.0",
"@vitejs/plugin-react-swc": "^4.3.0",
"typescript": "^5.5.3",
"vite": "^6.3.4"
"vite": "^7.3.2"
},
"packageManager": "<scrubbed>"
}
......
......@@ -43,7 +43,7 @@
},
{
"path": "package.json",
"content": "{\n \"name\": \"vite_react_shadcn_ts\",\n \"version\": \"0.0.0\",\n \"private\": true,\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"vite\",\n \"build\": \"vite build\",\n \"build:dev\": \"vite build --mode development\",\n \"lint\": \"eslint .\",\n \"preview\": \"vite preview\"\n },\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\": \"^19.2.3\",\n \"react-day-picker\": \"^9.13.0\",\n \"react-dom\": \"^19.2.3\",\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\": \"^19.2.8\",\n \"@types/react-dom\": \"^19.2.3\",\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}\n",
"content": "{\n \"name\": \"vite_react_shadcn_ts\",\n \"version\": \"0.0.0\",\n \"private\": true,\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"vite\",\n \"build\": \"vite build\",\n \"build:dev\": \"vite build --mode development\",\n \"lint\": \"eslint .\",\n \"preview\": \"vite preview\"\n },\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\": \"^19.2.3\",\n \"react-day-picker\": \"^9.13.0\",\n \"react-dom\": \"^19.2.3\",\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.9.0\",\n \"@eslint/js\": \"^9.9.0\",\n \"@tailwindcss/typography\": \"^0.5.15\",\n \"@types/node\": \"^22.5.5\",\n \"@types/react\": \"^19.2.8\",\n \"@types/react-dom\": \"^19.2.3\",\n \"@vitejs/plugin-react-swc\": \"^4.3.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\": \"^8.0.0\"\n }\n}\n",
"force": false
},
{
......
- paragraph: /Generate an AI_RULES\.md file for this app\. Describe the tech stack in 5-\d+ bullet points and describe clear rules about what libraries to use for what\./
- button "file1.txt file1.txt Edit":
- img
- text: ""
- button "Edit":
- img
- text: ""
- img
- paragraph: More EOM
- button "Copy":
- img
- img
- text: Approved
- img
- text: claude-opus-4-5
- img
- text: less than a minute ago
- img
- text: "Version 2: (1 files changed)"
- button "Copy Request ID":
- img
- text: ""
- paragraph: tc=local-agent/enable-nitro
- paragraph: I'll add a Nitro server layer so the app can run server-side code.
- img
- text: Server layer Added Nitro server layer
- img
- paragraph:
- text: API routes can now live under
- code: "`server/routes/api/`"
- paragraph: Server layer added. Now update vite.config.ts and write the API route.
- button "Copy":
- img
- img
- text: Approved
- img
- text: claude-opus-4-5
- img
- text: less than a minute ago
- img
- text: "Version 3: (5 files changed)"
- button "Copy Request ID":
- img
- text: ""
- button "Undo":
- img
- text: ""
- button "Retry":
- img
- text: ""
\ No newline at end of file
......@@ -359,6 +359,27 @@
}
}
},
{
"type": "function",
"function": {
"name": "enable_nitro",
"description": "Add a Nitro server layer to this Vite app so it can run secure server-side code\n(API routes, database clients, secrets, webhooks).\n\nWHEN TO CALL: Before writing any code under server/, before referencing DATABASE_URL\nor any server-only env var, or when the user asks for an API route, webhook, or\nserver-side compute. Skip for client-side fetch with public/anon keys, for use\ncases fully covered by Supabase (anon key + RLS), or when the user explicitly\nsays \"static only\" / \"no backend\".\n\nAfter this tool returns, follow the \"Nitro Server Layer\" section appended to\nAI_RULES.md — it covers the required vite.config.ts changes and the conventions\nfor routes under server/routes/api/.",
"parameters": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"reason": {
"type": "string",
"description": "One sentence explaining why server-side code is needed for this prompt."
}
},
"required": [
"reason"
],
"additionalProperties": false
}
}
},
{
"type": "function",
"function": {
......
......@@ -367,6 +367,27 @@
}
}
},
{
"type": "function",
"function": {
"name": "enable_nitro",
"description": "Add a Nitro server layer to this Vite app so it can run secure server-side code\n(API routes, database clients, secrets, webhooks).\n\nWHEN TO CALL: Before writing any code under server/, before referencing DATABASE_URL\nor any server-only env var, or when the user asks for an API route, webhook, or\nserver-side compute. Skip for client-side fetch with public/anon keys, for use\ncases fully covered by Supabase (anon key + RLS), or when the user explicitly\nsays \"static only\" / \"no backend\".\n\nAfter this tool returns, follow the \"Nitro Server Layer\" section appended to\nAI_RULES.md — it covers the required vite.config.ts changes and the conventions\nfor routes under server/routes/api/.",
"parameters": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"reason": {
"type": "string",
"description": "One sentence explaining why server-side code is needed for this prompt."
}
},
"required": [
"reason"
],
"additionalProperties": false
}
}
},
{
"type": "function",
"function": {
......
......@@ -43,7 +43,7 @@
},
{
"path": "package.json",
"content": "{\n \"name\": \"vite_react_shadcn_ts\",\n \"version\": \"0.0.0\",\n \"private\": true,\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"vite\",\n \"build\": \"vite build\",\n \"build:dev\": \"vite build --mode development\",\n \"lint\": \"eslint .\",\n \"preview\": \"vite preview\"\n },\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\": \"^19.2.3\",\n \"react-day-picker\": \"^9.13.0\",\n \"react-dom\": \"^19.2.3\",\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\": \"^19.2.8\",\n \"@types/react-dom\": \"^19.2.3\",\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}\n",
"content": "{\n \"name\": \"vite_react_shadcn_ts\",\n \"version\": \"0.0.0\",\n \"private\": true,\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"vite\",\n \"build\": \"vite build\",\n \"build:dev\": \"vite build --mode development\",\n \"lint\": \"eslint .\",\n \"preview\": \"vite preview\"\n },\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\": \"^19.2.3\",\n \"react-day-picker\": \"^9.13.0\",\n \"react-dom\": \"^19.2.3\",\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.9.0\",\n \"@eslint/js\": \"^9.9.0\",\n \"@tailwindcss/typography\": \"^0.5.15\",\n \"@types/node\": \"^22.5.5\",\n \"@types/react\": \"^19.2.8\",\n \"@types/react-dom\": \"^19.2.3\",\n \"@vitejs/plugin-react-swc\": \"^4.3.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\": \"^8.0.0\"\n }\n}\n",
"force": false
},
{
......
......@@ -65,9 +65,9 @@ A file (2)
"@types/node": "^22.5.5",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react-swc": "^3.9.0",
"@vitejs/plugin-react-swc": "^4.3.0",
"typescript": "^5.5.3",
"vite": "^6.3.4"
"vite": "^7.3.2"
},
"packageManager": "<scrubbed>"
}
......
......@@ -65,9 +65,9 @@ A file (2)
"@types/node": "^22.5.5",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react-swc": "^3.9.0",
"@vitejs/plugin-react-swc": "^4.3.0",
"typescript": "^5.5.3",
"vite": "^6.3.4"
"vite": "^7.3.2"
},
"packageManager": "<scrubbed>"
}
......
......@@ -95,7 +95,7 @@ dist-ssr
"tailwindcss": "^4.1.8"
},
"devDependencies": {
"@dyad-sh/react-vite-component-tagger": "^0.8.0",
"@dyad-sh/react-vite-component-tagger": "^0.9.0",
"@types/node": "^22.5.5",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
......
......@@ -43,7 +43,7 @@
},
{
"path": "package.json",
"content": "{\n \"name\": \"vite_react_shadcn_ts\",\n \"version\": \"0.0.0\",\n \"private\": true,\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"vite\",\n \"build\": \"vite build\",\n \"build:dev\": \"vite build --mode development\",\n \"lint\": \"eslint .\",\n \"preview\": \"vite preview\"\n },\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\": \"^19.2.3\",\n \"react-day-picker\": \"^9.13.0\",\n \"react-dom\": \"^19.2.3\",\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\": \"^19.2.8\",\n \"@types/react-dom\": \"^19.2.3\",\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}\n",
"content": "{\n \"name\": \"vite_react_shadcn_ts\",\n \"version\": \"0.0.0\",\n \"private\": true,\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"vite\",\n \"build\": \"vite build\",\n \"build:dev\": \"vite build --mode development\",\n \"lint\": \"eslint .\",\n \"preview\": \"vite preview\"\n },\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\": \"^19.2.3\",\n \"react-day-picker\": \"^9.13.0\",\n \"react-dom\": \"^19.2.3\",\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.9.0\",\n \"@eslint/js\": \"^9.9.0\",\n \"@tailwindcss/typography\": \"^0.5.15\",\n \"@types/node\": \"^22.5.5\",\n \"@types/react\": \"^19.2.8\",\n \"@types/react-dom\": \"^19.2.3\",\n \"@vitejs/plugin-react-swc\": \"^4.3.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\": \"^8.0.0\"\n }\n}\n",
"force": false
},
{
......
......@@ -43,7 +43,7 @@
},
{
"path": "package.json",
"content": "{\n \"name\": \"vite_react_shadcn_ts\",\n \"version\": \"0.0.0\",\n \"private\": true,\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"vite\",\n \"build\": \"vite build\",\n \"build:dev\": \"vite build --mode development\",\n \"lint\": \"eslint .\",\n \"preview\": \"vite preview\"\n },\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\": \"^19.2.3\",\n \"react-day-picker\": \"^9.13.0\",\n \"react-dom\": \"^19.2.3\",\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\": \"^19.2.8\",\n \"@types/react-dom\": \"^19.2.3\",\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}\n",
"content": "{\n \"name\": \"vite_react_shadcn_ts\",\n \"version\": \"0.0.0\",\n \"private\": true,\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"vite\",\n \"build\": \"vite build\",\n \"build:dev\": \"vite build --mode development\",\n \"lint\": \"eslint .\",\n \"preview\": \"vite preview\"\n },\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\": \"^19.2.3\",\n \"react-day-picker\": \"^9.13.0\",\n \"react-dom\": \"^19.2.3\",\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.9.0\",\n \"@eslint/js\": \"^9.9.0\",\n \"@tailwindcss/typography\": \"^0.5.15\",\n \"@types/node\": \"^22.5.5\",\n \"@types/react\": \"^19.2.8\",\n \"@types/react-dom\": \"^19.2.3\",\n \"@vitejs/plugin-react-swc\": \"^4.3.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\": \"^8.0.0\"\n }\n}\n",
"force": false
},
{
......
......@@ -65,9 +65,9 @@ A file (2)
"@types/node": "^22.5.5",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react-swc": "^3.9.0",
"@vitejs/plugin-react-swc": "^4.3.0",
"typescript": "^5.5.3",
"vite": "^6.3.4"
"vite": "^7.3.2"
},
"packageManager": "<scrubbed>"
}
......
{
"name": "dyad",
"version": "0.44.0-beta.1",
"version": "0.44.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "dyad",
"version": "0.44.0-beta.1",
"version": "0.44.0",
"license": "MIT",
"dependencies": {
"@ai-sdk/amazon-bedrock": "^4.0.46",
......
差异被折叠。
......@@ -12,10 +12,12 @@ IMPORTANT: Do NOT generate SQL migration files by hand! This is wrong.
## Drizzle migration conflicts during rebase
When rebasing a branch that has drizzle migrations conflicting with upstream (e.g., both have `0023_*.sql`):
When rebasing a branch that has drizzle migrations conflicting with upstream (e.g., both have `0028_*.sql`), prefer regenerating over manually editing snapshot/journal files:
1. Keep upstream's migration files (they're already deployed to production)
2. Rename the PR's conflicting migration to the next available index (e.g., `0023_romantic_mantis.sql``0025_romantic_mantis.sql`)
3. Update `drizzle/meta/_journal.json` to include all migrations with correct indices
4. Create/update the snapshot file (`drizzle/meta/00XX_snapshot.json`) with the new index, updating `prevId` to reference the previous snapshot's `id`
5. If the PR had subsequent commits that deleted/modified its migration files, those changes become no-ops after renaming — just accept the deletion conflicts by staging the renamed files
1. During the conflict, accept upstream's `drizzle/meta/_journal.json` and `drizzle/meta/00XX_snapshot.json` with `git checkout --ours <file>` (in a rebase, `--ours` = the branch being rebased onto, i.e. upstream).
2. Force-remove the PR's conflicting `drizzle/00XX_*.sql` with `git rm -f` (it's staged as a new file and must be unstaged via `-f`).
3. Stage the resolved metadata and run `git rebase --continue`. Verify `src/db/schema.ts` still contains the PR's schema additions (e.g., `nitroEnabled` column) — the rebase usually merges these correctly.
4. After the rebase completes, run `npm run db:generate` — drizzle-kit will compare the schema to the latest snapshot and emit a fresh `00YY_*.sql` and `00YY_snapshot.json` with the correct next index and `prevId`.
5. Commit the regenerated migration as a separate commit (e.g., `chore(db): renumber migration to 00YY after rebase`).
This avoids manual snapshot/journal editing and `prevId` mistakes.
......@@ -153,6 +153,7 @@ The stashed changes will be automatically merged back after the rebase completes
- **Preserve variable declarations used in common code**: When one side of a conflict declares a variable (e.g., `const iframe = po.previewPanel.getPreviewIframeElement()`) that is referenced in non-conflicting code between or after conflict markers, keep the declaration even when adopting the other side's verification approach — the variable is needed regardless of which style you choose
- **React component wrapper conflicts**: When rebasing UI changes that conflict on wrapper div classes (e.g., `flex items-start space-x-2` vs `flex items-end gap-1`), keep the newer styling from the incoming commit but preserve any functional components (like dialogs or modals) that exist in HEAD but not in the incoming change
- **Refactoring conflicts**: When incoming commits refactor code (e.g., extracting inline logic into helper functions), and HEAD has new features in the same area, integrate HEAD's features into the new structure. Example: if incoming code moves streaming logic to `runSingleStreamPass()` and HEAD adds mid-turn compaction to the inline code, add compaction support to the new function rather than keeping the old inline version
- **Snapshot file conflicts (e.g., `e2e-tests/snapshots/*.txt`, `*.snap`)**: When a rebase conflicts on a snapshot, neither side may match what the rebased code actually produces (e.g., upstream changed the system prompt, your branch added new tools). Resolve quickly with `git checkout --theirs <file>` to unblock the rebase, then **regenerate snapshots after the rebase completes**: `npm test -- -u` for vitest snapshots, and re-run the affected E2E spec with `--update-snapshots` for E2E `.txt`/`.yml` snapshots. The system-prompt snapshot in `src/__tests__/__snapshots__/local_agent_prompt.test.ts.snap` and the matching E2E snapshots often drift together — after rebasing, expect to update both.
## Rebasing with uncommitted changes
......
......@@ -27,3 +27,7 @@ Agent tool definitions live in `src/pro/main/ipc/handlers/local_agent/tools/`. E
## Prompt and request snapshots
- When changing local-agent prompt text or tool descriptions, update both prompt unit snapshots and E2E request snapshots; stale request snapshots can still contain old tool descriptions even after unit prompt snapshots pass.
## Tool spec mock contexts
- When adding a required field to `AgentContext` (in `tools/types.ts`), grep `src/pro/main/ipc/handlers/local_agent/tools/*.spec.ts` and update every mock context literal. The TS error appears as e.g. `Property 'nitroEnabled' is missing in type ... but required in type 'AgentContext'` and surfaces only via `npm run ts``npm run lint` does not catch it.
......@@ -62,13 +62,13 @@
"zod": "^3.23.8"
},
"devDependencies": {
"@dyad-sh/react-vite-component-tagger": "^0.8.0",
"@dyad-sh/react-vite-component-tagger": "^0.9.0",
"@eslint/js": "^9.9.0",
"@tailwindcss/typography": "^0.5.15",
"@types/node": "^22.5.5",
"@types/react": "^19.2.8",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react-swc": "^3.9.0",
"@vitejs/plugin-react-swc": "^4.3.0",
"autoprefixer": "^10.4.20",
"eslint": "^9.9.0",
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
......@@ -78,6 +78,6 @@
"tailwindcss": "^3.4.11",
"typescript": "^5.5.3",
"typescript-eslint": "^8.0.1",
"vite": "^6.3.4"
"vite": "^8.0.0"
}
}
差异被折叠。
......@@ -7,6 +7,22 @@ describe("local_agent_prompt", () => {
expect(prompt).toMatchSnapshot();
});
it("agent mode system prompt (vite framework includes Nitro nudge)", () => {
const prompt = constructLocalAgentPrompt(undefined, undefined, {
frameworkType: "vite",
});
expect(prompt).toMatchSnapshot();
});
it("agent mode system prompt (vite + supabase suppresses Nitro nudge)", () => {
const prompt = constructLocalAgentPrompt(undefined, undefined, {
frameworkType: "vite",
hasSupabaseProject: true,
});
expect(prompt).not.toContain("<server_layer>");
expect(prompt).not.toContain("enable_nitro");
});
it("basic agent mode system prompt", () => {
const prompt = constructLocalAgentPrompt(undefined, undefined, {
basicAgentMode: true,
......@@ -14,6 +30,14 @@ describe("local_agent_prompt", () => {
expect(prompt).toMatchSnapshot();
});
it("basic agent mode system prompt (vite framework includes Nitro nudge)", () => {
const prompt = constructLocalAgentPrompt(undefined, undefined, {
basicAgentMode: true,
frameworkType: "vite",
});
expect(prompt).toMatchSnapshot();
});
it("ask mode system prompt", () => {
const prompt = constructLocalAgentPrompt(undefined, undefined, {
readOnly: true,
......
import React from "react";
import { Server } from "lucide-react";
import {
DyadCard,
DyadCardHeader,
DyadBadge,
DyadStateIndicator,
} from "./DyadCardPrimitives";
import { CustomTagState } from "./stateTypes";
interface DyadEnableNitroProps {
state?: CustomTagState;
}
export const DyadEnableNitro: React.FC<DyadEnableNitroProps> = ({ state }) => {
const isPending = state === "pending";
const isAborted = state === "aborted";
const headline = isPending
? "Adding Nitro server layer"
: isAborted
? "Nitro server layer setup aborted"
: "Added Nitro server layer";
return (
<DyadCard accentColor="emerald" state={state}>
<DyadCardHeader icon={<Server size={15} />} accentColor="emerald">
<DyadBadge color="emerald">Server layer</DyadBadge>
<span className="text-sm font-medium text-foreground">{headline}</span>
{state && (
<DyadStateIndicator state={state} abortedLabel="Did not finish" />
)}
</DyadCardHeader>
{!isPending && !isAborted && (
<div className="px-3 pb-3">
<p className="text-xs text-muted-foreground leading-snug">
API routes can now live under{" "}
<code className="font-mono text-[11px] px-1 py-0.5 rounded bg-muted">
server/routes/api/
</code>
</p>
</div>
)}
</DyadCard>
);
};
......@@ -11,6 +11,7 @@ import { DyadExecuteSql } from "./DyadExecuteSql";
import { DyadLogs } from "./DyadLogs";
import { DyadGrep } from "./DyadGrep";
import { DyadAddIntegration } from "./DyadAddIntegration";
import { DyadEnableNitro } from "./DyadEnableNitro";
import { DyadEdit } from "./DyadEdit";
import { DyadSearchReplace } from "./DyadSearchReplace";
import { DyadCodebaseContext } from "./DyadCodebaseContext";
......@@ -57,6 +58,7 @@ const DYAD_CUSTOM_TAGS = [
"dyad-execute-sql",
"dyad-read-logs",
"dyad-add-integration",
"dyad-enable-nitro",
"dyad-output",
"dyad-problem-report",
"dyad-chat-summary",
......@@ -612,6 +614,9 @@ function renderCustomTag(
</DyadAddIntegration>
);
case "dyad-enable-nitro":
return <DyadEnableNitro state={getState({ isStreaming, inProgress })} />;
case "dyad-edit":
return (
<DyadEdit
......
......@@ -22,6 +22,7 @@ import {
constructSystemPrompt,
readAiRules,
} from "../../prompts/system_prompt";
import { detectFrameworkType } from "../utils/framework_utils";
import { getThemePromptById } from "../utils/theme_utils";
import {
getSupabaseAvailableSystemPrompt,
......@@ -813,6 +814,10 @@ ${componentSnippet}
`Theme for app ${updatedChat.app.id}: ${updatedChat.app.themeId ?? "none"}, prompt length: ${themePrompt.length} chars`,
);
const frameworkType = detectFrameworkType(
getDyadAppPath(updatedChat.app.path),
);
// Migration on read converts "agent" to "build", so no need to check for it here
let systemPrompt = constructSystemPrompt({
aiRules,
......@@ -820,6 +825,8 @@ ${componentSnippet}
enableTurboEditsV2: isTurboEditsV2Enabled(settings),
themePrompt,
basicAgentMode: isBasicAgentMode(settings),
frameworkType,
hasSupabaseProject: !!updatedChat.app?.supabaseProjectId,
});
// Add information about mentioned apps for build mode only.
......@@ -1362,6 +1369,8 @@ This conversation includes one or more image attachments. When the user uploads
),
chatMode: "build",
enableTurboEditsV2: false,
frameworkType,
hasSupabaseProject: !!updatedChat.app?.supabaseProjectId,
}),
files: files,
dyadDisableFiles: true,
......
......@@ -12,6 +12,7 @@ import {
} from "../../prompts/supabase_prompt";
import { buildNeonPromptForApp } from "../../neon_admin/neon_prompt_context";
import { getDyadAppPath } from "../../paths/paths";
import { detectFrameworkType } from "../utils/framework_utils";
import log from "electron-log";
import { extractCodebase } from "../../utils/codebase";
import {
......@@ -80,12 +81,15 @@ export function registerTokenCountHandlers() {
// Count system prompt tokens
// Migration on read converts "agent" to "build", so no need to check for it here
const themePrompt = await getThemePromptById(chat.app?.themeId ?? null);
const frameworkType = detectFrameworkType(getDyadAppPath(chat.app.path));
let systemPrompt = constructSystemPrompt({
aiRules: await readAiRules(getDyadAppPath(chat.app.path)),
chatMode:
selectedChatMode === "local-agent" ? "build" : selectedChatMode,
enableTurboEditsV2: isTurboEditsV2Enabled(settings),
themePrompt,
frameworkType,
hasSupabaseProject: !!chat.app?.supabaseProjectId,
});
let supabaseContext = "";
......
......@@ -155,14 +155,14 @@ async function runAddDependencyCommand(
}
}
export async function executeAddDependency({
export async function installPackages({
packages,
message,
appPath,
dev = false,
}: {
packages: string[];
message: Message;
appPath: string;
dev?: boolean;
}): Promise<ExecuteAddDependencyResult> {
const invalidPackage = packages.find(
(pkg) => !NPM_PACKAGE_NAME_PATTERN.test(pkg),
......@@ -192,10 +192,13 @@ export async function executeAddDependency({
}
const packageManager = await detectPreferredPackageManager();
let { succeeded, installResults, lastError } = await runAddDependencyCommand(
buildAddDependencyCommand(packages, packageManager, useSocketFirewall),
appPath,
);
const { succeeded, installResults, lastError } =
await runAddDependencyCommand(
buildAddDependencyCommand(packages, packageManager, useSocketFirewall, {
dev,
}),
appPath,
);
if (!succeeded && lastError) {
throw new ExecuteAddDependencyError({
......@@ -204,6 +207,26 @@ export async function executeAddDependency({
});
}
return {
installResults,
warningMessages,
};
}
export async function executeAddDependency({
packages,
message,
appPath,
}: {
packages: string[];
message: Message;
appPath: string;
}): Promise<ExecuteAddDependencyResult> {
const { installResults, warningMessages } = await installPackages({
packages,
appPath,
});
// Update the message content with the installation results
const escapedPackages = escapeXmlAttr(packages.join(" "));
const updatedContent = message.content.replace(
......
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import {
NITRO_RULES_END,
NITRO_RULES_START,
appendNitroRules,
restoreAiRules,
} from "./ai_rules_patcher";
describe("ai_rules_patcher", () => {
let appPath: string;
beforeEach(async () => {
appPath = await fs.mkdtemp(path.join(os.tmpdir(), "ai-rules-patcher-"));
});
afterEach(async () => {
await fs.rm(appPath, { recursive: true, force: true });
});
async function readFile() {
return fs.readFile(path.join(appPath, "AI_RULES.md"), "utf8");
}
it("creates AI_RULES.md when missing and appends the Nitro section", async () => {
const result = await appendNitroRules(appPath);
expect(result.wasAppended).toBe(true);
expect(result.backup).toBeNull();
const contents = await readFile();
expect(contents).toContain(NITRO_RULES_START);
expect(contents).toContain("## Nitro Server Layer");
expect(contents).toContain(NITRO_RULES_END);
});
it("preserves existing content above the Nitro markers", async () => {
const original = "# My Project Rules\n\nUse TypeScript.\n";
await fs.writeFile(path.join(appPath, "AI_RULES.md"), original, "utf8");
const result = await appendNitroRules(appPath);
expect(result.wasAppended).toBe(true);
expect(result.backup).toBe(original);
const contents = await readFile();
expect(contents.startsWith(original)).toBe(true);
expect(contents).toContain(NITRO_RULES_START);
});
it("is idempotent — running twice produces identical output", async () => {
const original = "# Rules\n";
await fs.writeFile(path.join(appPath, "AI_RULES.md"), original, "utf8");
await appendNitroRules(appPath);
const afterFirst = await readFile();
const second = await appendNitroRules(appPath);
const afterSecond = await readFile();
expect(second.wasAppended).toBe(false);
expect(afterSecond).toBe(afterFirst);
});
it("restoreAiRules rewrites the original contents when backup is non-null", async () => {
const original = "# Keep me\n";
await fs.writeFile(path.join(appPath, "AI_RULES.md"), original, "utf8");
const { backup } = await appendNitroRules(appPath);
await restoreAiRules(appPath, backup);
const restored = await readFile();
expect(restored).toBe(original);
});
it("restoreAiRules deletes the file when backup is null", async () => {
await appendNitroRules(appPath);
await restoreAiRules(appPath, null);
await expect(readFile()).rejects.toMatchObject({ code: "ENOENT" });
});
});
import fs from "node:fs/promises";
import path from "node:path";
export const NITRO_RULES_START = "<!-- nitro:start -->";
export const NITRO_RULES_END = "<!-- nitro:end -->";
export const NITRO_RULES_SECTION = `${NITRO_RULES_START}
## Nitro Server Layer
This project has a Nitro server layer for backend API routes. A \`nitro.config.ts\` at the app root sets \`serverDir: "./server"\` — do not move or remove it.
### vite.config.ts setup
Register the Nitro plugin as the LAST entry in the \`plugins\` array (after \`react()\`). It must run after Vite's module-transform middleware, otherwise Nitro's SPA fallback intercepts Vite internal URLs (\`/src/*.tsx\`, \`/@vite/client\`, \`/@react-refresh\`, \`/@fs/*\`) and returns \`index.html\`, breaking the preview.
\`\`\`ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
import { nitro } from "nitro/vite";
export default defineConfig(() => ({
plugins: [react(), nitro()],
}));
\`\`\`
### API Route Conventions
- Write routes in \`server/routes/api/\` (NEVER top-level \`/api/\`)
- Use \`defineHandler\` from \`"nitro"\` for handlers
- Dynamic routes: \`[param].ts\`
- Method-specific: \`hello.get.ts\`, \`hello.post.ts\`
- Runtime config: \`useRuntimeConfig()\` (env vars prefixed with \`NITRO_\`)
### Security Rules
NEVER import server-side code (database clients, secrets, env vars) in client-side React components. Server code lives in \`server/\` only.
${NITRO_RULES_END}`;
export interface AiRulesBackup {
/** Original contents before patching, or null if the file did not exist. */
backup: string | null;
/** True if the patcher appended the Nitro section this call. */
wasAppended: boolean;
}
function aiRulesPath(appPath: string): string {
return path.join(appPath, "AI_RULES.md");
}
async function readIfExists(filePath: string): Promise<string | null> {
try {
return await fs.readFile(filePath, "utf8");
} catch (err) {
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
return null;
}
throw err;
}
}
/**
* Idempotently append the Nitro rules section to `<app>/AI_RULES.md`.
*
* - If the file doesn't exist, it's created with just the Nitro section.
* - If the Nitro markers already exist, the file is left unchanged.
* - The original file contents are returned for rollback via `restoreAiRules`.
*/
export async function appendNitroRules(
appPath: string,
): Promise<AiRulesBackup> {
const filePath = aiRulesPath(appPath);
const existing = await readIfExists(filePath);
if (existing !== null && existing.includes(NITRO_RULES_START)) {
return { backup: existing, wasAppended: false };
}
const separator =
existing === null || existing.length === 0
? ""
: existing.endsWith("\n")
? "\n"
: "\n\n";
const next = (existing ?? "") + separator + NITRO_RULES_SECTION + "\n";
await fs.writeFile(filePath, next, "utf8");
return { backup: existing, wasAppended: true };
}
/**
* Restore `AI_RULES.md` to the contents captured in a prior `appendNitroRules`
* call. If the backup is null, the file is deleted (the patcher had created
* it). Safe to call even if nothing changed.
*/
export async function restoreAiRules(
appPath: string,
backup: string | null,
): Promise<void> {
const filePath = aiRulesPath(appPath);
if (backup === null) {
await fs.rm(filePath, { force: true });
return;
}
await fs.writeFile(filePath, backup, "utf8");
}
......@@ -7,6 +7,11 @@ import {
/**
* Detect the framework type for an app by checking config files and package.json.
*
* Vite apps with a Nitro server layer (added via `enable_nitro`) are reported
* as `"vite-nitro"`. Detection looks for `nitro.config.{ts,js,mjs}` first, then
* falls back to `nitro` in package.json deps — either is sufficient since the
* tool writes the config file and installs the package together.
*/
export function detectFrameworkType(appPath: string): AppFrameworkType | null {
try {
......@@ -17,22 +22,29 @@ export function detectFrameworkType(appPath: string): AppFrameworkType | null {
}
const viteConfigs = ["vite.config.js", "vite.config.ts", "vite.config.mjs"];
let isVite = false;
for (const config of viteConfigs) {
if (fs.existsSync(path.join(appPath, config))) {
return "vite";
isVite = true;
break;
}
}
// Fallback: check package.json dependencies
let packageJsonDeps: Record<string, string> | null = null;
const packageJsonPath = path.join(appPath, "package.json");
if (fs.existsSync(packageJsonPath)) {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
const deps = {
const deps: Record<string, string> = {
...packageJson.dependencies,
...packageJson.devDependencies,
};
if (deps.next) return "nextjs";
if (deps.vite) return "vite";
packageJsonDeps = deps;
if (!isVite && deps.next) return "nextjs";
if (!isVite && deps.vite) isVite = true;
}
if (isVite) {
return hasNitro(appPath, packageJsonDeps) ? "vite-nitro" : "vite";
}
return "other";
......@@ -41,6 +53,21 @@ export function detectFrameworkType(appPath: string): AppFrameworkType | null {
}
}
function hasNitro(
appPath: string,
deps: Record<string, string> | null,
): boolean {
const nitroConfigs = [
"nitro.config.ts",
"nitro.config.js",
"nitro.config.mjs",
];
for (const config of nitroConfigs) {
if (fs.existsSync(path.join(appPath, config))) return true;
}
return Boolean(deps?.nitro);
}
/**
* Read the Next.js major version from the app's package.json.
* Returns null when next is not installed or the version string is non-numeric
......
......@@ -130,6 +130,41 @@ describe("buildAddDependencyCommand", () => {
).toEqual(expected);
},
);
it.each<[PackageManager, boolean, { command: string; args: string[] }]>([
["pnpm", false, { command: "pnpm", args: ["add", "-D", "nitro"] }],
[
"npm",
false,
{
command: "npm",
args: ["install", "--legacy-peer-deps", "--save-dev", "nitro"],
},
],
[
"pnpm",
true,
{
command: "npx",
args: [
"--prefer-offline",
"--yes",
"sfw@2.0.4",
"pnpm",
"add",
"-D",
"nitro",
],
},
],
])(
"installs as a devDependency for %s with sfw=%s when dev:true",
(manager, useSfw, expected) => {
expect(
buildAddDependencyCommand(["nitro"], manager, useSfw, { dev: true }),
).toEqual(expected);
},
);
});
describe("ensureSocketFirewallInstalled", () => {
......
......@@ -208,11 +208,18 @@ export function buildAddDependencyCommand(
packages: string[],
packageManager: PackageManager,
useSocketFirewall: boolean,
options: { dev?: boolean } = {},
): { command: string; args: string[] } {
const { dev = false } = options;
const packageManagerArgs =
packageManager === "pnpm"
? ["add", ...packages]
: ["install", "--legacy-peer-deps", ...packages];
? ["add", ...(dev ? ["-D"] : []), ...packages]
: [
"install",
"--legacy-peer-deps",
...(dev ? ["--save-dev"] : []),
...packages,
];
if (useSocketFirewall) {
return {
......
export const APP_FRAMEWORK_TYPES = ["nextjs", "vite", "other"] as const;
export const APP_FRAMEWORK_TYPES = [
"nextjs",
"vite",
"vite-nitro",
"other",
] as const;
export type AppFrameworkType = (typeof APP_FRAMEWORK_TYPES)[number];
export const NEXTJS_CONFIG_FILES = [
......
......@@ -3,6 +3,7 @@ import log from "electron-log";
import { getNeonClient } from "./neon_management_client";
import { IS_TEST_BUILD } from "@/ipc/utils/test_utils";
import { DyadError, DyadErrorKind } from "@/errors/dyad_error";
import type { AppFrameworkType } from "@/lib/framework_constants";
const logger = log.scope("neon_context");
......@@ -390,7 +391,7 @@ export async function getNeonTableSchema({
* Generate framework-specific client code for connecting to Neon.
*/
export function getNeonClientCode(
frameworkType: "nextjs" | "vite" | "other" | null,
frameworkType: AppFrameworkType | null,
): string {
if (frameworkType === "nextjs") {
return `// Neon Database Client (server-side only)
......
......@@ -6,11 +6,12 @@ import {
detectFrameworkType,
detectNextJsMajorVersion,
} from "../ipc/utils/framework_utils";
import type { AppFrameworkType } from "@/lib/framework_constants";
interface BuildNeonPromptAdditionsParams {
projectId: string;
branchId?: string | null;
frameworkType: "nextjs" | "vite" | "other" | null;
frameworkType: AppFrameworkType | null;
nextjsMajorVersion?: number | null;
includeContext: boolean;
isLocalAgentMode: boolean;
......
......@@ -20,6 +20,7 @@ import { listFilesTool } from "./tools/list_files";
import { getSupabaseProjectInfoTool } from "./tools/get_supabase_project_info";
import { setChatSummaryTool } from "./tools/set_chat_summary";
import { addIntegrationTool } from "./tools/add_integration";
import { enableNitroTool } from "./tools/enable_nitro";
import { readLogsTool } from "./tools/read_logs";
import { searchReplaceTool } from "./tools/search_replace";
import { webSearchTool } from "./tools/web_search";
......@@ -84,6 +85,7 @@ export const TOOL_DEFINITIONS: readonly ToolDefinition[] = [
getDatabaseTableSchemaTool,
setChatSummaryTool,
addIntegrationTool,
enableNitroTool,
readLogsTool,
webSearchTool,
webCrawlTool,
......
import fs from "node:fs/promises";
import path from "node:path";
import { z } from "zod";
import log from "electron-log";
import { ToolDefinition, AgentContext } from "./types";
import {
installPackages,
ExecuteAddDependencyError,
} from "@/ipc/processors/executeAddDependency";
import { appendNitroRules, restoreAiRules } from "@/ipc/utils/ai_rules_patcher";
const logger = log.scope("enable_nitro");
const NITRO_CONFIG_CONTENTS = `import { defineConfig } from "nitro";
export default defineConfig({
serverDir: "./server",
});
`;
async function writeNitroConfigIfMissing(
appPath: string,
): Promise<{ filePath: string; wasCreated: boolean }> {
const filePath = path.join(appPath, "nitro.config.ts");
try {
await fs.access(filePath);
return { filePath, wasCreated: false };
} catch (err) {
if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err;
}
await fs.writeFile(filePath, NITRO_CONFIG_CONTENTS, "utf8");
return { filePath, wasCreated: true };
}
const enableNitroSchema = z.object({
reason: z
.string()
.describe(
"One sentence explaining why server-side code is needed for this prompt.",
),
});
const ENABLE_NITRO_DESCRIPTION = `
Add a Nitro server layer to this Vite app so it can run secure server-side code
(API routes, database clients, secrets, webhooks).
WHEN TO CALL: Before writing any code under server/, before referencing DATABASE_URL
or any server-only env var, or when the user asks for an API route, webhook, or
server-side compute. Skip for client-side fetch with public/anon keys, for use
cases fully covered by Supabase (anon key + RLS), or when the user explicitly
says "static only" / "no backend".
After this tool returns, follow the "Nitro Server Layer" section appended to
AI_RULES.md — it covers the required vite.config.ts changes and the conventions
for routes under server/routes/api/.
`.trim();
export const enableNitroTool: ToolDefinition<
z.infer<typeof enableNitroSchema>
> = {
name: "enable_nitro",
description: ENABLE_NITRO_DESCRIPTION,
inputSchema: enableNitroSchema,
defaultConsent: "always",
modifiesState: true,
isEnabled: (ctx) =>
ctx.frameworkType === "vite" && ctx.supabaseProjectId === null,
getConsentPreview: (args) => `Add Nitro server layer (${args.reason})`,
buildXml: () => `<dyad-enable-nitro></dyad-enable-nitro>`,
execute: async (_args, ctx: AgentContext) => {
// Belt-and-suspenders: `isEnabled` already filters this tool out when
// the framework is already "vite-nitro", but we re-check here in case the
// LLM tries to call it twice in the same turn (e.g. parallel tool calls or
// a retry) since `ctx.frameworkType` is updated below after install.
if (ctx.frameworkType === "vite-nitro") {
return "Nitro is already enabled for this app. Skipping setup.";
}
const rulesBackup = await appendNitroRules(ctx.appPath);
let nitroConfigResult: { filePath: string; wasCreated: boolean } | null =
null;
let serverDirCreated = false;
const serverDirPath = path.join(ctx.appPath, "server");
try {
nitroConfigResult = await writeNitroConfigIfMissing(ctx.appPath);
try {
await fs.access(serverDirPath);
} catch (err) {
if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err;
serverDirCreated = true;
}
await fs.mkdir(path.join(serverDirPath, "routes", "api"), {
recursive: true,
});
await fs.writeFile(
path.join(serverDirPath, "routes", "api", ".gitkeep"),
"",
"utf8",
);
const result = await installPackages({
packages: ["nitro"],
appPath: ctx.appPath,
});
for (const warningMessage of result.warningMessages) {
ctx.onWarningMessage?.(warningMessage);
}
ctx.frameworkType = "vite-nitro";
} catch (error) {
try {
await restoreAiRules(ctx.appPath, rulesBackup.backup);
if (nitroConfigResult?.wasCreated) {
await fs.rm(nitroConfigResult.filePath, { force: true });
}
if (serverDirCreated) {
await fs.rm(serverDirPath, { recursive: true, force: true });
}
} catch (rollbackError) {
logger.error("Rollback failed during enable_nitro:", rollbackError);
}
if (error instanceof ExecuteAddDependencyError) {
for (const warningMessage of error.warningMessages) {
ctx.onWarningMessage?.(warningMessage);
}
}
throw error;
}
return "Nitro server layer added. Follow the 'Nitro Server Layer' section in AI_RULES.md to update vite.config.ts and write the requested API route(s) under server/routes/api/.";
},
};
......@@ -7,6 +7,7 @@ import { IpcMainInvokeEvent } from "electron";
import { jsonrepair } from "jsonrepair";
import { AgentToolConsent } from "@/lib/schemas";
import { AgentTodo } from "@/ipc/types";
import type { AppFrameworkType } from "@/lib/framework_constants";
// ============================================================================
// XML Escape Helpers
......@@ -53,7 +54,7 @@ export interface AgentContext {
supabaseOrganizationSlug: string | null;
neonProjectId: string | null;
neonActiveBranchId: string | null;
frameworkType: "nextjs" | "vite" | "other" | null;
frameworkType: AppFrameworkType | null;
messageId: number;
isSharedModulesChanged: boolean;
chatSummary?: string;
......
......@@ -3,6 +3,8 @@
* Tool-based agent with parallel execution support
*/
import type { AppFrameworkType } from "@/lib/framework_constants";
// ============================================================================
// Shared Prompt Blocks (used by both Pro and Basic Agent modes)
// ============================================================================
......@@ -191,6 +193,14 @@ You have READ-ONLY tools at your disposal to understand the codebase. Follow the
[[AI_RULES]]
`;
// ============================================================================
// Server Layer Block (Vite-only; injected when frameworkType === "vite")
// ============================================================================
const SERVER_LAYER_BLOCK = `<server_layer>
This is a Vite app with NO server layer yet. Call \`enable_nitro\` BEFORE writing any server-side code (API routes, database clients, secrets, webhooks) — see the tool's description for the authoritative WHEN TO CALL rules. Once enabled, AI_RULES.md will contain the required \`vite.config.ts\` setup and route conventions.
</server_layer>`;
// ============================================================================
// Image Generation Block (Pro mode only)
// ============================================================================
......@@ -226,7 +236,7 @@ ${PRO_TOOL_CALLING_BEST_PRACTICES_BLOCK}
${PRO_FILE_EDITING_TOOL_SELECTION_BLOCK}
${PRO_DEVELOPMENT_WORKFLOW_BLOCK}
[[SERVER_LAYER]]
${IMAGE_GENERATION_BLOCK}
[[AI_RULES]]
......@@ -250,7 +260,7 @@ ${BASIC_TOOL_CALLING_BEST_PRACTICES_BLOCK}
${BASIC_FILE_EDITING_TOOL_SELECTION_BLOCK}
${BASIC_DEVELOPMENT_WORKFLOW_BLOCK}
[[SERVER_LAYER]]
[[AI_RULES]]
`;
......@@ -284,7 +294,12 @@ Available packages and libraries:
export function constructLocalAgentPrompt(
aiRules: string | undefined,
themePrompt?: string,
options?: { readOnly?: boolean; basicAgentMode?: boolean },
options?: {
readOnly?: boolean;
basicAgentMode?: boolean;
frameworkType?: AppFrameworkType | null;
hasSupabaseProject?: boolean;
},
): string {
// Select the appropriate base prompt
let basePrompt: string;
......@@ -296,7 +311,19 @@ export function constructLocalAgentPrompt(
basePrompt = LOCAL_AGENT_SYSTEM_PROMPT;
}
let prompt = basePrompt.replace("[[AI_RULES]]", aiRules ?? DEFAULT_AI_RULES);
// The Nitro nudge only applies to Vite apps without Nitro yet. `vite-nitro`
// already has the server layer (covered by AI_RULES.md); other frameworks
// have their own server conventions. Apps with a Supabase project skip the
// nudge too — Supabase Edge Functions cover server-side code, and offering
// both layers confuses the model about which one to use.
const serverLayer =
options?.frameworkType === "vite" && !options?.hasSupabaseProject
? `\n${SERVER_LAYER_BLOCK}\n`
: "";
let prompt = basePrompt
.replace("[[SERVER_LAYER]]", serverLayer)
.replace("[[AI_RULES]]", aiRules ?? DEFAULT_AI_RULES);
// Append theme prompt if provided
if (themePrompt) {
......
import addAuthenticationGuide from "./guides/add-authentication.md?raw";
import addEmailVerificationGuide from "./guides/add-email-verification.md?raw";
import addPasswordResetGuide from "./guides/add-password-reset.md?raw";
import type { AppFrameworkType } from "@/lib/framework_constants";
export function getNeonAvailableSystemPrompt(
neonClientCode: string,
frameworkType: "nextjs" | "vite" | "other" | null,
frameworkType: AppFrameworkType | null,
options?: {
emailVerificationEnabled?: boolean;
nextjsMajorVersion?: number | null;
......
......@@ -4,6 +4,7 @@ import log from "electron-log";
import { TURBO_EDITS_V2_SYSTEM_PROMPT } from "../pro/main/prompts/turbo_edits_v2_prompt";
import { constructLocalAgentPrompt } from "./local_agent_prompt";
import { constructPlanModePrompt } from "./plan_mode_prompt";
import type { AppFrameworkType } from "@/lib/framework_constants";
const logger = log.scope("system_prompt");
......@@ -334,7 +335,17 @@ export const BUILD_SYSTEM_POSTFIX = `Directory names MUST be all lower-case (src
> Do NOT use <dyad-file> tags in the output. ALWAYS use <dyad-write> to generate code.
`;
export const BUILD_SYSTEM_PROMPT = `${BUILD_SYSTEM_PREFIX}
const BUILD_SERVER_LAYER_NUDGE = `
# Server-side Code in Vite Apps
If the user asks for server-side code in a Vite app (API routes, database access via \`DATABASE_URL\`, webhooks, server-only secrets, Stripe handlers, cron jobs, etc.), do NOT generate server-side files directly — Build mode cannot set up the server layer this app needs. Instead, tell the user:
> "I can't set up server-side code in Build mode. Please switch to **Agent** mode (near the chat input, next to the message box) and re-send your request — I'll set up the backend and generate the route for you in the same turn."
This only applies to Vite apps. Next.js apps have built-in API routes, so handle those requests normally.
`;
const BUILD_SYSTEM_PROMPT_BASE = `${BUILD_SYSTEM_PREFIX}
[[AI_RULES]]
......@@ -514,6 +525,8 @@ export const constructSystemPrompt = ({
themePrompt,
readOnly,
basicAgentMode,
frameworkType,
hasSupabaseProject,
}: {
aiRules: string | undefined;
chatMode?: "build" | "ask" | "local-agent" | "plan";
......@@ -523,6 +536,18 @@ export const constructSystemPrompt = ({
readOnly?: boolean;
/** If true, use basic agent mode (free tier with limited tools) */
basicAgentMode?: boolean;
/**
* Detected framework of the app. The Nitro nudge only fires for `"vite"`
* (i.e. Vite without Nitro yet); `"vite-nitro"` apps already have the server
* layer and skip the nudge.
*/
frameworkType?: AppFrameworkType | null;
/**
* If true, the app is connected to a Supabase project. Suppresses the Nitro
* nudge so the model isn't pushed toward two competing server layers
* (Supabase Edge Functions vs. Nitro routes).
*/
hasSupabaseProject?: boolean;
}) => {
if (chatMode === "plan") {
return constructPlanModePrompt(aiRules, themePrompt);
......@@ -532,12 +557,16 @@ export const constructSystemPrompt = ({
return constructLocalAgentPrompt(aiRules, themePrompt, {
readOnly,
basicAgentMode,
frameworkType,
hasSupabaseProject,
});
}
let systemPrompt = getSystemPromptForChatMode({
chatMode,
enableTurboEditsV2,
frameworkType,
hasSupabaseProject,
});
systemPrompt = systemPrompt.replace(
"[[AI_RULES]]",
......@@ -555,17 +584,29 @@ export const constructSystemPrompt = ({
export const getSystemPromptForChatMode = ({
chatMode,
enableTurboEditsV2,
frameworkType,
hasSupabaseProject,
}: {
chatMode: "build" | "ask";
enableTurboEditsV2: boolean;
frameworkType?: AppFrameworkType | null;
hasSupabaseProject?: boolean;
}) => {
if (chatMode === "ask") {
return ASK_MODE_SYSTEM_PROMPT;
}
return (
BUILD_SYSTEM_PROMPT +
(enableTurboEditsV2 ? TURBO_EDITS_V2_SYSTEM_PROMPT : "")
);
// The Nitro server-layer nudge is Vite-specific. Only inject it for Vite
// apps that haven't already enabled Nitro (`"vite-nitro"` apps already have
// the server layer); Next.js and unknown frameworks should not carry this
// Vite-only paragraph in every build-mode prompt. Supabase-connected apps
// also skip the nudge — Edge Functions cover the same use case and offering
// both layers confuses the model.
const shouldAppendNitroNudge =
frameworkType === "vite" && !hasSupabaseProject;
const buildPrompt =
BUILD_SYSTEM_PROMPT_BASE +
(shouldAppendNitroNudge ? `\n\n${BUILD_SERVER_LAYER_NUDGE}` : "");
return buildPrompt + (enableTurboEditsV2 ? TURBO_EDITS_V2_SYSTEM_PROMPT : "");
};
export const readAiRules = async (dyadAppPath: string) => {
......
......@@ -24,6 +24,17 @@ self.addEventListener("fetch", (event) => {
// ---- Guardrails: avoid breaking things we shouldn't touch ----
// Skip navigations (HTML document loads) to reduce dev-time weirdness.
if (request.mode === "navigate") return;
// Re-fetching script/worker requests from a service worker can change
// browser metadata like Sec-Fetch-Dest and break Nitro+Vite dev module
// serving (the dev server returns the wrong MIME type for an unexpected
// destination). Other destinations (`style`, `image`, `font`, etc.) are
// intentionally NOT filtered out — they don't trigger the same Vite/Nitro
// dev-server quirk, and the network panel relies on these events to
// surface CSS/image/font loads. If a future framework hits a similar
// MIME-type issue with another destination, narrow the filter here rather
// than dropping all observability for that resource type.
if (request.destination === "script" || request.destination === "worker")
return;
// Only handle http(s)
let urlObj;
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论