diff --git a/docs/development/roadmap.md b/docs/development/roadmap.md
index 84cb4aba3..7d5e922e0 100644
--- a/docs/development/roadmap.md
+++ b/docs/development/roadmap.md
@@ -107,6 +107,9 @@ Core extraction, CLI, MCP server, design guidelines, screenshot verification loo
- .fig roundtrip tests with LFS fixtures (material3.fig 87K nodes, nuxtui.fig 314K nodes)
- .fig import O(n²) → O(n) fix (37s → 535ms on 87K nodes), ByteBuffer optimization
- test:coverage script
+- AI chat: OpenRouter direct (no backend), Stronghold key storage, 10 tools (create_shape, set_fill, etc.), model selector, ⌘J toggle, streaming markdown, Playwright tests with mock transport
+- Code panel: sceneNodeToJsx() export, Prism.js highlighting, line numbers, copy button, 14 tests
+- Properties panel restructured: Design | Code | AI tabs
- npm publishing preparation for core and cli packages
**Planned:**
diff --git a/docs/guide/features.md b/docs/guide/features.md
index 6cab6d426..4e86a4ec7 100644
--- a/docs/guide/features.md
+++ b/docs/guide/features.md
@@ -69,18 +69,22 @@ Both the layers panel and properties panel are resizable — drag the edge betwe
## Properties Panel
-Context-sensitive panel with sections:
+Tabbed interface with **Design** | **Code** | **AI** tabs (reka-ui Tabs).
+
+The **Design** tab is context-sensitive with sections:
- **Appearance** — opacity, corner radius (uniform or per-corner with independent toggle), visibility
- **Fill** — solid/gradient/image type picker, gradient stop editor, hex input, opacity
- **Stroke** — color, weight, opacity, cap, join, dash pattern
- **Effects** — add/remove effects, type picker (drop shadow, inner shadow, layer blur, background blur, foreground blur), inline expanded controls (offset, blur, spread, color for shadows; blur radius for blurs), per-effect visibility toggle
-- **Typography** — font family (FontPicker with virtual scroll and search), weight, size, alignment
+- **Typography** — font family (FontPicker with virtual scroll and search), weight, size, alignment, B/I/U/S buttons
- **Layout** — auto-layout controls when enabled
- **Position** — alignment buttons, rotation, flip
- **Export** — scale, format (PNG/JPG/WEBP), live preview, multi-export
- **Page** — canvas background color (shown when no nodes selected)
+The **Code** tab shows JSX export of the selection (see [Code Panel](#code-panel)). The **AI** tab provides an AI chat interface (see [AI Chat](#ai-chat)).
+
## Group/Ungroup
⌘G groups selected nodes. ⇧⌘G ungroups. Nodes are sorted by visual position when grouping to preserve reading order.
@@ -189,7 +193,23 @@ Two rendering paths:
- `renderTreeNode()` — tree → scene graph (any runtime, no external deps)
- `renderJsx()` — JSX string → esbuild → tree → scene graph (CLI/headless)
-Covered by 27 tests for all node types, layout props, effects, and nesting.
+
+
+## AI Chat
+
+Built-in AI assistant accessible via the AI tab in the properties panel or ⌘J. Communicates directly with OpenRouter from the browser — no backend server required. API key stored securely in Tauri Stronghold (localStorage fallback in browser).
+
+**Model selector** with curated models: Claude, Gemini, GPT, DeepSeek, Qwen, Kimi, Llama — stored in `@open-pencil/core` constants with benchmark-ranked tags. Responses stream as markdown (vue-stream-markdown).
+
+**10 AI tools** wired to the editor store with valibot schemas: `create_shape`, `set_fill`, `set_stroke`, `update_node`, `set_layout`, `delete_node`, `select_nodes`, `get_page_tree`, `get_selection`, `rename_node`. The ToolLoopAgent executes tools automatically in a loop. Tool calls display as collapsible timeline entries in the chat (reka-ui Collapsible).
+
+Tested with Playwright using mock transport for CI.
+
+## Code Panel
+
+The Code tab in the properties panel shows the JSX representation of the current selection. Uses `sceneNodeToJsx()` from `@open-pencil/core` to convert the SceneNode subtree into JSX with Tailwind-like shorthand props. Prism.js syntax highlighting with line numbers and a copy-to-clipboard button. Multi-selection shows each node's JSX. The exported JSX is compatible with `renderJsx()` for round-trip creation.
+
+
## Code Quality
diff --git a/docs/guide/figma-comparison.md b/docs/guide/figma-comparison.md
index f665ab75d..6efec7011 100644
--- a/docs/guide/figma-comparison.md
+++ b/docs/guide/figma-comparison.md
@@ -6,7 +6,7 @@ Feature-by-feature comparison of Figma Design capabilities with Open Pencil's cu
✅ Supported — feature works end-to-end · 🟡 Partial — core behavior exists, some sub-features missing · 🔲 Not yet implemented
:::
-**Coverage:** 80 of 150 Figma feature items addressed — 64 ✅ fully supported, 16 🟡 partial, 70 🔲 not yet. Last updated: 2026-03-01.
+**Coverage:** 83 of 150 Figma feature items addressed — 64 ✅ fully supported, 19 🟡 partial, 67 🔲 not yet. Last updated: 2026-03-01.
## Interface & Navigation
@@ -27,7 +27,7 @@ Feature-by-feature comparison of Figma Design capabilities with Open Pencil's cu
| Layer outlines view | 🔲 | Wireframe view of all layers |
| Custom file thumbnails | 🔲 | Thumbnail generated on export, but no custom thumbnail picker |
| Nudge value settings | 🔲 | Default 1px/10px; Figma allows custom small/big nudge values |
-| AI tools | 🔲 | Figma AI features (rename, search, generate); Open Pencil plans MCP-based AI in Phase 5 |
+| AI tools | 🟡 | 10 AI tools via OpenRouter (create/modify/delete shapes, fills/strokes, layout); no AI-generated images or AI-powered search yet |
## Layers & Shapes
@@ -204,9 +204,9 @@ Feature-by-feature comparison of Figma Design capabilities with Open Pencil's cu
| Real-time multiplayer | 🔲 | Planned (Yjs-based) |
| Cursor chat | 🔲 | Inline chat bubbles at cursor |
| Branching & merging | 🔲 | Version branches for design files |
-| Dev Mode (inspect) | 🔲 | CSS/code generation from designs |
+| Dev Mode (inspect) | 🟡 | Code tab shows JSX representation of selection; no CSS properties or handoff specs |
| Code Connect | 🔲 | Link design components to code |
-| Code snippets | 🔲 | Auto-generated code from layers |
+| Code snippets | 🟡 | JSX export with syntax highlighting and copy; no CSS/Swift/Kotlin snippets |
| Figma for VS Code | 🔲 | Editor plugin integration |
| MCP server | 🔲 | Planned for Phase 5; 117-tool MCP server from figma-use |
| CLI tools | 🟡 | Headless CLI: info, tree, find, export, analyze (colors/typography/spacing/clusters), node, pages, variables; MCP server not yet |
diff --git a/docs/reference/keyboard-shortcuts.md b/docs/reference/keyboard-shortcuts.md
index cbc278c7c..defffa138 100644
--- a/docs/reference/keyboard-shortcuts.md
+++ b/docs/reference/keyboard-shortcuts.md
@@ -68,6 +68,7 @@ Full Figma-compatible shortcut map. ✅ = implemented.
| ⌘0 | Zoom to 100% | ✅ |
| ⌘1 | Zoom to Fit | 🔲 |
| ⌘2 | Zoom to Selection | 🔲 |
+| ⌘J | Toggle AI Chat | ✅ |
## Object
@@ -96,9 +97,9 @@ Full Figma-compatible shortcut map. ✅ = implemented.
| Shortcut | Action | Status |
|----------|--------|--------|
-| ⌘B | Bold | 🔲 |
-| ⌘I | Italic | 🔲 |
-| ⌘U | Underline | 🔲 |
+| ⌘B | Bold | ✅ |
+| ⌘I | Italic | ✅ |
+| ⌘U | Underline | ✅ |
| ⇧⌘X | Strikethrough | 🔲 |
## Canvas Interaction
diff --git a/openspec/changes/archive/2026-03-01-sync-ai-chat-code-tab/.openspec.yaml b/openspec/changes/archive/2026-03-01-sync-ai-chat-code-tab/.openspec.yaml
new file mode 100644
index 000000000..0b4defe0e
--- /dev/null
+++ b/openspec/changes/archive/2026-03-01-sync-ai-chat-code-tab/.openspec.yaml
@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-03-01
diff --git a/openspec/changes/archive/2026-03-01-sync-ai-chat-code-tab/design.md b/openspec/changes/archive/2026-03-01-sync-ai-chat-code-tab/design.md
new file mode 100644
index 000000000..f5992628e
--- /dev/null
+++ b/openspec/changes/archive/2026-03-01-sync-ai-chat-code-tab/design.md
@@ -0,0 +1,19 @@
+# Design: Sync AI chat, Code tab
+
+## Approach
+Documentation-only. Update/create specs for AI chat and Code panel, propagate to VitePress docs and comparison matrix.
+
+## Affected spec domains
+
+1. **editor-ui** — Properties panel restructured to Design | Code | AI tabs, DesignPanel extraction, ChatPanel + sub-components, CodePanel, ⌘J shortcut
+2. **scene-graph** (new requirement) — sceneNodeToJsx() export function
+3. **testing** — Playwright chat tests with mock transport, 14 JSX export tests
+
+## New spec domain consideration
+AI chat could warrant its own spec domain, but it's tightly integrated into editor-ui (tab in properties panel, tools call editor store). Keep in editor-ui for now.
+
+## VitePress docs updates
+- features.md: add "AI Chat" section, add "Code Panel" section, update Properties Panel description
+- figma-comparison.md: AI tools 🔲→🟡, Code snippets 🔲→🟡, Dev Mode 🔲→🟡
+- roadmap.md: Phase 5 delivered items (AI chat, Code tab)
+- keyboard-shortcuts.md: ⌘J (toggle AI chat)
diff --git a/openspec/changes/archive/2026-03-01-sync-ai-chat-code-tab/proposal.md b/openspec/changes/archive/2026-03-01-sync-ai-chat-code-tab/proposal.md
new file mode 100644
index 000000000..790919a96
--- /dev/null
+++ b/openspec/changes/archive/2026-03-01-sync-ai-chat-code-tab/proposal.md
@@ -0,0 +1,25 @@
+# Proposal: Sync AI chat, Code tab
+
+## Why
+19 commits from master (6268d8b..c59d5fc) introduce AI chat integration and a Code panel for JSX export.
+
+## What changed
+
+### AI chat (5d23c3e → bf8636c, 15 commits)
+- ChatPanel with message list, tool timeline, typing indicator, API key setup via Tauri Stronghold
+- DirectChatTransport: no backend server, calls OpenRouter directly from browser
+- ToolLoopAgent with 10 AI tools (valibot schemas): create_shape, set_fill, set_stroke, update_node, set_layout, delete_node, select_nodes, get_page_tree, get_selection, rename_node
+- Model selector with curated list: Claude 4.6, Gemini 3.1, GPT-5.3, DeepSeek V3.2, Qwen 3.5, Kimi K2.5, Llama
+- ⌘J keyboard shortcut to toggle chat
+- Properties panel restructured: Design | Code | AI tabs (reka-ui Tabs)
+- DesignPanel extracted from PropertiesPanel
+- ChatMessage, ChatInput, APIKeySetup sub-components
+- reka-ui ScrollArea/Collapsible/Tooltip, tw-animate-css, vue-stream-markdown for streaming
+- Playwright tests with mock transport, tool call e2e test
+- X-OpenRouter-Title header for app identification
+
+### Code tab / JSX export (c59d5fc)
+- sceneNodeToJsx() in @open-pencil/core: converts SceneNode subtree to JSX using builder syntax
+- CodePanel.vue with Prism.js syntax highlighting, line numbers, copy button
+- Third tab in properties panel
+- 14 new tests covering shapes, text, layout, effects, multi-selection
diff --git a/openspec/changes/archive/2026-03-01-sync-ai-chat-code-tab/specs/editor-ui/spec.md b/openspec/changes/archive/2026-03-01-sync-ai-chat-code-tab/specs/editor-ui/spec.md
new file mode 100644
index 000000000..7b0d17a02
--- /dev/null
+++ b/openspec/changes/archive/2026-03-01-sync-ai-chat-code-tab/specs/editor-ui/spec.md
@@ -0,0 +1,63 @@
+# editor-ui Specification (delta)
+
+## Modified Requirements
+
+### Requirement: Properties panel with sections (MODIFIED)
+The properties panel SHALL be organized as a tabbed interface with Design | Code | AI tabs (reka-ui Tabs). The Design tab contains the existing property sections (DesignPanel). The Code tab shows JSX export. The AI tab shows the chat interface.
+
+#### Scenario: Switch to Code tab
+- **WHEN** user clicks the Code tab in the properties panel
+- **THEN** the JSX code representation of the selection is displayed
+
+#### Scenario: Switch to AI tab
+- **WHEN** user clicks the AI tab
+- **THEN** the AI chat interface is displayed
+
+## New Requirements
+
+### Requirement: AI chat panel
+The editor SHALL include an AI chat panel accessible via the AI tab in properties panel or ⌘J shortcut. The panel provides: message list with streaming markdown (vue-stream-markdown), tool call timeline with collapsible details (reka-ui Collapsible), typing indicator, API key setup for OpenRouter (stored in Tauri Stronghold). Messages scroll automatically (reka-ui ScrollArea).
+
+#### Scenario: Send message
+- **WHEN** user types a message and presses Enter in the chat input
+- **THEN** the message is sent to OpenRouter and a streaming response appears
+
+#### Scenario: Toggle chat
+- **WHEN** user presses ⌘J
+- **THEN** the properties panel switches to the AI tab (or back to Design)
+
+### Requirement: AI model selector
+The chat input SHALL include a model selector with curated models: Claude, Gemini, GPT, DeepSeek, Qwen, Kimi, Llama. Models are stored in @open-pencil/core constants with benchmark-ranked tags.
+
+#### Scenario: Change model
+- **WHEN** user selects a different model from the selector
+- **THEN** subsequent messages use the selected model
+
+### Requirement: AI tools
+The chat SHALL support 10 design tools with valibot schemas: create_shape, set_fill, set_stroke, update_node, set_layout, delete_node, select_nodes, get_page_tree, get_selection, rename_node. Tools execute against the editor store. The ToolLoopAgent calls tools automatically in a loop.
+
+#### Scenario: AI creates a shape
+- **WHEN** user asks "Create a blue rectangle 200×100"
+- **THEN** the AI calls create_shape with type RECTANGLE, width 200, height 100, and set_fill with blue color
+
+#### Scenario: Tool call visible in chat
+- **WHEN** the AI executes a tool
+- **THEN** a collapsible tool call section shows the tool name, input, and output
+
+### Requirement: DirectChatTransport
+The chat SHALL communicate directly with OpenRouter from the browser — no backend server required. The API key is stored securely in Tauri Stronghold (or localStorage fallback in browser). X-OpenRouter-Title header identifies the app.
+
+#### Scenario: No backend required
+- **WHEN** user configures an OpenRouter API key and sends a message
+- **THEN** the request goes directly to OpenRouter without a proxy server
+
+### Requirement: Code panel with JSX export
+The properties panel Code tab SHALL display the selected node(s) as JSX code using sceneNodeToJsx(). The panel provides Prism.js syntax highlighting, line numbers, and a copy-to-clipboard button. Multi-selection shows each node's JSX.
+
+#### Scenario: View JSX for selection
+- **WHEN** user selects a frame and clicks the Code tab
+- **THEN** the JSX representation of the frame and its children is displayed with syntax highlighting
+
+#### Scenario: Copy JSX
+- **WHEN** user clicks the copy button in the Code panel
+- **THEN** the JSX code is copied to the clipboard
diff --git a/openspec/changes/archive/2026-03-01-sync-ai-chat-code-tab/specs/scene-graph/spec.md b/openspec/changes/archive/2026-03-01-sync-ai-chat-code-tab/specs/scene-graph/spec.md
new file mode 100644
index 000000000..6c62cca53
--- /dev/null
+++ b/openspec/changes/archive/2026-03-01-sync-ai-chat-code-tab/specs/scene-graph/spec.md
@@ -0,0 +1,14 @@
+# scene-graph Specification (delta)
+
+## New Requirements
+
+### Requirement: Scene node to JSX export
+@open-pencil/core SHALL export a sceneNodeToJsx() function that converts a SceneNode subtree into JSX string using builder function syntax (Frame, Text, Rectangle, etc.) with Tailwind-like shorthand props. The output is valid JSX that can be consumed by renderJsx().
+
+#### Scenario: Export frame with children
+- **WHEN** sceneNodeToJsx is called on a frame with a rectangle and text child
+- **THEN** a JSX string with Frame wrapping Rectangle and Text is returned
+
+#### Scenario: Export with effects
+- **WHEN** sceneNodeToJsx is called on a node with drop shadow
+- **THEN** the JSX includes shadow props in the output
diff --git a/openspec/changes/archive/2026-03-01-sync-ai-chat-code-tab/specs/testing/spec.md b/openspec/changes/archive/2026-03-01-sync-ai-chat-code-tab/specs/testing/spec.md
new file mode 100644
index 000000000..b38da0419
--- /dev/null
+++ b/openspec/changes/archive/2026-03-01-sync-ai-chat-code-tab/specs/testing/spec.md
@@ -0,0 +1,17 @@
+# testing Specification (delta)
+
+## New Requirements
+
+### Requirement: AI chat Playwright tests
+The test suite SHALL include Playwright e2e tests for the AI chat panel with mock transport (no real LLM calls). Tests cover: sending a message, receiving a response, tool call display. Real LLM tests available via TEST_REAL_LLM=1 flag.
+
+#### Scenario: Chat with mock transport
+- **WHEN** a Playwright test sends a message via the chat input
+- **THEN** the mock transport returns a response and the message appears in the chat
+
+### Requirement: JSX export tests
+The test suite SHALL include tests for sceneNodeToJsx() covering shapes, text, layout props, effects, and multi-selection. Currently 14 tests.
+
+#### Scenario: Export rectangle to JSX
+- **WHEN** sceneNodeToJsx is called on a rectangle with blue fill
+- **THEN** the output includes Rectangle component with bg prop
diff --git a/openspec/changes/archive/2026-03-01-sync-ai-chat-code-tab/tasks.md b/openspec/changes/archive/2026-03-01-sync-ai-chat-code-tab/tasks.md
new file mode 100644
index 000000000..db1f2f7d6
--- /dev/null
+++ b/openspec/changes/archive/2026-03-01-sync-ai-chat-code-tab/tasks.md
@@ -0,0 +1,35 @@
+# Tasks: Sync AI chat, Code tab
+
+Reference: 19 commits from master (6268d8b..c59d5fc).
+Delta-specs in `openspec/changes/sync-ai-chat-code-tab/specs/` (editor-ui, scene-graph, testing).
+Proposal: `proposal.md`, Design: `design.md` — validated and aligned with delta-specs.
+
+Section 1 (specs) MUST complete before Section 2 (docs) — docs reference merged spec content.
+
+## 1. Merge Delta-Specs into Main Specs
+
+Each task merges ADDED and MODIFIED requirements from the delta-spec into the baseline at `openspec/specs//spec.md`. Untouched baseline requirements are preserved. No conflicts between domains.
+
+- [x] 1.1 Update `openspec/specs/editor-ui/spec.md` — MODIFIED: "Properties panel" → Design | Code | AI tabs. ADDED: "AI chat panel" (message list, streaming markdown, tool timeline, API key, ⌘J), "AI model selector" (curated models in core constants), "AI tools" (10 tools with valibot, ToolLoopAgent), "DirectChatTransport" (no backend, OpenRouter, Stronghold), "Code panel with JSX export" (Prism.js, line numbers, copy)
+- [x] 1.2 Update `openspec/specs/scene-graph/spec.md` — ADDED: "Scene node to JSX export" (sceneNodeToJsx function)
+- [x] 1.3 Update `openspec/specs/testing/spec.md` — ADDED: "AI chat Playwright tests" (mock transport, tool call e2e), "JSX export tests" (14 tests)
+
+## 2. Update VitePress Docs
+
+All target files already exist — no sidebar/nav changes needed.
+
+- [x] 2.1 Update `docs/guide/features.md`:
+ - **Modify "Properties Panel" bullet list**: add Code and AI tabs to description
+ - **Add "AI Chat" section** (after "Code Quality"): ⌘J toggle, OpenRouter direct (no backend), Stronghold key storage, model selector (Claude/Gemini/GPT/DeepSeek/Qwen/Kimi/Llama), 10 AI tools (create_shape, set_fill, etc.), streaming markdown, tool call timeline, Playwright tests with mock transport
+ - **Add "Code Panel" section** (after "AI Chat"): sceneNodeToJsx() in core, Prism.js highlighting, line numbers, copy button, Design | Code | AI tabs
+- [x] 2.2 Update `docs/guide/figma-comparison.md` — specific row changes:
+ - "AI tools" 🔲→🟡: "10 AI tools via OpenRouter (create/modify/delete shapes, set fills/strokes, layout); no AI-generated content or AI search yet"
+ - "Code snippets" 🔲→🟡: "JSX export from selection with syntax highlighting and copy; no CSS/Swift/Kotlin snippets"
+ - "Dev Mode (inspect)" 🔲→🟡: "Code tab shows JSX representation; no CSS properties or handoff specs"
+ - No other rows change status. After: 64✅, 19🟡, 67🔲 = 150 total. Update coverage line
+- [x] 2.3 Update `docs/development/roadmap.md` — Phase 5 Delivered: add AI chat (OpenRouter, 10 tools, model selector, Stronghold, ⌘J, Playwright tests), Code panel (sceneNodeToJsx, Prism.js, 14 tests)
+- [x] 2.4 Update `docs/reference/keyboard-shortcuts.md` — add ⌘J (Toggle AI chat) with ✅ status
+
+## 3. Verify
+
+- [x] 3.1 Run `bun run docs:build` — verify build passes. Confirm: coverage count matches actual ✅/🟡/🔲 tallies via grep -c, no broken anchor warnings
diff --git a/openspec/specs/editor-ui/spec.md b/openspec/specs/editor-ui/spec.md
index 56cc2a7d1..4c677c89a 100644
--- a/openspec/specs/editor-ui/spec.md
+++ b/openspec/specs/editor-ui/spec.md
@@ -48,8 +48,8 @@ The layers panel SHALL display a tree view of the document hierarchy using Reka
- **WHEN** user clicks the visibility icon next to a layer
- **THEN** the node is hidden/shown on canvas
-### Requirement: Properties panel with sections
-The properties panel SHALL be split into sections: Appearance (opacity, corner radius with independent mode, visibility toggle), Fill, Stroke, Effects, Typography, Layout, Position.
+### Requirement: Properties panel with tabs
+The properties panel SHALL be organized as a tabbed interface with Design | Code | AI tabs (reka-ui Tabs). The Design tab (DesignPanel) contains sections: Appearance (opacity, corner radius with independent mode, visibility toggle), Fill, Stroke, Effects, Typography, Layout, Position. The Code tab shows JSX export. The AI tab shows the chat interface.
#### Scenario: Properties panel shows fill section
- **WHEN** user selects a rectangle with a blue fill
@@ -292,3 +292,50 @@ The TypographySection SHALL include B/I/U/S toggle buttons for Bold, Italic, Und
#### Scenario: Underline button with no selection
- **WHEN** user clicks the Underline button during editing with no selection
- **THEN** the entire text node's underline style toggles
+
+### Requirement: AI chat panel
+The editor SHALL include an AI chat panel accessible via the AI tab in properties panel or ⌘J shortcut. The panel provides: message list with streaming markdown (vue-stream-markdown), tool call timeline with collapsible details (reka-ui Collapsible), typing indicator, API key setup for OpenRouter (stored in Tauri Stronghold). Messages scroll automatically (reka-ui ScrollArea).
+
+#### Scenario: Send message
+- **WHEN** user types a message and presses Enter in the chat input
+- **THEN** the message is sent to OpenRouter and a streaming response appears
+
+#### Scenario: Toggle chat
+- **WHEN** user presses ⌘J
+- **THEN** the properties panel switches to the AI tab (or back to Design)
+
+### Requirement: AI model selector
+The chat input SHALL include a model selector with curated models: Claude, Gemini, GPT, DeepSeek, Qwen, Kimi, Llama. Models are stored in @open-pencil/core constants with benchmark-ranked tags.
+
+#### Scenario: Change model
+- **WHEN** user selects a different model from the selector
+- **THEN** subsequent messages use the selected model
+
+### Requirement: AI tools
+The chat SHALL support 10 design tools with valibot schemas: create_shape, set_fill, set_stroke, update_node, set_layout, delete_node, select_nodes, get_page_tree, get_selection, rename_node. Tools execute against the editor store. The ToolLoopAgent calls tools automatically in a loop.
+
+#### Scenario: AI creates a shape
+- **WHEN** user asks "Create a blue rectangle 200×100"
+- **THEN** the AI calls create_shape with type RECTANGLE, width 200, height 100, and set_fill with blue color
+
+#### Scenario: Tool call visible in chat
+- **WHEN** the AI executes a tool
+- **THEN** a collapsible tool call section shows the tool name, input, and output
+
+### Requirement: DirectChatTransport
+The chat SHALL communicate directly with OpenRouter from the browser — no backend server required. The API key is stored securely in Tauri Stronghold (or localStorage fallback in browser). X-OpenRouter-Title header identifies the app.
+
+#### Scenario: No backend required
+- **WHEN** user configures an OpenRouter API key and sends a message
+- **THEN** the request goes directly to OpenRouter without a proxy server
+
+### Requirement: Code panel with JSX export
+The properties panel Code tab SHALL display the selected node(s) as JSX code using sceneNodeToJsx(). The panel provides Prism.js syntax highlighting, line numbers, and a copy-to-clipboard button. Multi-selection shows each node's JSX.
+
+#### Scenario: View JSX for selection
+- **WHEN** user selects a frame and clicks the Code tab
+- **THEN** the JSX representation of the frame and its children is displayed with syntax highlighting
+
+#### Scenario: Copy JSX
+- **WHEN** user clicks the copy button in the Code panel
+- **THEN** the JSX code is copied to the clipboard
diff --git a/openspec/specs/scene-graph/spec.md b/openspec/specs/scene-graph/spec.md
index c93338958..9dbae9496 100644
--- a/openspec/specs/scene-graph/spec.md
+++ b/openspec/specs/scene-graph/spec.md
@@ -263,3 +263,14 @@ SceneNode SHALL support a `styleRuns` array of `{start, length, style}` where st
#### Scenario: Render JSX string
- **WHEN** renderJsx('Hi') is called
- **THEN** a scene graph with a FRAME parent and TEXT child is produced
+
+### Requirement: Scene node to JSX export
+@open-pencil/core SHALL export a sceneNodeToJsx() function that converts a SceneNode subtree into JSX string using builder function syntax (Frame, Text, Rectangle, etc.) with Tailwind-like shorthand props. The output is valid JSX consumable by renderJsx().
+
+#### Scenario: Export frame with children
+- **WHEN** sceneNodeToJsx is called on a frame with a rectangle and text child
+- **THEN** a JSX string with Frame wrapping Rectangle and Text is returned
+
+#### Scenario: Export with effects
+- **WHEN** sceneNodeToJsx is called on a node with drop shadow
+- **THEN** the JSX includes shadow props in the output
diff --git a/openspec/specs/testing/spec.md b/openspec/specs/testing/spec.md
index 530fae253..29a4c1a5f 100644
--- a/openspec/specs/testing/spec.md
+++ b/openspec/specs/testing/spec.md
@@ -121,3 +121,17 @@ The test suite SHALL include tests for the JSX renderer covering all node types
#### Scenario: Render Frame with children
- **WHEN** a JSX tree with Frame containing Rectangle and Text is rendered
- **THEN** the scene graph contains correct parent-child relationships and properties
+
+### Requirement: AI chat Playwright tests
+The test suite SHALL include Playwright e2e tests for the AI chat panel with mock transport (no real LLM calls). Tests cover: sending a message, receiving a response, tool call display. Real LLM tests available via TEST_REAL_LLM=1 flag.
+
+#### Scenario: Chat with mock transport
+- **WHEN** a Playwright test sends a message via the chat input
+- **THEN** the mock transport returns a response and the message appears in the chat
+
+### Requirement: JSX export tests
+The test suite SHALL include tests for sceneNodeToJsx() covering shapes, text, layout props, effects, and multi-selection. Currently 14 tests.
+
+#### Scenario: Export rectangle to JSX
+- **WHEN** sceneNodeToJsx is called on a rectangle with blue fill
+- **THEN** the output includes Rectangle component with bg prop