diff --git a/CLAUDE.md b/CLAUDE.md index 06e3f4f..c97387c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,405 +4,199 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -Thinkix is an infinite canvas whiteboard application built with Next.js 16, React 19, and the Plait board library. It provides a collaborative thinking space with support for mind maps, freehand drawing, shapes, text, and images. The project uses Bun workspaces for shared code. +Thinkix is an AI-native infinite canvas whiteboard built with Next.js 16, React 19, and the Plait board library. The AI agent can read the current board, create new structures, and edit existing elements via a Unix-shell-style command set and a custom DSL. The project uses Bun workspaces for shared code and Liveblocks + Yjs for live collaboration. ## Development Commands ```bash -bun dev # Start development server (Turbopack, http://localhost:3000) -bun run build # Build for production -bun start # Start production server -bun run lint # Run ESLint -bun run typecheck # Run TypeScript type checking -bun install # Install workspace dependencies -bun run build:parser # Build Peggy DSL parser - -# Testing -bun test # Run tests in watch mode -bun run test:run # Run tests once -bun run test:coverage # Run tests with coverage report +bun install # Install workspace dependencies (runs postinstall patches) +bun dev # Start dev server at http://localhost:3000 +bun run build # Production build (runs prebuild: patches + DSL parser) +bun start # Run production build +bun run lint # ESLint +bun run typecheck # tsc --noEmit --skipLibCheck +bun run build:parser # Regenerate features/agent/tools/dsl/parser.js + +# Unit / integration / component tests (Vitest) +bun run test # Vitest in watch mode +bun run test:run # Vitest once (CI mode) +bun run test:ui # Vitest UI +bun run test:coverage # Coverage + coverage-summary.ts + +# E2E (Playwright) +bun run test:e2e # Full run (builds and starts app on :3100) +bun run test:e2e:ui # Playwright UI +bun run test:e2e:debug # Debug mode +bun run test:all # test:run + test:e2e ``` -**Note:** This project uses Bun as the package manager and runtime. Install Bun from [bun.sh](https://bun.sh). +**IMPORTANT: Do NOT run `bun test`.** That invokes Bun's native test runner, which is not configured for this project. Always use `bun run test` / `bun run test:run`. See `tests/README.md`. -## Monorepo Structure - -The project uses Bun workspaces with the following structure: +### Single-test invocations -```text -thinkix/ -├── packages/ # Workspace packages -│ ├── ui/ # @thinkix/ui - Shared UI components (shadcn-based) -│ ├── ai/ # @thinkix/ai - AI SDK integration and utilities -│ ├── plait-utils/ # @thinkix/plait-utils - Plait board helpers -│ ├── storage/ # @thinkix/storage - IndexedDB storage -│ ├── shared/ # @thinkix/shared - Shared types -│ └── file-utils/ # @thinkix/file-utils - File I/O and board export -│ -├── features/ # Feature modules (board, toolbar, storage) -├── app/ # Next.js app router and API routes -├── shared/ # Shared constants with JSX (icons, tool configs) -└── tests/ # Test files (unit, integration, components) +```bash +bun run test:run -- tests/unit/serializer.test.ts # one Vitest file +bun run test:run -- -t "grep command" # by test name +bun run test:e2e -- tests/e2e/agent-pane.spec.ts # one Playwright spec +bun run test:e2e -- --grep "create sticky" ``` -### Workspace Packages +### Generated files and build hooks -**@thinkix/ui** (`packages/ui/`) -- Shared React components built on shadcn/ui patterns -- Exports: Button, Tooltip, Separator, Toggle, ToggleGroup, DropdownMenu, ImageViewer, Dialog, Input -- Utilities: `cn()` class merge function -- Import: `import { Button } from '@thinkix/ui';` +- `postinstall` and `prebuild` run `scripts/apply-patches.sh`, which patches `node_modules/@plait/draw` for the actually-installed `@plait/draw` version using the `.patch` files in `patches/`. Do not commit edits to `node_modules`; update the patch file instead. +- `prebuild` and `pretest` both run `build:parser`, which regenerates `features/agent/tools/dsl/parser.js` from `thinkix.peggy`. **Never hand-edit `parser.js`.** If you change `thinkix.peggy`, run `bun run build:parser` and commit the regenerated parser. -**@thinkix/ai** (`packages/ai/`) -- AI SDK v5 integration with multi-provider support (OpenAI, Anthropic) -- Exports: `createAIProvider()`, `resolveAIProvider()`, `resolveAIModel()`, `resolveAIKey()`, `getClientAIConfig()`, `toolSchemas` -- Types: `AIProvider`, `ProviderConfig`, `ClientAIConfig` -- Sub-exports: `@thinkix/ai/tool-schemas`, `@thinkix/ai/prompts` -- Import: `import { createAIProvider, resolveAIProvider, toolSchemas } from '@thinkix/ai';` - -**@thinkix/plait-utils** (`packages/plait-utils/`) -- Helper functions for Plait board operations -- Exports: `getSelectedElements()`, `getSelectedMindElements()`, `getCanvasContext()`, `findElementById()` -- Import: `import { getCanvasContext } from '@thinkix/plait-utils';` - -**@thinkix/storage** (`packages/storage/`) -- IndexedDB-based board storage with Dexie -- Exports: `useBoardStore`, types: `Board`, `BoardMetadata`, `SaveStatus` -- Import: `import { useBoardStore } from '@thinkix/storage';` +## Monorepo Structure -**@thinkix/shared** (`packages/shared/`) -- TypeScript types for the application -- Exports: `DrawingTool`, `ToolConfig`, `BoardState`, `BoardContextValue` -- Import: `import type { DrawingTool } from '@thinkix/shared';` +Bun workspaces declared under `packages/*`. Path aliases live in `tsconfig.json` and are mirrored in `vitest.config.mts`. -**@thinkix/file-utils** (`packages/file-utils/`) -- File system operations and board export utilities -- Exports: `fileOpen`, `fileSave`, `parseFileContents`, `download`, `base64ToBlob` -- Board exports: `saveBoardToFile`, `loadBoardFromFile`, `exportAsSvg`, `exportAsPng`, `exportAsJpg` -- Validation: `isValidThinkixData`, `sanitizeFileName` -- Types: `ThinkixExportedData`, `FileOpenOptions`, `FileSaveOptions` -- Import: `import { saveBoardToFile, exportAsPng } from '@thinkix/file-utils';` +```text +thinkix/ +├── app/ # Next.js app router (pages, API routes, styles) +│ ├── api/agent/ # LLM streaming endpoint + /config subroute +│ ├── api/collaboration/ # Liveblocks auth +│ └── page.tsx # Main board experience +├── features/ # App-level feature modules +│ ├── board/ # Canvas, plugins, grid, board utils +│ ├── agent/ # Agent pane, tools, DSL, command shell +│ ├── toolbar/ # Toolbars and inline formatting controls +│ ├── dialogs/ # MermaidToBoard, MarkdownToMindmap dialogs +│ ├── collaboration/ # Liveblocks room wrappers + collab UI +│ └── storage/ # Auto-save hook, BoardSwitcher +├── packages/ # Workspace packages (@thinkix/*) +├── shared/constants/ # App-level constants (JSX allowed) +├── tests/ # Vitest + Playwright tests +├── scripts/ # apply-patches.sh, coverage-summary.ts +├── patches/ # @plait/draw patches keyed by version +└── public/ +``` -### Shared Directory +### Workspace packages (`packages/*`) -**Why two locations for shared code?** +| Package | Import | Notes | +|--------|-----|-----| +| `@thinkix/ui` | `@thinkix/ui` | shadcn-style primitives (`Button`, `Tooltip`, `Dialog`, …) + `cn()` | +| `@thinkix/ai` | `@thinkix/ai`, `@thinkix/ai/tool-schemas`, `@thinkix/ai/prompts`, `@thinkix/ai/tools` | AI SDK v5 provider resolution, prompts, Zod tool schemas | +| `@thinkix/plait-utils` | `@thinkix/plait-utils` | Board helpers: `getCanvasContext`, `findElementById`, `parseMarkdownToMindElement` | +| `@thinkix/storage` | `@thinkix/storage` | Dexie/IndexedDB `useBoardStore`, `Board`, `BoardMetadata`, `SaveStatus` | +| `@thinkix/shared` | `@thinkix/shared` | **Types only** (no JSX) | +| `@thinkix/file-utils` | `@thinkix/file-utils` | File I/O, `.thinkix` import/export, SVG/PNG/JPG export | +| `@thinkix/collaboration` | `@thinkix/collaboration` + subpaths | Yjs + Liveblocks adapter, sync bus, cursor manager, presence. Sub-exports include `/hooks`, `/components`, `/providers`, `/adapter`, `/types`, `/test-utils` | +| `@thinkix/mermaid-to-thinkix` | `@thinkix/mermaid-to-thinkix` | Converts Mermaid diagrams into Plait elements. Transpiled by Next (`next.config.ts`) | -The monorepo uses a dual approach to avoid type duplication issues: +### Shared code: two locations by design -| Location | Contains | Reason | +| Location | Contents | Import | |----------|----------|--------| -| `packages/shared/` | **Types only** (no JSX) | Workspace package, import as `@thinkix/shared` | -| `shared/constants/` | **JSX + constants** | App-level, import as `@/shared/constants` | +| `packages/shared/` | **Types only, no JSX** | `import type { … } from '@thinkix/shared'` | +| `shared/constants/` | **Constants, icons, JSX** | `import { BASIC_TOOLS } from '@/shared/constants'` | -**Usage:** -```typescript -// Types (workspace package) -import type { DrawingTool, BoardState } from '@thinkix/shared'; - -// Constants with JSX (app-level) -import { BASIC_TOOLS, TOOLBAR_ITEM_CLASS } from '@/shared/constants'; -``` +Mixing them triggers React type duplication. When adding workspace packages with JSX, use `peerDependencies` for React and do not add `@types/react` to `devDependencies`. ## Architecture -### Feature-Based Structure +### Board canvas (`features/board/`) -```text -features/ -├── board/ # Board canvas and state management -│ ├── components/ -│ │ └── BoardCanvas.tsx # Main canvas with Plait Wrapper + plugins -│ ├── hooks/ -│ │ └── use-board-state.tsx # Board context and tool state -│ ├── plugins/ -│ │ ├── add-image-renderer.tsx -│ │ ├── add-emoji-renderer.tsx -│ │ ├── add-text-renderer.tsx -│ │ ├── add-mind-node-resize.ts -│ │ ├── add-pen-mode.ts -│ │ ├── add-image-interactions.ts -│ │ ├── with-sticky-note.ts -│ │ ├── with-eraser.ts -│ │ ├── with-text-normalization.ts -│ │ ├── handdrawn-mode/ # Hand-drawn style mode -│ │ └── scribble/ # Freehand drawing plugin -│ └── utils/ -│ └── laser-pointer.ts -│ -├── agent/ # AI Agent pane and tool execution -│ ├── components/ -│ │ ├── AgentPane.tsx # Main agent pane UI (Cmd+J toggle) -│ │ ├── AgentToolRenderer.tsx # Custom tool display labels -│ │ └── AgentSettingsDialog.tsx # API key configuration -│ ├── hooks/ -│ │ └── use-agent-chat.ts # AI SDK v5 chat hook with tool execution -│ ├── tools/ -│ │ ├── file-system-ops.ts # Tool execution (ls, read, write, patch, rm, select) -│ │ ├── serializer.ts # Plait elements -> DSL serialization -│ │ └── dsl/ -│ │ ├── types.ts # AST types (ShapeNode, StickyNode, etc.) -│ │ ├── thinkix.peggy # Peggy grammar for DSL parsing -│ │ ├── parser.js # Generated parser (run `bun run build:parser`) -│ │ ├── parser.ts # Parser wrapper with error handling -│ │ └── compiler.ts # AST -> PlaitElement compilation -│ └── index.ts # Barrel export -│ -├── toolbar/ # Toolbar UI -│ └── components/ -│ ├── BoardToolbar.tsx # Main toolbar -│ ├── AppMenu.tsx # Application menu -│ ├── ZoomToolbar.tsx # Zoom controls -│ ├── SelectionToolbar.tsx # Selection-specific tools -│ └── inline/ # Inline formatting controls -│ ├── TextColorDropdown.tsx -│ ├── FontSizeControl.tsx -│ ├── ColorDropdown.tsx -│ └── ArrowDropdown.tsx -│ -└── storage/ # Storage management - ├── hooks/ - │ └── use-auto-save.ts - └── components/ - └── BoardSwitcher.tsx - -shared/constants/ -├── tools.tsx # Tool configurations -├── icons.tsx # Icon components -├── colors.ts # Color definitions -├── styles.ts # Style constants -└── inline-toolbar.ts # Inline toolbar config +`BoardCanvas.tsx` wires Plait plugins in a fixed order: + +``` +withGrid → withDraw → withTextNormalization → withText → addTextRenderer → +addImageRenderer → withGroup → withMind → addEmojiRenderer → addMindNodeResize → +addPenMode → addImageInteractions → withScribble → withEraser → withStickyNote → +withHanddrawn → withToolSync → withPinchZoom ``` -### Plait Integration - -The board uses `@plait-board/react-board@0.4.0-2` with Plait 0.92.1 packages. - -**Custom Plugins** (loaded in BoardCanvas.tsx): -- `addImageRenderer` - React-based image rendering -- `addTextRenderer` - Custom Slate-based text editor -- `addEmojiRenderer` - Emoji rendering for mind maps -- `addMindNodeResize` - Resize handles for mind nodes -- `addPenMode` - Stylus/pencil detection -- `addImageInteractions` - Drag-drop, paste, click-to-view -- `withScribble` - Freehand drawing with smoothing -- `withStickyNote` - Sticky note support -- `withEraser` - Eraser tool -- `withTextNormalization` - Text value normalization -- `withHanddrawnMode` - Hand-drawn aesthetic mode - -**Standard Plait Plugins:** -- `withText` - Text editing from `@plait/common` -- `withSelection` - Element selection -- `withDraw` - Drawing primitives -- `withGroup` - Grouping support -- `withMind` - Mind map creation -- `withHistory` - Undo/redo -- `withHotkey` - Keyboard shortcuts - -### Custom Text Renderer - -**IMPORTANT**: The project uses a custom Slate-based text renderer (`features/board/plugins/add-text-renderer.tsx`) instead of `@plait-board/react-text`. - -**Why custom?** -- Fixes text typing persistence issue (React 19 + root.render() incompatibility) -- Removes default Chinese text ("文本") on new text elements -- Provides full control over editor lifecycle and state management - -**Text element properties:** -- `autoSize: true` - Elements automatically resize to fit text content -- Text elements use Slate JSON format: `{ children: [{ text: 'content' }] }` - -### State Management - -- `BoardProvider` (features/board/hooks/use-board-state.tsx) provides: - - `board` - PlaitBoard instance - - `state.activeTool` - Current drawing tool - - `setActiveTool()` - Change tool, updates Plait pointer type - - `insertImage()` - Insert image at viewport center - -- Board operations use Plait APIs: - - `getSelectedElements(board)` - - `deleteFragment(board)` - - `duplicateElements(board)` - - `board.undo() / board.redo()` - -### Styling - -- Tailwind CSS v4 with `@import "tailwindcss"` syntax -- OKLCH colors for light/dark theme -- Shadcn-style components using `class-variance-authority` -- Plait CSS imported via `app/styles/plait-react-board.css` - -### Code Style - -- Write self-documenting code; avoid excessive comments -- Use feature-based organization for scalability -- Export types from shared/ for reusability -- Prefer early returns: `if (!board) return null;` +Notable custom plugins (`features/board/plugins/`): +- `add-text-renderer.tsx` — custom Slate-based renderer (replaces `@plait-board/react-text`). Fixes React 19 + `root.render()` persistence issues and removes the default "文本" placeholder. Text elements use `{ children: [{ text: '…' }] }`, `autoSize: true`. +- `with-tool-sync.ts` — monkey-patches `BoardTransforms.updatePointerType` to dispatch `CUSTOM_EVENTS.TOOL_CHANGE`. Plait has no native hook for internal pointer changes (e.g. after creating an element), so this bridges Plait state into React. +- `scribble/`, `handdrawn-mode/`, `with-sticky-note.ts`, `with-eraser.ts` — freehand drawing, hand-drawn aesthetic, sticky notes, eraser. -## Testing +The grid system lives under `features/board/grid/` (separate plugin, constants, renderers, `GridToolbar`). -Tests are located in `tests/` directory: +`BoardProvider` (`features/board/hooks/use-board-state.tsx`) exposes `board`, `state.activeTool`, `setActiveTool()`, `insertImage()`, pencil-mode state, and current-board id. -```text -tests/ -├── unit/ # Unit tests -│ └── file-utils.test.ts -├── integration/ # Integration tests -│ └── storage.test.ts -├── components/ # Component tests -│ └── loading-logo.test.tsx -├── __utils__/ -│ └── test-utils.ts # Testing utilities -└── __mocks__/ - └── setup.mts # Global test setup -``` +### AI agent (`features/agent/`) -**Testing Stack:** -- Vitest with `happy-dom` environment -- `@testing-library/react` for component testing -- `@vitest/coverage-v8` for coverage reports +Flow: `AgentPane` (Cmd/Ctrl+J) → `useAgentChat` → POST `/api/agent` (AI SDK v5 `streamText`) → streamed tool calls executed client-side in `tools/run-tool.ts`. -**Coverage Configuration:** -- Excludes: `node_modules`, `scratch`, `.next`, `tests`, `app`, config files +Two parallel tool layers exist; they share the same serializer: +1. **Shell commands** (`tools/commands/index.ts`) — a Unix-shell-style surface with `ls, cd, pwd, cat, touch, mkdir, rm, rmdir, cp, patch, write, select, grep`. Invoked through `chain-parser.ts` (supports `&&` / `;`). Board-scoped vs global (`ls /` lists boards). +2. **DSL** (`tools/dsl/`) — Peggy grammar in `thinkix.peggy`, generated `parser.js`, typed AST (`types.ts`), and AST-to-PlaitElement `compiler.ts`. Used by `write mermaid`/`write mindmap` and higher-level creation commands. -## CI/CD +Other agent internals: +- `serializer.ts` — PlaitElement → text (`summary` / `full`) used for `ls`, `cat`, `grep` output. +- `connection-graph.ts` — resolves connection-heavy layouts. +- `diagram-layout.ts` — `relayoutHeaderDrivenDiagram` after Mermaid parsing. +- `presentation-layer.ts` — overflow output storage (`/tmp/cmd-output/`). +- `result-types.ts` — structured `CommandResultData` variants rendered by `AgentToolRenderer`. -GitHub Actions workflow with parallel jobs: +Server prompt + provider resolution: +- `packages/ai/prompts/system.ts` — system prompt builder. +- `packages/ai/src/core.ts` — `resolveAIProvider`, `resolveAIModel`, `resolveAIKey`, `resolveAIBaseURL`, `normalizeAIBaseURL` (handles z.ai `/v1/` quirk), `getClientAIConfig`. Default provider is OpenAI (`gpt-4o`); Anthropic default is `claude-opus-4-6`. -```text -┌─────────────┐ ┌─────────────┐ ┌─────────────┐ -│ lint │ │ typecheck │ │ test │ -│ (parallel) │ │ (parallel) │ │ (parallel) │ -└──────┬──────┘ └──────┬──────┘ └──────┬──────┘ - │ │ │ - └─────────────────┴─────────────────┘ - │ - All must pass ✓ - │ - ┌──────▼──────┐ - │ build │ - └─────────────┘ -``` +### Collaboration -**Workflow Files:** -- `.github/workflows/ci.yml` - Main CI pipeline -- `.github/actions/setup-bun/` - Reusable Bun setup with caching -- `.github/actions/coverage-report/` - Coverage upload and PR comments -- `.github/scripts/coverage-summary.sh` - Coverage parsing script +`@thinkix/collaboration` wraps Liveblocks + Yjs. `SyncBus` (`sync-bus.tsx`) broadcasts local element changes and subscribes to remote changes. `BoardCanvas` consumes it via `useOptionalSyncBus()` and routes remote updates through `syncElementsForBoardTheme` + `listRender.update`. `cursor-manager.ts` + `user-identity.ts` drive presence cursors. Auth is served from `app/api/collaboration/auth`. Requires `LIVEBLOCKS_SECRET_KEY` and `NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEY`; otherwise collab is gracefully absent. -## Important Configuration +### Dialogs (`features/dialogs/`) -- **next.config.ts**: Transpiles `@plait-board/*` packages (required for client-side rendering) -- **tsconfig.json**: Path aliases `@/features/*`, `@/shared/*`, `@/components/*` -- **vitest.config.mts**: Test configuration with path aliases matching tsconfig -- **Plait packages**: Do NOT use webpack resolve aliases - causes internal dependency conflicts +- `MermaidToBoardDialog` — paste Mermaid, convert via `@thinkix/mermaid-to-thinkix`, insert onto the board. +- `MarkdownToMindmapDialog` — markdown outline → mind map via `parseMarkdownToMindElement`. -## AI Environment Variables +### API routes -The agent supports both local per-user API keys and server-side defaults. +- `app/api/agent/route.ts` — `POST` streams AI responses and tool calls. +- `app/api/agent/config/route.ts` — safe client config (`getClientAIConfig`); the client never receives raw keys. +- `app/api/collaboration/auth/` — Liveblocks token endpoint. -Preferred env vars: +## Environment variables + +The agent accepts per-user keys entered in the UI *and* server-side env defaults. Resolution order: UI settings → provider-specific env (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`) → generic env (`AI_API_KEY`, `AI_PROVIDER`, `AI_MODEL`, `AI_BASE_URL`) → provider defaults in `packages/ai/src/core.ts`. ```bash -AI_PROVIDER=openai +# AI (any combination) +AI_PROVIDER=openai|anthropic AI_MODEL=gpt-4o -AI_API_KEY=sk-... -AI_BASE_URL=https://api.openai.com/v1 - -OPENAI_API_KEY=sk-... -OPENAI_BASE_URL=https://api.openai.com/v1 - -ANTHROPIC_API_KEY=sk-ant-... -ANTHROPIC_BASE_URL=https://api.anthropic.com +AI_API_KEY=... +AI_BASE_URL=... +OPENAI_API_KEY=... +OPENAI_BASE_URL=... +ANTHROPIC_API_KEY=... +ANTHROPIC_BASE_URL=... + +# Collaboration +LIVEBLOCKS_SECRET_KEY=... +NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEY=... + +# Web search tool +EXA_API_KEY=... + +# Analytics +NEXT_PUBLIC_POSTHOG_KEY=... +NEXT_PUBLIC_POSTHOG_HOST=... ``` -Resolution order: -- user-supplied agent settings from the UI -- provider-specific env vars such as `OPENAI_API_KEY` / `ANTHROPIC_API_KEY` -- generic env vars such as `AI_API_KEY`, `AI_PROVIDER`, `AI_MODEL`, `AI_BASE_URL` -- built-in provider defaults from `packages/ai/src/core.ts` - -The client should never receive raw keys. The UI reads a safe `/api/agent/config` response to learn whether server defaults exist and what provider/model should be shown by default. - -## AI Agent Architecture - -The AI Agent pane provides an LLM-powered assistant that can read, create, edit, and delete canvas elements via tool calling with a custom DSL. +## Configuration gotchas -### Tech Stack -- **AI SDK v5** (`ai` package) with `streamText` for server-side streaming -- **AI Elements** (shadcn-based AI components) for UI: Conversation, Message, Tool, PromptInput -- **Peggy parser generator** for DSL parsing -- **Zod** for tool schema validation +- **`next.config.ts`** transpiles `@plait-board/react-board`, `@plait-board/react-text`, and `@thinkix/mermaid-to-thinkix`. Add new packages that ship untranspiled TS/JSX here. +- **Do not add webpack `resolve.alias` for `@plait/*` packages** — it breaks Plait's internal dependency graph. Use the TS path aliases only. +- **Path aliases** (`tsconfig.json`): `@/*`, `@/features/*`, `@/shared/*`, and every `@thinkix/*` package plus their sub-exports. `vitest.config.mts` mirrors these. +- **Patches** live in `patches/@plait+draw+.patch`. When upgrading `@plait/draw`, regenerate the patch for the new version (otherwise `apply-patches.sh` logs a warning and exits 0). -### How It Works -1. User sends a message via `AgentPane` (Cmd+J to toggle) -2. Request goes to `/api/agent` which streams LLM response with tool calls -3. Client-side `useAgentChat` hook intercepts tool calls and executes them via `file-system-ops.ts` -4. Tools operate on the Plait board: `ls`, `read`, `write`, `patch`, `rm`, `select` - -### DSL (Domain-Specific Language) -Custom DSL bridges natural language to Plait elements: - -```text -shape rect "My Box" # Create rectangle -sticky "Note" color:yellow # Create sticky note -connect id:abc123 -> id:def456 # Connect elements -mindmap "Root" # Create mind map - "Child 1" -patch abc123 text:"New label" # Update element -layout grid # Arrange in grid -``` +## Testing -### Key Files -- `features/agent/tools/dsl/thinkix.peggy` - Peggy grammar -- `features/agent/tools/dsl/compiler.ts` - AST -> PlaitElement -- `features/agent/tools/serializer.ts` - PlaitElement -> DSL -- `features/agent/tools/file-system-ops.ts` - Tool execution -- `packages/ai/prompts/system.ts` - System prompt builder +- **Vitest** (`tests/unit/`, `tests/integration/`, `tests/components/`) with `happy-dom`, coverage via `@vitest/coverage-v8`. Global setup in `tests/__mocks__/setup.mts` mocks Canvas, FileSystem, IndexedDB, `matchMedia`, `ResizeObserver`, and Plait APIs. Shared helpers in `tests/__utils__/test-utils.ts` (`createMockBoard`, `createMockThinkixFile`). +- **Playwright** (`tests/e2e/`) runs a real production build on `:3100`. Retries once on CI, 6 workers on CI. `tests/e2e/helpers/` holds shared page-object helpers. +- Coverage excludes: `node_modules`, `scratch`, `.next`, `tests`, `app`, config files. Threshold is 70% across lines/functions/branches/statements. -### Parser Generation -The DSL parser must be regenerated after grammar changes: -```bash -bun run build:parser # Generates features/agent/tools/dsl/parser.js -``` +## CI -## Adding New Workspace Packages +`.github/workflows/ci.yml` runs `lint`, `typecheck`, and `test:coverage` in parallel; all must pass before `build`. Reusable setup at `.github/actions/setup-bun` (caches Bun + workspace `node_modules`). Coverage is uploaded and commented on PRs by `.github/actions/coverage-report` + `.github/scripts/coverage-summary.sh`. -When creating workspace packages, follow these guidelines: +## Code conventions -### Types-Only Packages (preferred) -For packages containing only TypeScript types (no JSX): -```json -{ - "name": "@thinkix/types", - "devDependencies": { - "typescript": "^5" - } -} -``` -- Do NOT add `@types/react` as devDependency (causes duplicate type conflicts) -- Use `peerDependencies` if the package requires React at runtime - -### Packages with JSX -For packages containing React components (JSX): -- Keep dependencies minimal -- Use `peerDependencies` for React instead of regular dependencies -- Avoid `@types/react` in devDependencies (comes from root) - -## Known Issues & Solutions - -### Tool Change Event System -- **Issue**: Plait doesn't provide hooks for tool changes triggered internally (e.g., after creating elements) -- **Solution**: `withToolSync` plugin monkey-patches `BoardTransforms.updatePointerType` to emit custom events -- **Event**: `CUSTOM_EVENTS.TOOL_CHANGE` dispatched when pointer transitions to selection mode -- **Usage**: Listen with `window.addEventListener(CUSTOM_EVENTS.TOOL_CHANGE, handler)` -- **Location**: `features/board/plugins/with-tool-sync.ts` -- **Why this approach**: - - Enables React state integration for tool tracking - - Supports conditional rendering based on active tool - - Triggers side effects (analytics, UI updates) on tool changes - - Handles internal Plait pointer changes (e.g., after element creation) -- **Alternative**: Drawnix reads `board.pointer` directly (simpler but no React state) -- **Tradeoff**: More complex but provides better React integration -- **Note**: Monitor Plait updates for native event support +- Self-documenting code; no ceremonial comments. Only comment non-obvious *why*. +- Feature modules expose a small `index.ts` barrel. Follow existing patterns instead of introducing new ones. +- Types flow from `@thinkix/shared` (no JSX) and `shared/constants` (JSX allowed). +- Prefer early returns (`if (!board) return null`). +- Keep PRs focused; if you change the DSL grammar, regenerate the parser before opening the PR (see `CONTRIBUTING.md`).