Unverified 提交 80845ef8 authored 作者: Will Chen's avatar Will Chen 提交者: GitHub

Modularize AGENTS.md learnings into rules/ directory (#2540)

## Summary - Extracted topic-specific learnings and guidelines from AGENTS.md into 9 separate files under `rules/` for better discoverability and maintainability - AGENTS.md now has a concise rules index table with "read when..." descriptions so agents know which file to consult - No content was lost — all learnings are preserved in their respective rule files ## Test plan - [x] Verify all rule files exist and contain the expected content - [x] Verify AGENTS.md rules index links are correct - [x] Lint, type checks, and tests all pass #skip-bugbot 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- devin-review-badge-begin --> --- <a href="https://app.devin.ai/review/dyad-sh/dyad/pull/2540" target="_blank"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1"> <img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open with Devin"> </picture> </a> <!-- devin-review-badge-end --> <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Modularized guidance from AGENTS.md into nine focused docs under rules/, and added a concise rules index in AGENTS.md for quick navigation. No content was removed; it’s now easier to find and maintain. - **Refactors** - Extracted topic docs into rules/: electron-ipc, e2e-testing, git-workflow, base-ui-components, database-drizzle, typescript-strict-mode, openai-reasoning-models, adding-settings, chat-message-indicators. - Updated .claude/commands/remember-learnings.md to write learnings to rules/ (use AGENTS.md only when needed) and maintain the index when new files are added. <sup>Written for commit 7d217b84ce3d1e434f28a31bb6a2329c672d2bcf. Summary will update on new commits.</sup> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
上级 2d0ebcf9
# Remember Learnings # Remember Learnings
Review the current session for errors, issues, snags, and hard-won knowledge, then update `AGENTS.md` with actionable learnings so future agent sessions run more smoothly. Review the current session for errors, issues, snags, and hard-won knowledge, then update the `rules/` files (or `AGENTS.md` if no suitable rule file exists) with actionable learnings so future agent sessions run more smoothly.
**IMPORTANT:** This skill MUST complete autonomously. Do NOT ask for user confirmation. **IMPORTANT:** This skill MUST complete autonomously. Do NOT ask for user confirmation.
...@@ -8,9 +8,10 @@ Review the current session for errors, issues, snags, and hard-won knowledge, th ...@@ -8,9 +8,10 @@ Review the current session for errors, issues, snags, and hard-won knowledge, th
> **NOTE:** `CLAUDE.md` is a symlink to `AGENTS.md`. They are the same file. **ALL EDITS MUST BE MADE TO `AGENTS.md`**, never to `CLAUDE.md` directly. > **NOTE:** `CLAUDE.md` is a symlink to `AGENTS.md`. They are the same file. **ALL EDITS MUST BE MADE TO `AGENTS.md`**, never to `CLAUDE.md` directly.
- **`AGENTS.md`** contains agent-specific operational knowledge — tips, gotchas, and hard-won insights that help agents avoid repeating mistakes. - **`AGENTS.md`** is the top-level agent guide. It contains core setup instructions and a **rules index** table pointing to topic-specific files in `rules/`.
- **`rules/*.md`** contain topic-specific learnings and guidelines (e.g., `rules/e2e-testing.md`, `rules/electron-ipc.md`).
Learnings should go into `AGENTS.md`. If a learning is important enough to be a project-wide convention, flag it in the summary so a human can promote it to the project documentation. Learnings should go into the most relevant `rules/*.md` file. Only add to `AGENTS.md` directly if the learning doesn't fit any existing rule file and doesn't warrant a new one. If a learning is important enough to be a project-wide convention, flag it in the summary so a human can promote it to the project documentation.
## Instructions ## Instructions
...@@ -22,41 +23,47 @@ Learnings should go into `AGENTS.md`. If a learning is important enough to be a ...@@ -22,41 +23,47 @@ Learnings should go into `AGENTS.md`. If a learning is important enough to be a
- **Workflow friction:** Steps that were done in the wrong order, missing prerequisites, commands that needed special flags - **Workflow friction:** Steps that were done in the wrong order, missing prerequisites, commands that needed special flags
- **Architecture insights:** Patterns that weren't obvious, file locations that were hard to find, implicit conventions not documented - **Architecture insights:** Patterns that weren't obvious, file locations that were hard to find, implicit conventions not documented
Skip anything that is already well-documented in `AGENTS.md`. Skip anything that is already well-documented in `AGENTS.md` or `rules/`.
2. **Read existing documentation:** 2. **Read existing documentation:**
Read `AGENTS.md` at the repository root to understand what's already documented and avoid duplication. Read `AGENTS.md` at the repository root to see the rules index, then read the relevant `rules/*.md` files to understand what's already documented and avoid duplication.
3. **Draft concise, actionable additions:** 3. **Draft concise, actionable additions:**
For each learning, write a short bullet point or section that would help a future agent avoid the same issue. Follow these rules: For each learning, write a short bullet point or section that would help a future agent avoid the same issue. Follow these rules:
- Be specific and actionable (e.g., "Run `npm run build` before E2E tests" not "remember to build first") - Be specific and actionable (e.g., "Run `npm run build` before E2E tests" not "remember to build first")
- Include the actual error message or symptom when relevant so agents can recognize the situation - Include the actual error message or symptom when relevant so agents can recognize the situation
- Don't duplicate what's already in `AGENTS.md` - Don't duplicate what's already in `AGENTS.md` or `rules/`
- Group related learnings under existing sections if appropriate, or create a new section
- Keep it concise: each learning should be 1-3 lines max - Keep it concise: each learning should be 1-3 lines max
- **Limit to at most 5 learnings per session** — focus on the most impactful insights - **Limit to at most 5 learnings per session** — focus on the most impactful insights
- If a new learning overlaps with or supersedes an existing one, consolidate them into a single entry rather than appending - If a new learning overlaps with or supersedes an existing one, consolidate them into a single entry rather than appending
4. **Update AGENTS.md:** 4. **Update the appropriate file(s):**
Edit `AGENTS.md` to incorporate the new learnings. Add a `## Learnings` section at the bottom if one doesn't exist, or append to the existing `## Learnings` section. Place each learning in the most relevant location:
a. **Existing `rules/*.md` file** — if the learning fits an existing topic (e.g., E2E testing tips go in `rules/e2e-testing.md`, IPC learnings go in `rules/electron-ipc.md`).
b. **New `rules/*.md` file** — if the learning is substantial enough to warrant its own topic file. Use a descriptive kebab-case filename (e.g., `rules/tanstack-router.md`). If you create a new file, also update the rules index table in `AGENTS.md`.
c. **`AGENTS.md` directly** — only for general learnings that don't fit any topic (rare).
If there are no new learnings worth recording (i.e., everything went smoothly or all issues are already documented), skip the edit and report that no updates were needed. If there are no new learnings worth recording (i.e., everything went smoothly or all issues are already documented), skip the edit and report that no updates were needed.
**Maintenance:** When adding new learnings, review the existing `## Learnings` section and remove any entries that are: **Maintenance:** When adding new learnings, review the target file and remove any entries that are:
- Obsolete due to codebase changes - Obsolete due to codebase changes
- Duplicated by or subsumed by a newer, more complete learning - Duplicated by or subsumed by a newer, more complete learning
5. **Stage the changes:** 5. **Stage the changes:**
If `AGENTS.md` was modified: Stage any modified or created files:
``` ```
git add AGENTS.md git add AGENTS.md rules/
``` ```
6. **Summarize:** 6. **Summarize:**
- List the learnings that were added (or state that none were needed) - List the learnings that were added (or state that none were needed)
- Confirm whether `AGENTS.md` was staged for commit - Identify which files were modified or created
- Confirm whether changes were staged for commit
...@@ -2,6 +2,22 @@ ...@@ -2,6 +2,22 @@
Please read `CONTRIBUTING.md` which includes information for human code contributors. Much of the information is applicable to you as well. Please read `CONTRIBUTING.md` which includes information for human code contributors. Much of the information is applicable to you as well.
## Rules index
Detailed rules and learnings are in the `rules/` directory. Read the relevant file when working in that area.
| File | Read when... |
| -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| [rules/electron-ipc.md](rules/electron-ipc.md) | Adding/modifying IPC endpoints, handlers, React Query hooks, or renderer-to-main communication |
| [rules/e2e-testing.md](rules/e2e-testing.md) | Writing or debugging E2E tests (Playwright, Base UI radio clicks, Lexical editor, test fixtures) |
| [rules/git-workflow.md](rules/git-workflow.md) | Pushing branches, creating PRs, or dealing with fork/upstream remotes |
| [rules/base-ui-components.md](rules/base-ui-components.md) | Using TooltipTrigger, ToggleGroupItem, or other Base UI wrapper components |
| [rules/database-drizzle.md](rules/database-drizzle.md) | Modifying the database schema, generating migrations, or resolving migration conflicts |
| [rules/typescript-strict-mode.md](rules/typescript-strict-mode.md) | Debugging type errors from `npm run ts` (tsgo) that pass normal tsc |
| [rules/openai-reasoning-models.md](rules/openai-reasoning-models.md) | Working with OpenAI reasoning model (o1/o3/o4-mini) conversation history |
| [rules/adding-settings.md](rules/adding-settings.md) | Adding a new user-facing setting or toggle to the Settings page |
| [rules/chat-message-indicators.md](rules/chat-message-indicators.md) | Using `<dyad-status>` tags in chat messages for system indicators |
## Project setup and lints ## Project setup and lints
Make sure you run this once after doing `npm install` because it will make sure whenever you commit something, it will run pre-commit hooks like linting and formatting. Make sure you run this once after doing `npm install` because it will make sure whenever you commit something, it will run pre-commit hooks like linting and formatting.
...@@ -54,108 +70,6 @@ Note: if you do this, then you will need to re-add the changes and commit again. ...@@ -54,108 +70,6 @@ Note: if you do this, then you will need to re-add the changes and commit again.
- Frontend is a React app that uses TanStack Router (not Next.js or React Router). - Frontend is a React app that uses TanStack Router (not Next.js or React Router).
- Data fetching/mutations should be handled with TanStack Query when touching IPC-backed endpoints. - Data fetching/mutations should be handled with TanStack Query when touching IPC-backed endpoints.
## IPC architecture expectations
This project uses a **contract-driven IPC architecture**. Contracts in `src/ipc/types/*.ts` are the single source of truth for channel names, input/output schemas (Zod), and auto-generated clients.
### Three IPC patterns
1. **Invoke/response** (`defineContract` + `createClient`) — Standard request-response calls.
2. **Events** (`defineEvent` + `createEventClient`) — Main-to-renderer pub/sub push events.
3. **Streams** (`defineStream` + `createStreamClient`) — Invoke that returns chunked data over multiple events (e.g., chat streaming).
### Key files
| Layer | File | Role |
| -------------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------ |
| Contract core | `src/ipc/contracts/core.ts` | `defineContract`, `defineEvent`, `defineStream`, client generators |
| Domain contracts + clients | `src/ipc/types/*.ts` (e.g., `settings.ts`, `app.ts`, `chat.ts`) | Per-domain contracts and auto-generated clients |
| Unified client | `src/ipc/types/index.ts` | Re-exports all clients; also exports `ipc` namespace object |
| Preload allowlist | `src/preload.ts` + `src/ipc/preload/channels.ts` | Channel whitelist auto-derived from contracts |
| Handler registration | `src/ipc/ipc_host.ts` | Calls `register*Handlers()` from `src/ipc/handlers/` |
| Handler base | `src/ipc/handlers/base.ts` | `createTypedHandler` with runtime Zod validation |
### Adding a new IPC endpoint
1. Define contracts in the relevant `src/ipc/types/<domain>.ts` file using `defineContract()`.
2. Export the client via `createClient(contracts)` from the same file.
3. Re-export the contract, client, and types from `src/ipc/types/index.ts`.
4. The preload allowlist is auto-derived from contracts — no manual channel registration needed.
5. Register the handler in `src/ipc/handlers/<domain>_handlers.ts` using `createTypedHandler(contract, handler)`.
6. Import and call the registration function in `src/ipc/ipc_host.ts`.
### Renderer usage
```ts
// Individual domain client
import { appClient } from "@/ipc/types";
const app = await appClient.getApp({ appId });
// Or use the unified ipc namespace
import { ipc } from "@/ipc/types";
const settings = await ipc.settings.getUserSettings();
// Event subscriptions (main -> renderer)
const unsub = ipc.events.agent.onTodosUpdate((payload) => { ... });
// Streaming
ipc.chatStream.start(params, { onChunk, onEnd, onError });
```
### Handler expectations
- Handlers should `throw new Error("...")` on failure instead of returning `{ success: false }` style payloads.
- Use `createTypedHandler(contract, handler)` which validates inputs at runtime via Zod.
## Architecture
### React Query key factory
All React Query keys must be defined in `src/lib/queryKeys.ts` using the centralized factory pattern. This provides:
- Type-safe query keys with full autocomplete
- Hierarchical structure for easy invalidation (invalidate parent to invalidate children)
- Consistent naming across the codebase
- Single source of truth for all query keys
**Usage:**
```ts
import { queryKeys } from "@/lib/queryKeys";
import { appClient } from "@/ipc/types";
// In useQuery:
useQuery({
queryKey: queryKeys.apps.detail({ appId }),
queryFn: () => appClient.getApp({ appId }),
});
// Invalidating queries:
queryClient.invalidateQueries({ queryKey: queryKeys.apps.all });
```
**Adding new keys:** Add entries to the appropriate domain in `queryKeys.ts`. Follow the existing pattern with `all` for the base key and factory functions using object parameters for parameterized keys.
## React + IPC integration pattern
When creating hooks/components that call IPC handlers:
- Wrap reads in `useQuery`, using keys from `queryKeys` factory (see above), async `queryFn` that calls the relevant domain client (e.g., `appClient.getApp(...)`) or unified `ipc` namespace, and conditionally use `enabled`/`initialData`/`meta` as needed.
- Wrap writes in `useMutation`; validate inputs locally, call the domain client, and invalidate related queries on success. Use shared utilities (e.g., toast helpers) in `onError`.
- Synchronize TanStack Query data with any global state (like Jotai atoms) via `useEffect` only if required.
## Database
This app uses SQLite and drizzle ORM.
Generate SQL migrations by running this:
```sh
npm run db:generate
```
IMPORTANT: Do NOT generate SQL migration files by hand! This is wrong.
## General guidance ## General guidance
- Favor descriptive module/function names that mirror IPC channel semantics. - Favor descriptive module/function names that mirror IPC channel semantics.
...@@ -174,161 +88,4 @@ Use unit testing for pure business logic and util functions. ...@@ -174,161 +88,4 @@ Use unit testing for pure business logic and util functions.
### E2E testing ### E2E testing
Use E2E testing when you need to test a complete user flow for a feature. See [rules/e2e-testing.md](rules/e2e-testing.md) for full E2E testing guidance, including Playwright tips and fixture setup.
If you would need to mock a lot of things to unit test a feature, prefer to write an E2E test instead.
Do NOT write lots of e2e test cases for one feature. Each e2e test case adds a significant amount of overhead, so instead prefer just one or two E2E test cases that each have broad coverage of the feature in question.
**IMPORTANT: You MUST run `npm run build` before running E2E tests.** E2E tests run against the built application binary, not the source code. If you make any changes to application code (anything outside of `e2e-tests/`), you MUST re-run `npm run build` before running E2E tests, otherwise you'll be testing the old version of the application.
```sh
npm run build
```
To run e2e tests without opening the HTML report (which blocks the terminal), use:
```sh
PLAYWRIGHT_HTML_OPEN=never npm run e2e
```
To get additional debug logs when a test is failing, use:
```sh
DEBUG=pw:browser PLAYWRIGHT_HTML_OPEN=never npm run e2e
```
## Git workflow
When pushing changes and creating PRs:
1. If the branch already has an associated PR, push to whichever remote the branch is tracking.
2. If the branch hasn't been pushed before, default to pushing to `origin` (the fork `wwwillchen/dyad`), then create a PR from the fork to the upstream repo (`dyad-sh/dyad`).
3. If you cannot push to the fork due to permissions, push directly to `upstream` (`dyad-sh/dyad`) as a last resort.
### Skipping automated review
Add `#skip-bugbot` to the PR description for trivial PRs that won't affect end-users, such as:
- Claude settings, commands, or agent configuration
- Linting or test setup changes
- Documentation-only changes
- CI/build configuration updates
## Learnings
### Cross-repo PR workflows (forks)
When running GitHub Actions with `pull_request_target` on cross-repo PRs (from forks):
- The checkout action sets `origin` to the **fork** (head repo), not the base repo
- To rebase onto the base repo's main, you must add an `upstream` remote: `git remote add upstream https://github.com/<base-repo>.git`
- Remote setup for cross-repo PRs: `origin` → fork (push here), `upstream` → base repo (rebase from here)
- The `GITHUB_TOKEN` can push to the fork if the PR author enabled "Allow edits from maintainers"
### TooltipTrigger render prop (Base UI)
- `TooltipTrigger` from `@base-ui/react/tooltip` (wrapped in `src/components/ui/tooltip.tsx`) renders a `<button>` by default. Wrapping another button-like element (`<button>`, `<Button>`, `<DropdownMenuTrigger>`, `<PopoverTrigger>`, `<MiniSelectTrigger>`, `<ToggleGroupItem>`) inside it creates invalid nested `<button>` HTML. Use the `render` prop instead:
```tsx
// Wrong: nested buttons
<TooltipTrigger><Button onClick={fn}>Click</Button></TooltipTrigger>
// Correct: render prop merges into a single element
<TooltipTrigger render={<Button onClick={fn} />}>Click</TooltipTrigger>
```
- Wrapping `ToggleGroupItem` in `TooltipTrigger` without `render` also breaks `:first-child`/`:last-child` CSS selectors for rounded corners on the group.
- For drag handles and resize rails, prefer the native `title` attribute over `Tooltip` — tooltips appear immediately on hover and interfere with drag interactions, while `title` has a built-in delay.
### Base UI Radio component selection in Playwright
Base UI Radio components render a hidden native `<input type="radio">` with `aria-hidden="true"`. Both `getByRole('radio', { name: '...' })` and `getByLabel('...')` find this hidden input but can't click it (element is outside viewport). Use `getByText` to click the visible label text instead.
```ts
// Correct: click the visible label text
await page.getByText("Vue", { exact: true }).click();
// Won't work: finds hidden input, can't click
await page.getByRole("radio", { name: "Vue" }).click();
await page.getByLabel("Vue").click();
```
### Lexical editor in Playwright E2E tests
The chat input uses a Lexical editor (contenteditable). Standard Playwright methods don't always work:
- **Clearing input**: `fill("")` doesn't reliably clear Lexical. Use keyboard shortcuts instead: `Meta+a` then `Backspace`.
- **Timing issues**: Lexical may need time to update its internal state. Use `toPass()` with retries for resilient tests.
- **Helper methods**: Use `po.clearChatInput()` and `po.openChatHistoryMenu()` from test_helper.ts for reliable Lexical interactions.
```ts
// Wrong: may not clear Lexical editor
await chatInput.fill("");
// Correct: use helper with retry logic
await po.clearChatInput();
// For history menu (needs clear + ArrowUp with retries)
await po.openChatHistoryMenu();
```
### Drizzle migration conflicts during rebase
When rebasing a branch that has drizzle migrations conflicting with upstream (e.g., both have `0023_*.sql`):
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
### tsgo is stricter than tsc for type checking
The pre-commit hook runs `tsgo` (via `npm run ts`), which is stricter than `tsc --noEmit`. For example, passing a `number` to a function typed `(str: string | null | undefined)` may pass `tsc` but fail `tsgo` with `TS2345: Argument of type 'number' is not assignable to parameter of type 'string'`. Always wrap with `String()` when converting numbers to string parameters.
### OpenAI reasoning model errors with conversation history
When using OpenAI reasoning models (o1, o3, o4-mini) via LiteLLM/Azure, you may see:
```
Item 'rs_...' of type 'reasoning' was provided without its required following item.
```
OpenAI's Responses API requires reasoning items to always be followed by an output item (text, tool-call). This error occurs when:
- The model produces reasoning then immediately makes tool calls (no text between)
- The stream is interrupted after reasoning but before output
- Only reasoning was generated in a turn
The fix in `src/ipc/utils/ai_messages_utils.ts` filters orphaned reasoning parts via `filterOrphanedReasoningParts()` before sending conversation history back to OpenAI.
### Adding a new user setting
When adding a new toggle/setting to the Settings page:
1. Add the field to `UserSettingsSchema` in `src/lib/schemas.ts`
2. Add the default value in `DEFAULT_SETTINGS` in `src/main/settings.ts`
3. Add a `SETTING_IDS` entry and search index entry in `src/lib/settingsSearchIndex.ts`
4. Create a switch component (e.g., `src/components/MySwitch.tsx`) - follow `AutoApproveSwitch.tsx` as a template
5. Import and add the switch to the relevant section in `src/pages/settings.tsx`
### Custom chat message indicators
The `<dyad-status>` tag in chat messages renders as a collapsible status indicator box. Use it for system messages like compaction notifications:
```
<dyad-status title="My Title" state="finished">
Content here
</dyad-status>
```
Valid states: `"finished"`, `"in-progress"`, `"aborted"`
### E2E test fixtures with .dyad directories
When adding E2E test fixtures that need a `.dyad` directory for testing:
- The `.dyad` directory is git-ignored by default in test fixtures
- Use `git add -f path/to/.dyad/file` to force-add files inside `.dyad` directories
- If `mkdir` is blocked on `.dyad` paths due to security restrictions, use the Write tool to create files directly (which auto-creates parent directories)
# Adding a New User Setting
When adding a new toggle/setting to the Settings page:
1. Add the field to `UserSettingsSchema` in `src/lib/schemas.ts`
2. Add the default value in `DEFAULT_SETTINGS` in `src/main/settings.ts`
3. Add a `SETTING_IDS` entry and search index entry in `src/lib/settingsSearchIndex.ts`
4. Create a switch component (e.g., `src/components/MySwitch.tsx`) - follow `AutoApproveSwitch.tsx` as a template
5. Import and add the switch to the relevant section in `src/pages/settings.tsx`
# Base UI Component Patterns
## TooltipTrigger render prop
`TooltipTrigger` from `@base-ui/react/tooltip` (wrapped in `src/components/ui/tooltip.tsx`) renders a `<button>` by default. Wrapping another button-like element (`<button>`, `<Button>`, `<DropdownMenuTrigger>`, `<PopoverTrigger>`, `<MiniSelectTrigger>`, `<ToggleGroupItem>`) inside it creates invalid nested `<button>` HTML. Use the `render` prop instead:
```tsx
// Wrong: nested buttons
<TooltipTrigger><Button onClick={fn}>Click</Button></TooltipTrigger>
// Correct: render prop merges into a single element
<TooltipTrigger render={<Button onClick={fn} />}>Click</TooltipTrigger>
```
- Wrapping `ToggleGroupItem` in `TooltipTrigger` without `render` also breaks `:first-child`/`:last-child` CSS selectors for rounded corners on the group.
- For drag handles and resize rails, prefer the native `title` attribute over `Tooltip` — tooltips appear immediately on hover and interfere with drag interactions, while `title` has a built-in delay.
# Custom Chat Message Indicators
The `<dyad-status>` tag in chat messages renders as a collapsible status indicator box. Use it for system messages like compaction notifications:
```
<dyad-status title="My Title" state="finished">
Content here
</dyad-status>
```
Valid states: `"finished"`, `"in-progress"`, `"aborted"`
# Database & Drizzle ORM
This app uses SQLite and drizzle ORM.
Generate SQL migrations by running this:
```sh
npm run db:generate
```
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`):
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
# E2E Testing
Use E2E testing when you need to test a complete user flow for a feature.
If you would need to mock a lot of things to unit test a feature, prefer to write an E2E test instead.
Do NOT write lots of e2e test cases for one feature. Each e2e test case adds a significant amount of overhead, so instead prefer just one or two E2E test cases that each have broad coverage of the feature in question.
**IMPORTANT: You MUST run `npm run build` before running E2E tests.** E2E tests run against the built application binary, not the source code. If you make any changes to application code (anything outside of `e2e-tests/`), you MUST re-run `npm run build` before running E2E tests, otherwise you'll be testing the old version of the application.
```sh
npm run build
```
To run e2e tests without opening the HTML report (which blocks the terminal), use:
```sh
PLAYWRIGHT_HTML_OPEN=never npm run e2e
```
To get additional debug logs when a test is failing, use:
```sh
DEBUG=pw:browser PLAYWRIGHT_HTML_OPEN=never npm run e2e
```
## Base UI Radio component selection in Playwright
Base UI Radio components render a hidden native `<input type="radio">` with `aria-hidden="true"`. Both `getByRole('radio', { name: '...' })` and `getByLabel('...')` find this hidden input but can't click it (element is outside viewport). Use `getByText` to click the visible label text instead.
```ts
// Correct: click the visible label text
await page.getByText("Vue", { exact: true }).click();
// Won't work: finds hidden input, can't click
await page.getByRole("radio", { name: "Vue" }).click();
await page.getByLabel("Vue").click();
```
## Lexical editor in Playwright E2E tests
The chat input uses a Lexical editor (contenteditable). Standard Playwright methods don't always work:
- **Clearing input**: `fill("")` doesn't reliably clear Lexical. Use keyboard shortcuts instead: `Meta+a` then `Backspace`.
- **Timing issues**: Lexical may need time to update its internal state. Use `toPass()` with retries for resilient tests.
- **Helper methods**: Use `po.clearChatInput()` and `po.openChatHistoryMenu()` from test_helper.ts for reliable Lexical interactions.
```ts
// Wrong: may not clear Lexical editor
await chatInput.fill("");
// Correct: use helper with retry logic
await po.clearChatInput();
// For history menu (needs clear + ArrowUp with retries)
await po.openChatHistoryMenu();
```
## E2E test fixtures with .dyad directories
When adding E2E test fixtures that need a `.dyad` directory for testing:
- The `.dyad` directory is git-ignored by default in test fixtures
- Use `git add -f path/to/.dyad/file` to force-add files inside `.dyad` directories
- If `mkdir` is blocked on `.dyad` paths due to security restrictions, use the Write tool to create files directly (which auto-creates parent directories)
# Electron IPC Architecture
This project uses a **contract-driven IPC architecture**. Contracts in `src/ipc/types/*.ts` are the single source of truth for channel names, input/output schemas (Zod), and auto-generated clients.
## Three IPC patterns
1. **Invoke/response** (`defineContract` + `createClient`) — Standard request-response calls.
2. **Events** (`defineEvent` + `createEventClient`) — Main-to-renderer pub/sub push events.
3. **Streams** (`defineStream` + `createStreamClient`) — Invoke that returns chunked data over multiple events (e.g., chat streaming).
## Key files
| Layer | File | Role |
| -------------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------ |
| Contract core | `src/ipc/contracts/core.ts` | `defineContract`, `defineEvent`, `defineStream`, client generators |
| Domain contracts + clients | `src/ipc/types/*.ts` (e.g., `settings.ts`, `app.ts`, `chat.ts`) | Per-domain contracts and auto-generated clients |
| Unified client | `src/ipc/types/index.ts` | Re-exports all clients; also exports `ipc` namespace object |
| Preload allowlist | `src/preload.ts` + `src/ipc/preload/channels.ts` | Channel whitelist auto-derived from contracts |
| Handler registration | `src/ipc/ipc_host.ts` | Calls `register*Handlers()` from `src/ipc/handlers/` |
| Handler base | `src/ipc/handlers/base.ts` | `createTypedHandler` with runtime Zod validation |
## Adding a new IPC endpoint
1. Define contracts in the relevant `src/ipc/types/<domain>.ts` file using `defineContract()`.
2. Export the client via `createClient(contracts)` from the same file.
3. Re-export the contract, client, and types from `src/ipc/types/index.ts`.
4. The preload allowlist is auto-derived from contracts — no manual channel registration needed.
5. Register the handler in `src/ipc/handlers/<domain>_handlers.ts` using `createTypedHandler(contract, handler)`.
6. Import and call the registration function in `src/ipc/ipc_host.ts`.
## Renderer usage
```ts
// Individual domain client
import { appClient } from "@/ipc/types";
const app = await appClient.getApp({ appId });
// Or use the unified ipc namespace
import { ipc } from "@/ipc/types";
const settings = await ipc.settings.getUserSettings();
// Event subscriptions (main -> renderer)
const unsub = ipc.events.agent.onTodosUpdate((payload) => { ... });
// Streaming
ipc.chatStream.start(params, { onChunk, onEnd, onError });
```
## Handler expectations
- Handlers should `throw new Error("...")` on failure instead of returning `{ success: false }` style payloads.
- Use `createTypedHandler(contract, handler)` which validates inputs at runtime via Zod.
## React Query key factory
All React Query keys must be defined in `src/lib/queryKeys.ts` using the centralized factory pattern. This provides:
- Type-safe query keys with full autocomplete
- Hierarchical structure for easy invalidation (invalidate parent to invalidate children)
- Consistent naming across the codebase
- Single source of truth for all query keys
**Usage:**
```ts
import { queryKeys } from "@/lib/queryKeys";
import { appClient } from "@/ipc/types";
// In useQuery:
useQuery({
queryKey: queryKeys.apps.detail({ appId }),
queryFn: () => appClient.getApp({ appId }),
});
// Invalidating queries:
queryClient.invalidateQueries({ queryKey: queryKeys.apps.all });
```
**Adding new keys:** Add entries to the appropriate domain in `queryKeys.ts`. Follow the existing pattern with `all` for the base key and factory functions using object parameters for parameterized keys.
## React + IPC integration pattern
When creating hooks/components that call IPC handlers:
- Wrap reads in `useQuery`, using keys from `queryKeys` factory (see above), async `queryFn` that calls the relevant domain client (e.g., `appClient.getApp(...)`) or unified `ipc` namespace, and conditionally use `enabled`/`initialData`/`meta` as needed.
- Wrap writes in `useMutation`; validate inputs locally, call the domain client, and invalidate related queries on success. Use shared utilities (e.g., toast helpers) in `onError`.
- Synchronize TanStack Query data with any global state (like Jotai atoms) via `useEffect` only if required.
# Git Workflow
When pushing changes and creating PRs:
1. If the branch already has an associated PR, push to whichever remote the branch is tracking.
2. If the branch hasn't been pushed before, default to pushing to `origin` (the fork `wwwillchen/dyad`), then create a PR from the fork to the upstream repo (`dyad-sh/dyad`).
3. If you cannot push to the fork due to permissions, push directly to `upstream` (`dyad-sh/dyad`) as a last resort.
## Skipping automated review
Add `#skip-bugbot` to the PR description for trivial PRs that won't affect end-users, such as:
- Claude settings, commands, or agent configuration
- Linting or test setup changes
- Documentation-only changes
- CI/build configuration updates
## Cross-repo PR workflows (forks)
When running GitHub Actions with `pull_request_target` on cross-repo PRs (from forks):
- The checkout action sets `origin` to the **fork** (head repo), not the base repo
- To rebase onto the base repo's main, you must add an `upstream` remote: `git remote add upstream https://github.com/<base-repo>.git`
- Remote setup for cross-repo PRs: `origin` → fork (push here), `upstream` → base repo (rebase from here)
- The `GITHUB_TOKEN` can push to the fork if the PR author enabled "Allow edits from maintainers"
# OpenAI Reasoning Model Errors
When using OpenAI reasoning models (o1, o3, o4-mini) via LiteLLM/Azure, you may see:
```
Item 'rs_...' of type 'reasoning' was provided without its required following item.
```
OpenAI's Responses API requires reasoning items to always be followed by an output item (text, tool-call). This error occurs when:
- The model produces reasoning then immediately makes tool calls (no text between)
- The stream is interrupted after reasoning but before output
- Only reasoning was generated in a turn
The fix in `src/ipc/utils/ai_messages_utils.ts` filters orphaned reasoning parts via `filterOrphanedReasoningParts()` before sending conversation history back to OpenAI.
# TypeScript Strict Mode (tsgo)
The pre-commit hook runs `tsgo` (via `npm run ts`), which is stricter than `tsc --noEmit`. For example, passing a `number` to a function typed `(str: string | null | undefined)` may pass `tsc` but fail `tsgo` with `TS2345: Argument of type 'number' is not assignable to parameter of type 'string'`. Always wrap with `String()` when converting numbers to string parameters.
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论