From 4073efa8d1a1dfd841b6c2052161e964f266f788 Mon Sep 17 00:00:00 2001 From: Guilherme Rodrigues Date: Sat, 31 Jan 2026 23:17:29 -0300 Subject: [PATCH 01/60] Add HELLO_WORLD.md with greeting and explanation Co-Authored-By: Claude Opus 4.5 --- HELLO_WORLD.md | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 HELLO_WORLD.md diff --git a/HELLO_WORLD.md b/HELLO_WORLD.md new file mode 100644 index 0000000000..5867518383 --- /dev/null +++ b/HELLO_WORLD.md @@ -0,0 +1,44 @@ +# Hello, World! + +Welcome! I'm Claude, an AI assistant built by Anthropic. I'm here to help you with software engineering tasks in this codebase. + +## How I Work + +### Understanding Your Request +When you give me a task, I first work to understand what you need. I'll read relevant files, explore the codebase structure, and ask clarifying questions if something is unclear. + +### Planning and Tracking +For complex tasks, I break them down into smaller steps and track my progress using a todo list. This helps me stay organized and gives you visibility into what I'm doing. + +### Making Changes +I make changes incrementally and commit frequently. I prefer: +- **Reading before writing** - I always understand existing code before modifying it +- **Small, focused changes** - One logical change at a time +- **Using the right tools** - Specialized tools for file operations rather than shell commands when possible + +### Communication +I communicate directly with you through text responses. If I need more information or want to validate an approach, I'll ask. I aim to be concise and focus on technical accuracy. + +### Safety First +I follow safety practices: +- Commit frequently to preserve work +- Avoid destructive operations +- Ask when requirements are unclear +- Never introduce security vulnerabilities + +## What I Can Help With + +- **Bug fixing** - Finding and resolving issues in code +- **Feature development** - Implementing new functionality +- **Code exploration** - Understanding how parts of the codebase work +- **Refactoring** - Improving code structure and quality +- **Documentation** - Explaining code and writing docs +- **Testing** - Writing and running tests + +## Getting Started + +Just tell me what you'd like to accomplish, and I'll get to work. The more context you provide, the better I can help. + +--- + +*Built on Anthropic's Claude, powered by the Opus 4.5 model.* From 0a44db124994d05ede628cbbfd2763ded9f80ade Mon Sep 17 00:00:00 2001 From: Guilherme Rodrigues Date: Sat, 31 Jan 2026 23:27:30 -0300 Subject: [PATCH 02/60] docs: update HELLO_WORLD.md with project description Replace Claude-centric content with actual MCP Mesh project description, architecture diagram, tech stack, and quick start guide. Co-Authored-By: Claude Opus 4.5 --- .beads/config.json | 4 + .beads/sessions.json | 15 + .beads/tasks.json | 49 ++ .claude/settings.local.json | 12 + HELLO_WORLD.md | 89 +- SITE_BUILDER_PLAN.md | 677 +++++++++++++++ TASK_RUNNER_PLAN.md | 527 ++++++++++++ apps/mesh/e2e/.gitignore | 6 + apps/mesh/e2e/connections.spec.ts | 76 ++ apps/mesh/e2e/global.setup.ts | 95 +++ apps/mesh/package.json | 9 +- apps/mesh/playwright.config.ts | 48 ++ apps/mesh/src/web/components/chat/context.tsx | 88 +- apps/mesh/src/web/components/chat/index.tsx | 3 +- .../components/details/connection/index.tsx | 62 +- apps/mesh/src/web/index.tsx | 50 +- .../src/web/layouts/dynamic-plugin-layout.tsx | 10 +- apps/mesh/src/web/layouts/shell-layout.tsx | 34 +- apps/mesh/src/web/plugins.ts | 2 + packages/bindings/src/core/plugins.ts | 6 + packages/bindings/src/index.ts | 7 + .../bindings/src/well-known/task-runner.ts | 357 ++++++++ .../components/plugin-empty-state.tsx | 27 + .../components/plugin-header.tsx | 104 +++ .../components/task-board.tsx | 798 ++++++++++++++++++ .../hooks/use-tasks.ts | 473 +++++++++++ packages/mesh-plugin-task-runner/index.tsx | 41 + .../mesh-plugin-task-runner/lib/query-keys.ts | 18 + .../mesh-plugin-task-runner/lib/router.ts | 37 + packages/mesh-plugin-task-runner/package.json | 20 + .../mesh-plugin-task-runner/tsconfig.json | 8 + skills/README.md | 31 + skills/mesh-development/SKILL.md | 174 ++++ .../references/design-tokens.md | 123 +++ .../references/ui-components.md | 190 +++++ 35 files changed, 4197 insertions(+), 73 deletions(-) create mode 100644 .beads/config.json create mode 100644 .beads/sessions.json create mode 100644 .beads/tasks.json create mode 100644 .claude/settings.local.json create mode 100644 SITE_BUILDER_PLAN.md create mode 100644 TASK_RUNNER_PLAN.md create mode 100644 apps/mesh/e2e/.gitignore create mode 100644 apps/mesh/e2e/connections.spec.ts create mode 100644 apps/mesh/e2e/global.setup.ts create mode 100644 apps/mesh/playwright.config.ts create mode 100644 packages/bindings/src/well-known/task-runner.ts create mode 100644 packages/mesh-plugin-task-runner/components/plugin-empty-state.tsx create mode 100644 packages/mesh-plugin-task-runner/components/plugin-header.tsx create mode 100644 packages/mesh-plugin-task-runner/components/task-board.tsx create mode 100644 packages/mesh-plugin-task-runner/hooks/use-tasks.ts create mode 100644 packages/mesh-plugin-task-runner/index.tsx create mode 100644 packages/mesh-plugin-task-runner/lib/query-keys.ts create mode 100644 packages/mesh-plugin-task-runner/lib/router.ts create mode 100644 packages/mesh-plugin-task-runner/package.json create mode 100644 packages/mesh-plugin-task-runner/tsconfig.json create mode 100644 skills/README.md create mode 100644 skills/mesh-development/SKILL.md create mode 100644 skills/mesh-development/references/design-tokens.md create mode 100644 skills/mesh-development/references/ui-components.md diff --git a/.beads/config.json b/.beads/config.json new file mode 100644 index 0000000000..86816a290b --- /dev/null +++ b/.beads/config.json @@ -0,0 +1,4 @@ +{ + "version": "1.0.0", + "created": "2026-02-01T01:10:40.594Z" +} diff --git a/.beads/sessions.json b/.beads/sessions.json new file mode 100644 index 0000000000..ccd03c9d23 --- /dev/null +++ b/.beads/sessions.json @@ -0,0 +1,15 @@ +{ + "sessions": [ + { + "id": "session-ml341b16-br4tdn", + "taskId": "task-1769912212526-a6dpne", + "pid": 93856, + "status": "completed", + "startedAt": "2026-02-01T02:16:57.127Z", + "output": "The commit was successful. The task is complete - I've created `HELLO_WORLD.md` with a greeting and explanation of how I work.\n\nCOMPLETE\n", + "exitCode": 0, + "completedAt": "2026-02-01T02:17:32.782Z" + } + ], + "lastUpdated": "2026-02-01T02:17:32.782Z" +} diff --git a/.beads/tasks.json b/.beads/tasks.json new file mode 100644 index 0000000000..5f917b56fd --- /dev/null +++ b/.beads/tasks.json @@ -0,0 +1,49 @@ +{ + "tasks": [ + { + "id": "task-1769911249988-h0rqne", + "title": "write HELLO_WORLD.md with a greeting and explanation of how you work", + "description": "Follow the instructions in skills/mesh-development/SKILL.md", + "status": "closed", + "createdAt": "2026-02-01T02:00:49.988Z", + "updatedAt": "2026-02-01T02:02:55.381Z" + }, + { + "id": "task-1769911381482-byaa8d", + "title": "write HELLO_WORLD.md with a greeting and explanation of how you work", + "description": "Follow the instructions in skills/mesh-development/SKILL.md", + "status": "closed", + "createdAt": "2026-02-01T02:03:01.482Z", + "updatedAt": "2026-02-01T02:04:29.518Z" + }, + { + "id": "task-1769911815986-pntok3", + "title": "write HELLO_WORLD.md with a greeting and explanation of how you work", + "description": "Follow the instructions in skills/mesh-development/SKILL.md", + "status": "in_progress", + "createdAt": "2026-02-01T02:10:15.986Z", + "updatedAt": "2026-02-01T02:10:17.852Z" + }, + { + "id": "task-1769911952970-mdlzs3", + "title": "write HELLO_WORLD.md with a greeting and explanation of how you work", + "status": "in_progress", + "createdAt": "2026-02-01T02:12:32.970Z", + "updatedAt": "2026-02-01T02:12:34.201Z" + }, + { + "id": "task-1769912180028-9lzcsw", + "title": "write HELLO_WORLD.md with a greeting and explanation of how you work", + "status": "in_progress", + "createdAt": "2026-02-01T02:16:20.028Z", + "updatedAt": "2026-02-01T02:16:21.293Z" + }, + { + "id": "task-1769912212526-a6dpne", + "title": "write HELLO_WORLD.md with a greeting and explanation of how you work", + "status": "in_progress", + "createdAt": "2026-02-01T02:16:52.526Z", + "updatedAt": "2026-02-01T02:16:53.612Z" + } + ] +} diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000000..ccd21520aa --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,12 @@ +{ + "permissions": { + "allow": [ + "Bash(gh pr view:*)", + "Bash(git ls-tree:*)", + "Bash(git log:*)", + "Bash(bun run build:*)", + "Bash(bun run check)", + "Bash(git checkout:*)" + ] + } +} diff --git a/HELLO_WORLD.md b/HELLO_WORLD.md index 5867518383..a625b0a52d 100644 --- a/HELLO_WORLD.md +++ b/HELLO_WORLD.md @@ -1,44 +1,75 @@ # Hello, World! -Welcome! I'm Claude, an AI assistant built by Anthropic. I'm here to help you with software engineering tasks in this codebase. +Welcome to **MCP Mesh** - an open-source control plane for Model Context Protocol (MCP) traffic. -## How I Work +## What We're Building -### Understanding Your Request -When you give me a task, I first work to understand what you need. I'll read relevant files, explore the codebase structure, and ask clarifying questions if something is unclear. +MCP Mesh is a full-stack platform for orchestrating MCP connections, tools, and AI agents. It sits between MCP clients (Cursor, Claude, VS Code, custom agents) and MCP servers, providing a unified layer for authentication, routing, and observability. -### Planning and Tracking -For complex tasks, I break them down into smaller steps and track my progress using a todo list. This helps me stay organized and gives you visibility into what I'm doing. +### The Problem We Solve -### Making Changes -I make changes incrementally and commit frequently. I prefer: -- **Reading before writing** - I always understand existing code before modifying it -- **Small, focused changes** - One logical change at a time -- **Using the right tools** - Specialized tools for file operations rather than shell commands when possible +Without MCP Mesh, you have M×N integrations: M MCP servers × N clients, each requiring separate configs. MCP Mesh replaces this complexity with one production endpoint, so you stop maintaining separate configurations in every client. -### Communication -I communicate directly with you through text responses. If I need more information or want to validate an approach, I'll ask. I aim to be concise and focus on technical accuracy. +### Core Capabilities -### Safety First -I follow safety practices: -- Commit frequently to preserve work -- Avoid destructive operations -- Ask when requirements are unclear -- Never introduce security vulnerabilities +- **Virtual MCPs** - Runtime strategies for optimal tool selection +- **Access Control** - Fine-grained RBAC via OAuth 2.1 + API keys +- **Multi-tenancy** - Workspace/project isolation for configs, credentials, and logs +- **Observability** - Full tracing with OpenTelemetry +- **Token Vault** - Secure credential management +- **Event Bus** - Pub/sub between connections with at-least-once delivery -## What I Can Help With +## Architecture -- **Bug fixing** - Finding and resolving issues in code -- **Feature development** - Implementing new functionality -- **Code exploration** - Understanding how parts of the codebase work -- **Refactoring** - Improving code structure and quality -- **Documentation** - Explaining code and writing docs -- **Testing** - Writing and running tests +``` +┌─────────────────────────────────────────────────────────────────┐ +│ MCP Clients │ +│ Cursor · Claude · VS Code · Custom Agents │ +└───────────────────────────┬─────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ MCP MESH │ +│ Virtual MCP · Policy Engine · Observability · Token Vault │ +└───────────────────────────┬─────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ MCP Servers │ +│ Salesforce · Slack · GitHub · Postgres · Your APIs │ +└─────────────────────────────────────────────────────────────────┘ +``` -## Getting Started +## Tech Stack -Just tell me what you'd like to accomplish, and I'll get to work. The more context you provide, the better I can help. +| Layer | Technology | +|-------|------------| +| Runtime | Bun / Node | +| Language | TypeScript + Zod | +| Framework | Hono (API) + Vite + React 19 | +| Database | Kysely → SQLite / PostgreSQL | +| Auth | Better Auth (OAuth 2.1 + API keys) | +| Observability | OpenTelemetry | +| UI | React 19 + Tailwind v4 + shadcn | +| Protocol | Model Context Protocol (MCP) | + +## Quick Start + +```bash +# Clone and install +git clone https://github.com/decocms/mesh.git +bun install + +# Run locally +bun run dev +``` + +Open `http://localhost:4000` to access the admin UI. + +## Part of deco CMS + +MCP Mesh is the infrastructure layer of [decoCMS](https://decocms.com), providing the foundation for connecting, governing, and observing MCP traffic. --- -*Built on Anthropic's Claude, powered by the Opus 4.5 model.* +*One secure endpoint for every MCP server.* diff --git a/SITE_BUILDER_PLAN.md b/SITE_BUILDER_PLAN.md new file mode 100644 index 0000000000..5100ee308a --- /dev/null +++ b/SITE_BUILDER_PLAN.md @@ -0,0 +1,677 @@ +# Site Builder Plugin for Mesh + +## Overview + +Stack-agnostic Mesh plugin for orchestrating AI agents to build sites with live preview, using Beads for task storage and Ralph-style loops for autonomous execution until budget exhaustion or completion. + +## Todos + +- [ ] Create site-builder MCP with site control, Beads integration, skills, and Ralph loop engine +- [ ] Create mesh-plugin-site-builder with folder picker, site list/detail, and preview panel +- [ ] Integrate Beads (bd CLI) for git-backed task storage with dependency graph +- [ ] Implement Ralph-style execution loop with budget control and streaming progress +- [ ] Create page block first, stream components incrementally with HMR refresh +- [ ] Bundle skills directly in MCP with good out-of-box prompts +- [ ] Auto-detect site stack (Deno/deco.json, Node/package.json) and adapt commands + +## Vision + +Control any site from Mesh: select a folder, mount it as a site, describe what you want (or pick a skill), and agents build it iteratively with live streaming preview until complete or budget exhausted. Stack-agnostic, skill-driven, closed-loop. + +## Key Design Decisions + +**Why Beads (not Drover)?** + +- [Beads](https://github.com/steveyegge/beads) = Git-backed task storage with dependency graph ("memory") +- [Ralph TUI](https://github.com/subsy/ralph-tui) pattern = Execution loop (SELECT -> PROMPT -> EXECUTE -> EVALUATE) +- Drover adds parallel git worktrees which is overkill for sequential page building +- Beads + Ralph pattern = simpler, git-native, works with existing Mesh architecture + +**Stack-Agnostic Approach:** + +- Detect stack from config files (deno.json, package.json, etc.) +- Skills define stack-specific commands (start, build, check) +- Start with Deco stack, add more via new skills + +## Learnings from Ralph TUI and Beads + +### Ralph TUI Patterns to Adopt + +From [Ralph TUI docs](https://ralph-tui.com/docs): + +1. **Autonomous Loop**: `SELECT → BUILD PROMPT → EXECUTE → DETECT COMPLETION` +2. **Completion Detection**: Agent outputs `COMPLETE` when done +3. **Quality Gates**: Commands that must pass (e.g., `deno task check`, `deno task build`) +4. **PRD Format**: User stories with acceptance criteria: + ```markdown + ### US-001: Create hero section + **As a** visitor + **I want** a compelling hero section + **So that** I understand the value proposition + + #### Acceptance Criteria + - [ ] Hero component renders at /pitch/company + - [ ] Headline matches company value prop + - [ ] CTA button links to contact form + ``` +5. **Subagent Tracing**: Shows nested agent calls in tree hierarchy +6. **Session Persistence**: Resume interrupted work + +### Beads Integration Patterns + +From [Beads AGENT_INSTRUCTIONS.md](https://github.com/steveyegge/beads/blob/main/AGENT_INSTRUCTIONS.md): + +1. **Key Commands**: + - `bd init` - Initialize in project + - `bd ready` - Get tasks with no blockers + - `bd create "Title" -p 1` - Create P1 task + - `bd update --status in_progress` + - `bd close --reason "Completed"` + - `bd sync` - Force immediate export/commit/push + +2. **Hierarchical IDs**: `bd-a3f8` (Epic) → `bd-a3f8.1` (Task) → `bd-a3f8.1.1` (Sub-task) + +3. **Landing the Plane**: When work is done: + ```bash + bd close --reason "Completed" + bd sync + git push + ``` + +4. **Auto-Sync**: 30-second debounce, exports to `.beads/issues.jsonl` + +5. **Agent Workflow**: Make changes → `bd sync` at end of session + +### Beads UI Inspiration + +From [Beads UI](https://github.com/mantoni/beads-ui): + +- **Views**: Issues (filter/search), Epics (progress), Board (Blocked/Ready/In Progress/Closed) +- **Live Updates**: Monitors `.beads/` for changes +- **Multi-workspace**: Switch between projects via dropdown + +### Quality Gates for Deco Stack + +```toml +# Quality gates that must pass +[quality_gates] +commands = [ + "deno task check", # Lint + typecheck + "deno fmt --check", # Format check +] +``` + +### Completion Detection + +Agents must output this token when task is complete: +``` +COMPLETE +``` + +If not output, Ralph loop continues (agent thinks it's not done). + +## Architecture Overview + +```mermaid +flowchart TB + subgraph MeshUI [Mesh UI - React] + Chat[Chat Panel - right] + SitePanel[Site Panel - middle] + Preview[Preview iframe] + end + + subgraph SitePanel + FolderPicker[Folder Picker] + SiteList[Site List] + SiteDetail[Site Detail View] + TaskBoard[Task Board - Beads] + end + + subgraph SiteBuilderMCP [Site Builder MCP] + SiteTools[Site Control] + BeadsTools[Beads Integration] + SkillsBundle[Bundled Skills] + RalphEngine[Ralph Loop Engine] + end + + subgraph LocalMachine [Local Machine] + BeadsCLI[bd CLI] + DevServer[Dev Server] + FileSystem[Project Files] + end + + Chat -->|"user prompts"| SiteBuilderMCP + SitePanel --> SiteBuilderMCP + SiteBuilderMCP --> BeadsCLI + SiteBuilderMCP --> DevServer + RalphEngine -->|"file edits"| FileSystem + RalphEngine -->|"streaming progress"| SitePanel + Preview -->|"HMR"| DevServer +``` + +## UI Layout + +Uses existing Mesh chat on the right. Plugin controls the middle panel only. + +``` ++------------------------------------------+------------------+ +| MIDDLE PANEL | RIGHT PANEL | +| (Site Builder Plugin) | (Mesh Chat) | ++------------------------------------------+ | +| Sites [+ Add Folder] | | +| ┌────────────────────────────────────┐ | Normal Mesh | +| │ > decocms/ [Running :8000] │ | chat interface | +| │ vibegui.com/ [Stopped] │ | talks to MCP | +| │ my-site/ [Stopped] │ | | +| └────────────────────────────────────┘ | | ++------------------------------------------+ | +| Site: decocms/ | | +| ┌─────────────────────────────────────┐ | | +| │ Preview [/pitch/good-american ▼] │ | | +| │ ┌─────────────────────────────────┐ │ | | +| │ │ │ │ | | +| │ │ Live Preview (iframe) │ │ | | +| │ │ localhost:8000 │ │ | | +| │ │ │ │ | | +| │ └─────────────────────────────────┘ │ | | +| └─────────────────────────────────────┘ | | +| Tasks (Beads) Budget: $2.50 | | +| ┌─────────────────────────────────────┐ | | +| │ ○ bd-a3f8 Create hero section [3] │ | | +| │ ◐ bd-a3f8.1 Add metrics [1] │ | | +| │ ● bd-a3f8.2 Add CTA done │ | | +| └─────────────────────────────────────┘ | | +| [+ New Task] [Select Skill ▼] [Run All] | | ++------------------------------------------+------------------+ +``` + +## Component Breakdown + +### 1. Site Builder MCP Server + +New MCP in `mcps/site-builder/` - stack-agnostic with bundled skills. + +**Site Control Tools:** +- `SITE_MOUNT` - Register a folder as a site (auto-detect stack) +- `SITE_UNMOUNT` - Remove site from list +- `SITE_LIST` - List mounted sites with status +- `SITE_START` - Start dev server for site (stack-aware command) +- `SITE_STOP` - Stop dev server +- `SITE_STATUS` - Get running state, port, URL +- `SITE_PAGES` - List available pages/routes for preview dropdown + +**Beads Integration Tools:** +- `BEADS_INIT` - Initialize Beads in project (`bd init`) +- `BEADS_READY` - Get tasks with no blockers (`bd ready`) +- `BEADS_CREATE` - Create task with dependencies (`bd create`) +- `BEADS_UPDATE` - Update task status +- `BEADS_SHOW` - Get task details +- `BEADS_LIST` - List all tasks in tree format + +**Skill Tools:** +- `SKILL_LIST` - List bundled skills +- `SKILL_GET` - Get skill details and prompts +- `SKILL_RUN` - Execute skill workflow (creates Beads tasks + runs Ralph loop) + +**Ralph Loop Engine:** +- `LOOP_START` - Begin execution loop with budget +- `LOOP_STATUS` - Get current iteration, spend, progress +- `LOOP_PAUSE` - Pause execution +- `LOOP_RESUME` - Resume execution +- `LOOP_STOP` - Stop and cleanup + +### 2. Mesh Plugin (Client) + +New plugin in `packages/mesh-plugin-site-builder/`. + +**Views:** +- **Site List View** - Folder picker, mounted sites, status indicators +- **Site Detail View** - Preview iframe, page dropdown, task board, controls + +**Key Components:** +- `folder-picker.tsx` - Native folder selection dialog +- `site-list.tsx` - List of mounted sites with status +- `site-detail.tsx` - Main detail view container +- `preview-frame.tsx` - Resizable iframe with URL bar and page dropdown +- `task-board.tsx` - Beads task visualization (inspired by beads-ui) +- `budget-control.tsx` - Set and monitor agent budget +- `skill-selector.tsx` - Dropdown to pick skill workflow + +### 3. Streaming Page Creation + +Critical for UX - show progress as it happens. + +**Flow:** +``` +1. User selects "Landing Page" skill, provides brief +2. MCP creates page JSON with placeholder: + { + "path": "/pitch/new-page", + "sections": [{ + "__resolveType": "site/sections/Placeholder.tsx", + "message": "Deco Pilot is building your page..." + }] + } +3. Preview iframe loads placeholder page +4. As each section is built: + a. Agent creates section component (if new) + b. Agent updates page JSON to add section + c. HMR triggers -> preview updates automatically +5. Placeholder replaced incrementally as real sections appear +``` + +**HMR Optimization:** + +- Deco's Fresh framework has built-in HMR +- If slow, we can add WebSocket push from MCP to force iframe reload +- Section-level granularity (each section addition triggers update) + +### 4. Ralph Loop Engine + +Based on [Ralph TUI](https://ralph-tui.com/docs) patterns. Runs in MCP server. + +```mermaid +flowchart LR + SELECT[SELECT\nbd ready] --> PROMPT[PROMPT\nBuild from template] + PROMPT --> EXECUTE[EXECUTE\nCall Claude] + EXECUTE --> DETECT[DETECT\npromise COMPLETE?] + DETECT -->|"no token"| GATES[GATES\nRun quality gates] + GATES -->|"fail"| SELECT + DETECT -->|"COMPLETE"| CLOSE[CLOSE\nbd close + sync] + GATES -->|"pass"| CLOSE + CLOSE --> NEXT[NEXT\nbd ready] + NEXT -->|"more tasks"| PROMPT + NEXT -->|"done/budget"| FINISHED[FINISHED] +``` + +**Completion Detection:** +Agent outputs `COMPLETE` when task is done: + +```typescript +function detectCompletion(output: string): boolean { + return output.includes("COMPLETE"); +} +``` + +**Quality Gates (per stack):** +```typescript +const DECO_QUALITY_GATES = [ + "deno task check", // Lint + typecheck + "deno fmt --check", // Format verification +]; +``` + +**Budget Control:** + +- Set max iterations (e.g., 10) or max spend (e.g., $5.00) +- Track token usage per iteration +- Auto-pause at budget threshold +- Events: `loop.iteration`, `loop.budget_warning`, `loop.budget_exhausted` + +**State Management:** + +- Beads stores task graph (git-backed, `.beads/issues.jsonl`) +- Loop state: `{ iteration, currentTask, totalSpend, status }` +- Progress streamed via Mesh event bus + +**Prompt Template (Handlebars):** +```handlebars +You are building a {{skill.name}} for {{site.name}}. + +## Current Task +**{{task.title}}** ({{task.id}}) +{{task.description}} + +## Acceptance Criteria +{{#each task.acceptance_criteria}} +- [ ] {{this}} +{{/each}} + +## Quality Gates +{{#each quality_gates}} +- `{{this}}` +{{/each}} + +When complete, output: COMPLETE +``` + +### 5. Bundled Skills + +Skills live inside the MCP, exposed as MCP resources. Format follows Ralph TUI's PRD pattern. + +**Skill Format (PRD-style):** + +```typescript +interface Skill { + id: string; + name: string; + description: string; + stack: string[]; // ["deco", "fresh"] or ["*"] for any + + // PRD-style user stories + userStories: UserStory[]; + + // Quality gates per stack + qualityGates: Record; + + // Prompts + prompts: { + system: string; // Stack context and role + taskTemplate: string; // Handlebars template for each task + acceptanceCriteria: string; + }; +} + +interface UserStory { + id: string; // US-001, US-002, etc. + title: string; + asA: string; // User role + iWant: string; // Capability + soThat: string; // Benefit + acceptanceCriteria: string[]; + dependsOn?: string[]; // Story dependencies +} +``` + +**Example Skill - Landing Page (Deco):** + +```typescript +export const landingPageDeco: Skill = { + id: "landing-page-deco", + name: "Landing Page", + description: "Create a landing page with hero, features, and CTA", + stack: ["deco"], + + userStories: [ + { + id: "US-001", + title: "Create page JSON and placeholder", + asA: "developer", + iWant: "an empty page at the target URL", + soThat: "I can see progress as sections are added", + acceptanceCriteria: [ + "Page JSON exists at .deco/blocks/pages-{slug}.json", + "Page loads at /{slug} with placeholder section", + "No console errors in browser" + ] + }, + { + id: "US-002", + title: "Add hero section", + asA: "visitor", + iWant: "a compelling hero section", + soThat: "I understand the value proposition immediately", + acceptanceCriteria: [ + "Hero section renders with headline, subheadline, CTA", + "Responsive on mobile and desktop", + "Page JSON updated with hero section" + ], + dependsOn: ["US-001"] + }, + // ... more stories + ], + + qualityGates: { + deco: ["deno task check", "deno fmt --check"] + }, + + prompts: { + system: `You are an expert Deco developer building landing pages. +Key patterns: +- Sections live in sections/{Feature}/*.tsx +- Page configs are JSON in .deco/blocks/pages-*.json +- Use __resolveType: "site/sections/..." to reference sections +- Props use JSDoc comments for CMS integration`, + + taskTemplate: `...`, // Handlebars template + acceptanceCriteria: `When all criteria are met, output: COMPLETE` + } +}; +``` + +**Initial Skills:** + +| Skill ID | Name | Description | +|----------|------|-------------| +| `landing-page-deco` | Landing Page | Hero, features, CTA sections | +| `sales-pitch-page` | Sales Pitch | Research + personalized pitch (from deco-sales-pitch-pages skill) | +| `blog-post-deco` | Blog Post | SEO-optimized blog post with schema | +| `section-component` | New Section | Create a new reusable section component | + +**Skill as MCP Resource:** + +Skills are exposed via MCP resources so Mesh can list and display them: + +```typescript +server.resource("skill://landing-page-deco", async () => ({ + contents: [{ + uri: "skill://landing-page-deco", + mimeType: "application/json", + text: JSON.stringify(landingPageDeco) + }] +})); +``` + +### 6. Stack Detection + +Auto-detect and adapt to project stack. + +**Detection Logic:** + +```typescript +function detectStack(dir: string): Stack { + if (exists(join(dir, "deno.json"))) { + const config = readJson("deno.json"); + if (config.imports?.["deco/"]) return "deco"; + if (config.imports?.["$fresh/"]) return "fresh"; + return "deno"; + } + if (exists(join(dir, "package.json"))) { + const pkg = readJson("package.json"); + if (pkg.dependencies?.next) return "nextjs"; + if (pkg.dependencies?.astro) return "astro"; + return "node"; + } + return "unknown"; +} +``` + +**Stack Configurations:** + +```typescript +const STACK_CONFIGS = { + deco: { + startCmd: "deno task dev", + checkCmd: "deno task check", + pagesGlob: ".deco/blocks/pages-*.json", + sectionsDir: "sections/", + }, + nextjs: { + startCmd: "npm run dev", + checkCmd: "npm run lint && npm run typecheck", + pagesGlob: "app/**/page.tsx", + componentsDir: "components/", + }, + // ... more stacks +}; +``` + +## Agent Execution Strategy + +**How agents are called:** + +Ralph TUI calls agent CLIs (claude, opencode, etc.) as subprocesses. We have two options: + +### Option A: Call Claude Code CLI (like Ralph TUI) + +```typescript +// Spawn claude CLI with prompt piped to stdin +const proc = spawn("claude", ["-p"], { + cwd: siteDir, + env: { ...process.env, ANTHROPIC_API_KEY: key } +}); +proc.stdin.write(prompt); +proc.stdin.end(); + +// Stream stdout to UI +proc.stdout.on("data", (chunk) => { + eventBus.publish("agent.output", { chunk: chunk.toString() }); + // Check for completion token + if (chunk.includes("COMPLETE")) { + // Task complete + } +}); +``` + +**Pros:** Full Claude Code capabilities (tool use, file editing) +**Cons:** Requires Claude Code CLI installed, harder to control + +### Option B: Use Mesh Thread/Chat Infrastructure (Recommended) + +Mesh already has thread infrastructure. Create a thread per task, use existing LLM routing: + +```typescript +// Create thread for task +const thread = await createThread({ + name: `Task: ${task.title}`, + siteId: site.id, + taskId: task.id +}); + +// Send prompt as message, get streaming response +const response = await sendMessage(thread.id, prompt, { + stream: true, + tools: ["file_read", "file_write", "shell"] // MCP tools for file ops +}); + +// Stream to UI +for await (const chunk of response) { + eventBus.publish("agent.output", { threadId: thread.id, chunk }); +} +``` + +**Pros:** Uses existing Mesh infrastructure, better control, token tracking built-in +**Cons:** Need to ensure file editing tools work in site directory + +### Recommendation: Hybrid Approach + +1. **Phase 1**: Use Claude Code CLI for proven file editing capabilities +2. **Phase 2**: Add Mesh thread option for better integration and budget tracking +3. Let user choose via config: `agent: "claude-cli" | "mesh-thread"` + +## File Structure + +``` +mcps/site-builder/ +├── package.json +├── src/ +│ ├── index.ts # MCP server entry +│ ├── tools/ +│ │ ├── site.ts # SITE_* tools +│ │ ├── beads.ts # BEADS_* tools (wraps bd CLI) +│ │ ├── skills.ts # SKILL_* tools +│ │ └── loop.ts # LOOP_* tools +│ ├── engine/ +│ │ ├── ralph-loop.ts # Ralph execution loop +│ │ ├── budget.ts # Budget tracking +│ │ └── streaming.ts # Progress streaming +│ ├── stacks/ +│ │ ├── detector.ts # Stack detection +│ │ ├── deco.ts # Deco-specific logic +│ │ ├── nextjs.ts # Next.js logic +│ │ └── index.ts # Stack registry +│ ├── skills/ +│ │ ├── landing-page-deco.ts +│ │ ├── sales-pitch-page.ts +│ │ ├── blog-post.ts +│ │ └── index.ts # Skill registry +│ └── process/ +│ └── dev-server.ts # Process spawning/management + +mesh/packages/mesh-plugin-site-builder/ +├── package.json +├── index.tsx # Plugin definition +├── components/ +│ ├── folder-picker.tsx +│ ├── site-list.tsx +│ ├── site-detail.tsx +│ ├── preview-frame.tsx +│ ├── task-board.tsx +│ ├── budget-control.tsx +│ └── skill-selector.tsx +└── lib/ + ├── router.ts + └── binding.ts # SITE_BUILDER_BINDING +``` + +## Implementation Phases + +### Phase 1: Core Infrastructure (Week 1) + +**Deliverables:** +- [ ] Site Builder MCP scaffold in `mcps/site-builder/` +- [ ] Site control tools: `SITE_MOUNT`, `SITE_START`, `SITE_STOP`, `SITE_STATUS` +- [ ] Basic Mesh plugin with sidebar item +- [ ] Folder picker component (native dialog) +- [ ] Site list view with status indicators +- [ ] Preview iframe with URL input +- [ ] Stack detection for Deco (`deno.json` with `deco/` import) + +**Integration Points:** +- MCP registered in Mesh connection list +- Plugin filters connections by SITE_BUILDER_BINDING + +### Phase 2: Beads + Task Management (Week 2) + +**Deliverables:** +- [ ] Beads CLI wrapper tools: `BEADS_INIT`, `BEADS_READY`, `BEADS_CREATE`, `BEADS_CLOSE`, `BEADS_SYNC` +- [ ] Task board component (Blocked / Ready / In Progress / Done columns) +- [ ] Skill selector dropdown with 2 bundled skills +- [ ] Page dropdown for preview (from SITE_PAGES tool) +- [ ] Basic event streaming for task updates + +**Integration Points:** +- `bd` CLI installed as dependency or expected on PATH +- Events via Mesh event bus: `task.created`, `task.updated`, `task.closed` + +### Phase 3: Ralph Loop + Streaming (Week 3) + +**Deliverables:** +- [ ] Ralph loop engine: `LOOP_START`, `LOOP_STATUS`, `LOOP_PAUSE`, `LOOP_STOP` +- [ ] Completion detection (`COMPLETE`) +- [ ] Quality gates execution +- [ ] Budget control UI (set limit, show spend) +- [ ] Streaming page creation (placeholder first, then incremental sections) +- [ ] Agent output streaming to UI + +**Integration Points:** +- Agent execution via Claude Code CLI or Mesh threads +- HMR triggers on file changes (Deco's Fresh handles this) + +### Phase 4: Polish + Multi-Stack (Week 4+) + +**Deliverables:** +- [ ] Full skill library (sales-pitch, blog-post, component) +- [ ] Subagent tracing UI (like Ralph TUI's tree view) +- [ ] Next.js stack support +- [ ] Custom stack configuration +- [ ] Iteration history and logs + +## Success Criteria + +1. Mount any Deco folder as a site from Mesh +2. Start dev server and see preview in iframe +3. Select "Landing Page" skill, provide brief +4. Watch tasks created in Beads, agents working +5. See page build incrementally in preview (streaming) +6. Budget controls pause before overspend +7. Get notification when complete +8. All work persisted in `.beads/` (git-backed) + +## References + +- [Ralph TUI](https://ralph-tui.com/) - AI Agent Loop Orchestrator +- [Ralph TUI GitHub](https://github.com/subsy/ralph-tui) +- [Beads](https://github.com/steveyegge/beads) - Git-backed issue tracker for AI agents +- [Beads UI](https://github.com/mantoni/beads-ui) - Local web UI for Beads +- [Drover](https://github.com/cloud-shuttle/drover) - Parallel workflow orchestrator (reference only) diff --git a/TASK_RUNNER_PLAN.md b/TASK_RUNNER_PLAN.md new file mode 100644 index 0000000000..7caa2f3daf --- /dev/null +++ b/TASK_RUNNER_PLAN.md @@ -0,0 +1,527 @@ +# Task Runner: Self-Hosted Agent Orchestration for Mesh + +## Vision + +Extract the core agent orchestration (Beads + Ralph loop) into a **generic Task Runner MCP** that can be used to build ANY feature - including itself. Bootstrap the system: use Task Runner to build Site Builder, then use Site Builder to build landing pages. + +**Goal:** Make closed-loop agent development the normal workflow for the team. + +## Separation of Concerns + +```mermaid +flowchart TB + subgraph Layer1 [Layer 1: Core Orchestration - Generic] + TaskRunnerMCP[Task Runner MCP] + TaskRunnerPlugin[Task Runner Plugin] + end + + subgraph Layer2 [Layer 2: Domain-Specific - Site Building] + SiteBuilderMCP[Site Builder MCP] + SiteBuilderPlugin[Site Builder Plugin] + end + + subgraph Uses [What They're Used For] + BuildMesh[Build Mesh Features] + BuildSites[Build Deco Sites] + end + + TaskRunnerMCP -->|"provides"| BeadsIntegration[Beads Integration] + TaskRunnerMCP -->|"provides"| RalphLoop[Ralph Loop Engine] + TaskRunnerMCP -->|"provides"| AgentExecution[Agent Execution] + TaskRunnerMCP -->|"provides"| BudgetControl[Budget Control] + + SiteBuilderMCP -->|"uses"| TaskRunnerMCP + SiteBuilderMCP -->|"adds"| SiteControl[Site Control] + SiteBuilderMCP -->|"adds"| Preview[Preview/HMR] + SiteBuilderMCP -->|"adds"| SiteSkills[Site Skills] + + TaskRunnerPlugin --> BuildMesh + SiteBuilderPlugin --> BuildSites + SiteBuilderPlugin -->|"extends"| TaskRunnerPlugin +``` + +## The Two MCPs + +### 1. Task Runner MCP (Generic) + +Location: `mcps/task-runner/` + +**What it does:** +- Beads CLI integration (task storage, dependencies) +- Ralph-style execution loop +- Agent calling (Claude CLI or API) +- Budget/iteration control +- Skill loading and execution +- Progress streaming + +**Tools:** +```typescript +// Workspace management +WORKSPACE_SET // Set working directory for all operations +WORKSPACE_GET // Get current workspace + +// Beads integration +BEADS_INIT // bd init +BEADS_READY // bd ready --json +BEADS_CREATE // bd create "title" -p N +BEADS_UPDATE // bd update --status +BEADS_CLOSE // bd close --reason +BEADS_SYNC // bd sync +BEADS_LIST // bd list --tree --json + +// Skills +SKILL_LIST // List available skills +SKILL_GET // Get skill details +SKILL_REGISTER // Register a skill from file or inline + +// Loop control +LOOP_START // Start Ralph loop with config +LOOP_STATUS // Get current iteration, task, spend +LOOP_PAUSE // Pause execution +LOOP_RESUME // Resume execution +LOOP_STOP // Stop and cleanup + +// Agent execution +AGENT_PROMPT // Send prompt to agent, get streaming response +AGENT_STATUS // Check if agent is running +``` + +**This MCP knows nothing about sites, previews, or stacks.** It's pure task orchestration. + +### 2. Site Builder MCP (Domain-Specific) + +Location: `mcps/site-builder/` + +**What it does:** +- Site/folder management +- Dev server control (start, stop) +- Stack detection (Deco, Next.js, etc.) +- Preview URL management +- Site-specific skills + +**Tools:** +```typescript +// Site control +SITE_MOUNT // Register folder as site +SITE_UNMOUNT // Remove site +SITE_LIST // List sites with status +SITE_START // Start dev server +SITE_STOP // Stop dev server +SITE_STATUS // Get running state, port, URL +SITE_PAGES // List available pages for preview + +// Stack +STACK_DETECT // Detect stack from config files +STACK_CONFIG // Get stack-specific commands +``` + +**This MCP depends on Task Runner MCP for orchestration.** Skills registered here are site-specific. + +## The Two Plugins + +### 1. Task Runner Plugin (Generic) + +Location: `packages/mesh-plugin-task-runner/` + +**UI Components:** +- **Workspace Picker** - Select any folder to work on +- **Task Board** - Beads visualization (Blocked/Ready/In Progress/Done) +- **Skill Selector** - Pick or create skills +- **Agent Output Panel** - Streaming logs from agent +- **Budget Control** - Set limits, see spend +- **Loop Controls** - Start/Pause/Stop + +**Layout:** +``` ++------------------------------------------+------------------+ +| TASK RUNNER PLUGIN | MESH CHAT | ++------------------------------------------+ | +| Workspace: /Users/gui/Projects/mesh | | +| [Change Workspace] | | ++------------------------------------------+ | +| Skills: [Select Skill ▼] [+ New Skill] | | ++------------------------------------------+ | +| Tasks (Beads) Budget: $5/$10 | | +| ┌─────────────────────────────────────┐ | | +| │ BLOCKED READY PROGRESS DONE │ | | +| │ ┌──────┐ ┌──────┐ ┌──────┐ ┌───┐ │ | | +| │ │bd-01 │ │bd-02 │ │bd-03 │ │...│ │ | | +| │ └──────┘ │bd-04 │ └──────┘ └───┘ │ | | +| │ └──────┘ │ | | +| └─────────────────────────────────────┘ | | ++------------------------------------------+ | +| Agent Output [Pause][Stop]| | +| ┌─────────────────────────────────────┐ | | +| │ > Creating MCP scaffold... │ | | +| │ > Writing src/index.ts... │ | | +| │ > COMPLETE │ | | +| └─────────────────────────────────────┘ | | ++------------------------------------------+------------------+ +``` + +### 2. Site Builder Plugin (Domain-Specific) + +Location: `packages/mesh-plugin-site-builder/` + +**Extends Task Runner Plugin with:** +- **Site List** - Mounted sites with status +- **Preview Panel** - iframe with HMR +- **Page Selector** - Dropdown of available pages +- **Site-specific skills** pre-registered + +**Layout:** +``` ++------------------------------------------+------------------+ +| SITE BUILDER PLUGIN | MESH CHAT | ++------------------------------------------+ | +| Sites [+ Add Folder] | | +| ┌────────────────────────────────────┐ | | +| │ > decocms/ [Running :8000] │ | | +| │ vibegui.com/ [Stopped] │ | | +| └────────────────────────────────────┘ | | ++------------------------------------------+ | +| Preview [/pitch/good-american ▼] | | +| ┌─────────────────────────────────────┐ | | +| │ │ | | +| │ Live Preview (iframe) │ | | +| │ localhost:8000 │ | | +| │ │ | | +| └─────────────────────────────────────┘ | | ++------------------------------------------+ | +| Tasks (from Task Runner) Budget: $2.50 | | +| ┌─────────────────────────────────────┐ | | +| │ ○ bd-a3f8 Create hero section [3] │ | | +| │ ◐ bd-a3f8.1 Add metrics [1] │ | | +| └─────────────────────────────────────┘ | | ++------------------------------------------+------------------+ +``` + +## Bootstrap Workflow: Using Task Runner to Build Itself + +### Step 1: Set Up Beads in mesh/ and mcps/ + +```bash +# Initialize Beads in mesh repo +cd ~/Projects/mesh +bd init + +# Create epic for Task Runner +bd create "Task Runner MCP + Plugin" -t epic -p 0 +# Returns: bd-abc + +# Create sub-tasks +bd create "MCP scaffold with Beads tools" -p 1 --epic bd-abc +bd create "Ralph loop engine" -p 1 --epic bd-abc --blocked-by bd-abc.1 +bd create "Task Runner Plugin UI" -p 1 --epic bd-abc --blocked-by bd-abc.2 +# ... etc +``` + +### Step 2: Create Bootstrap Skill + +Before we have the UI, create a skill file manually: + +```typescript +// mcps/task-runner/skills/build-mcp.ts +export const buildMcpSkill: Skill = { + id: "build-mcp", + name: "Build MCP Server", + description: "Create an MCP server with tools and resources", + stack: ["*"], // Any stack + + userStories: [ + { + id: "US-001", + title: "Create MCP scaffold", + asA: "developer", + iWant: "a working MCP server entry point", + soThat: "I can add tools incrementally", + acceptanceCriteria: [ + "package.json exists with @modelcontextprotocol/sdk dependency", + "src/index.ts creates and starts MCP server", + "Server responds to initialize request" + ] + }, + { + id: "US-002", + title: "Add first tool", + asA: "developer", + iWant: "a working tool implementation", + soThat: "I can verify the pattern works", + acceptanceCriteria: [ + "Tool is registered in server", + "Tool has Zod schema for input/output", + "Tool can be called and returns expected result" + ], + dependsOn: ["US-001"] + }, + // ... more stories + ], + + qualityGates: { + "*": ["bun run check", "bun run lint"] + }, + + prompts: { + system: `You are building an MCP server for Mesh. +Follow patterns from existing MCPs in mcps/ folder. +Use @modelcontextprotocol/sdk for the server. +Use Zod for schema validation.`, + taskTemplate: "...", + acceptanceCriteria: "When complete, output: COMPLETE" + } +}; +``` + +### Step 3: Run Ralph Loop Manually (Bootstrap Phase) + +Before we have the UI, run the loop via CLI: + +```bash +# In mesh/ directory +# This is what Task Runner will automate + +while true; do + # Get next ready task + TASK=$(bd ready --json | jq -r '.[0]') + if [ -z "$TASK" ]; then + echo "All tasks complete!" + break + fi + + # Build prompt from task + PROMPT="Build this: $TASK. When done, output COMPLETE" + + # Call Claude + echo "$PROMPT" | claude -p --cwd ~/Projects/mesh + + # Check for completion (manual for now) + read -p "Task complete? (y/n): " COMPLETE + if [ "$COMPLETE" = "y" ]; then + bd close $(echo $TASK | jq -r '.id') --reason "Completed" + bd sync + fi +done +``` + +### Step 4: Once Task Runner Plugin Exists, Use It + +Once the Task Runner plugin is built (by the bootstrap process above): + +1. Open Mesh +2. Go to Task Runner plugin +3. Set workspace to `~/Projects/mesh` +4. Select "Build Site Builder" skill +5. Click "Run All" +6. Watch agents build the Site Builder feature +7. Budget controls prevent overspend + +## File Structure + +``` +mcps/ +├── task-runner/ # Generic orchestration +│ ├── package.json +│ ├── src/ +│ │ ├── index.ts # MCP server entry +│ │ ├── tools/ +│ │ │ ├── workspace.ts # WORKSPACE_* tools +│ │ │ ├── beads.ts # BEADS_* tools +│ │ │ ├── skills.ts # SKILL_* tools +│ │ │ └── loop.ts # LOOP_* tools +│ │ ├── engine/ +│ │ │ ├── ralph-loop.ts # Ralph execution loop +│ │ │ ├── completion.ts # Completion detection +│ │ │ ├── quality-gates.ts # Run quality commands +│ │ │ └── budget.ts # Budget tracking +│ │ ├── agent/ +│ │ │ ├── claude-cli.ts # Claude Code CLI wrapper +│ │ │ └── api.ts # Direct API calls (future) +│ │ └── skills/ +│ │ ├── types.ts # Skill interface +│ │ ├── registry.ts # Skill registry +│ │ └── builtin/ +│ │ ├── build-mcp.ts +│ │ ├── build-plugin.ts +│ │ └── build-feature.ts +│ +├── site-builder/ # Site-specific +│ ├── package.json +│ ├── src/ +│ │ ├── index.ts +│ │ ├── tools/ +│ │ │ ├── site.ts # SITE_* tools +│ │ │ └── stack.ts # STACK_* tools +│ │ └── skills/ +│ │ ├── landing-page.ts +│ │ ├── sales-pitch.ts +│ │ └── blog-post.ts + +mesh/packages/ +├── mesh-plugin-task-runner/ # Generic orchestration UI +│ ├── package.json +│ ├── index.tsx +│ ├── components/ +│ │ ├── workspace-picker.tsx +│ │ ├── task-board.tsx +│ │ ├── skill-selector.tsx +│ │ ├── agent-output.tsx +│ │ ├── budget-control.tsx +│ │ └── loop-controls.tsx +│ └── lib/ +│ ├── router.ts +│ └── binding.ts # TASK_RUNNER_BINDING + +├── mesh-plugin-site-builder/ # Site-specific UI +│ ├── package.json +│ ├── index.tsx +│ ├── components/ +│ │ ├── site-list.tsx +│ │ ├── preview-frame.tsx +│ │ └── page-selector.tsx +│ └── lib/ +│ ├── router.ts +│ └── binding.ts # SITE_BUILDER_BINDING +``` + +## Skills for Building Mesh + +Pre-built skills for common Mesh development tasks: + +| Skill ID | Name | Description | +|----------|------|-------------| +| `build-mcp` | Build MCP Server | Create MCP with tools, resources, schemas | +| `build-plugin` | Build Mesh Plugin | Create React plugin with routing, components | +| `add-tool` | Add MCP Tool | Add a new tool to existing MCP | +| `add-component` | Add Plugin Component | Add a new component to existing plugin | +| `build-feature` | Build Feature | Full feature (MCP + Plugin + Integration) | +| `fix-bug` | Fix Bug | Investigate and fix a bug with tests | +| `add-tests` | Add Tests | Add tests for existing code | + +## Implementation Phases + +### Phase 0: Bootstrap (Manual - 1 day) + +**Goal:** Get minimal Task Runner working to build itself + +- [ ] Create `mcps/task-runner/` scaffold manually +- [ ] Implement `BEADS_*` tools (wrap bd CLI) +- [ ] Implement basic `LOOP_START` (single iteration) +- [ ] Create `build-mcp` skill +- [ ] Test: Can it complete one Beads task? + +### Phase 1: Task Runner MCP (Using Bootstrap - 3 days) + +**Goal:** Full Task Runner MCP, built by agents + +- [ ] Set up Beads in mesh/ with epic + tasks +- [ ] Run bootstrap loop to build: + - [ ] All BEADS_* tools + - [ ] Ralph loop engine with completion detection + - [ ] Quality gates execution + - [ ] Budget tracking + - [ ] SKILL_* tools + - [ ] AGENT_* tools +- [ ] Verify: Can run 10 tasks to completion + +### Phase 2: Task Runner Plugin (Using Task Runner - 3 days) + +**Goal:** UI for Task Runner, built by Task Runner + +- [ ] Create epic + tasks for plugin +- [ ] Run Task Runner to build: + - [ ] Plugin scaffold with sidebar + - [ ] Workspace picker component + - [ ] Task board (Beads visualization) + - [ ] Agent output streaming + - [ ] Budget control UI + - [ ] Loop controls +- [ ] Verify: Can use plugin to run tasks + +### Phase 3: Site Builder (Using Task Runner - 3 days) + +**Goal:** Site Builder MCP + Plugin, built by Task Runner + +- [ ] Create epic + tasks for Site Builder +- [ ] Run Task Runner (via its own UI!) to build: + - [ ] Site Builder MCP (site control, stack detection) + - [ ] Site Builder Plugin (preview, site list) + - [ ] Site-specific skills +- [ ] Verify: Can build a landing page with preview + +### Phase 4: Team Workflow (Ongoing) + +**Goal:** Make this the standard way to build features + +- [ ] Document workflow for team +- [ ] Create skill templates +- [ ] Add iteration history and logs +- [ ] Add team features (assign tasks, notifications) + +## How the Team Uses This + +### Daily Workflow + +1. **Create Feature Epic** + ```bash + bd create "Add user settings page" -t epic -p 1 + ``` + +2. **Break Down into Tasks** + ```bash + bd create "Settings API endpoint" --epic bd-xyz -p 1 + bd create "Settings React component" --epic bd-xyz -p 2 --blocked-by bd-xyz.1 + bd create "Integrate with sidebar" --epic bd-xyz -p 3 --blocked-by bd-xyz.2 + ``` + +3. **Open Mesh, Go to Task Runner** + +4. **Set Workspace and Budget** + - Workspace: `/Users/gui/Projects/mesh` + - Budget: $10 or 20 iterations + +5. **Select Skill** (or use "Generic Task") + +6. **Click "Run All"** + +7. **Monitor Progress** + - Watch task board update + - See agent output streaming + - Pause if something looks wrong + +8. **Review When Done** + - All tasks in Beads marked complete + - Code changes committed + - Run `bd sync` to push + +### Team Visibility + +- Everyone can see task progress in Mesh +- Budget spent per feature tracked +- Agent output logs preserved +- Git history shows what agents did + +## Success Criteria + +1. **Bootstrap Success**: Task Runner can build the Task Runner Plugin +2. **Self-Hosting**: Task Runner Plugin can run tasks to build Site Builder +3. **Team Adoption**: Team uses Task Runner for feature development +4. **Budget Control**: No runaway spending, clear limits enforced +5. **Visibility**: Everyone can see what agents are doing +6. **Git Integration**: All work persisted in Beads, committed to git + +## Key Differences from Original Plan + +| Aspect | Original Plan | This Plan | +|--------|---------------|-----------| +| Scope | Site Builder only | Generic + Site Builder | +| Reusability | Site-specific | Any Mesh feature | +| Bootstrap | Manual build | Self-building | +| Team Use | Build sites | Build everything | +| Skills | Site skills only | MCP, Plugin, Feature skills | + +## References + +- [Ralph TUI](https://ralph-tui.com/) - Patterns for loop execution +- [Beads](https://github.com/steveyegge/beads) - Task storage and workflow +- [Beads UI](https://github.com/mantoni/beads-ui) - UI inspiration +- Original Site Builder plan: `mesh/SITE_BUILDER_PLAN.md` diff --git a/apps/mesh/e2e/.gitignore b/apps/mesh/e2e/.gitignore new file mode 100644 index 0000000000..49098d13d9 --- /dev/null +++ b/apps/mesh/e2e/.gitignore @@ -0,0 +1,6 @@ +# Auth state - contains session cookies, don't commit +.auth/ + +# Test artifacts +test-results/ +playwright-report/ diff --git a/apps/mesh/e2e/connections.spec.ts b/apps/mesh/e2e/connections.spec.ts new file mode 100644 index 0000000000..a4c928163f --- /dev/null +++ b/apps/mesh/e2e/connections.spec.ts @@ -0,0 +1,76 @@ +import { test, expect } from "@playwright/test"; + +/** + * Basic E2E Tests for Mesh + * + * Tests the core flow: navigate to org, view connections + */ + +test.describe("Connections", () => { + test("should navigate to org and see connections page", async ({ page }) => { + // Go to home (should redirect to first org) + await page.goto("/"); + + // Wait for the page to load and potentially redirect to an org + await page.waitForLoadState("networkidle"); + + // We should be on some org page (the shell layout) + // The URL should have an org slug like /{org-slug}/... + const url = page.url(); + expect(url).not.toContain("/login"); + + // Look for navigation or sidebar that indicates we're in the app + // Try to find and click on "Connections" in the sidebar + const connectionsLink = page.getByRole("link", { name: /connections/i }); + + // If connections link is visible, click it + if (await connectionsLink.isVisible()) { + await connectionsLink.click(); + + // Wait for connections page to load + await page.waitForLoadState("networkidle"); + + // Should see connections-related content + // Either a list of connections or an empty state + const hasConnections = await page + .getByRole("heading", { name: /connections/i }) + .isVisible(); + const hasEmptyState = await page + .getByText(/no connections|add.*connection|create.*connection/i) + .isVisible(); + const hasConnectionsList = await page + .locator('[data-testid="connections-list"], table, [role="list"]') + .isVisible(); + + expect( + hasConnections || hasEmptyState || hasConnectionsList, + ).toBeTruthy(); + } else { + // If no connections link, we might be on a different layout + // Just verify we're authenticated and on some page + console.log("Connections link not found, checking for authenticated UI"); + + // Should have some authenticated content + const hasAuthenticatedUI = await page + .locator("nav, [data-testid], header") + .first() + .isVisible(); + expect(hasAuthenticatedUI).toBeTruthy(); + } + }); + + test("should show org home with overview", async ({ page }) => { + await page.goto("/"); + await page.waitForLoadState("networkidle"); + + // Should not be on login + expect(page.url()).not.toContain("/login"); + + // Take a screenshot for debugging + await page.screenshot({ path: "e2e/.auth/org-home.png" }); + + // The home page should show something - could be dashboard, connections, etc. + const pageContent = await page.content(); + expect(pageContent.length).toBeGreaterThan(100); + }); +}); diff --git a/apps/mesh/e2e/global.setup.ts b/apps/mesh/e2e/global.setup.ts new file mode 100644 index 0000000000..b3379a9afb --- /dev/null +++ b/apps/mesh/e2e/global.setup.ts @@ -0,0 +1,95 @@ +import { test as setup, expect } from "@playwright/test"; +import path from "path"; + +const authFile = path.join(__dirname, ".auth/user.json"); + +/** + * Global setup: Create a test user and save authenticated session + * + * This runs once before all tests. It signs up (or signs in) a test user + * and saves the session cookies to be reused by all tests. + * + * Test credentials (configure via env or use defaults): + * - TEST_USER_EMAIL: test@example.com + * - TEST_USER_PASSWORD: TestPassword123! + */ +setup("authenticate", async ({ page }) => { + const email = process.env.TEST_USER_EMAIL || "test@example.com"; + const password = process.env.TEST_USER_PASSWORD || "TestPassword123!"; + const name = process.env.TEST_USER_NAME || "Test User"; + + // Go to login page + await page.goto("/login"); + + // Wait for the auth form to be visible + await page.waitForSelector("form", { timeout: 10000 }); + + // Check if we're on a sign-in or sign-up form + // Try to sign in first, if that fails, sign up + const signInTab = page.getByRole("tab", { name: /sign in/i }); + const signUpTab = page.getByRole("tab", { name: /sign up/i }); + + // If tabs exist, we have a unified auth form + if (await signInTab.isVisible()) { + // Try sign in first + await signInTab.click(); + + await page.getByLabel(/email/i).fill(email); + await page.getByLabel(/password/i).fill(password); + await page.getByRole("button", { name: /sign in/i }).click(); + + // Wait a bit for the response + await page.waitForTimeout(2000); + + // Check if we're still on login (sign in failed, need to sign up) + if (page.url().includes("/login")) { + console.log("Sign in failed, trying sign up..."); + + // Switch to sign up + await signUpTab.click(); + + await page.getByLabel(/name/i).fill(name); + await page.getByLabel(/email/i).fill(email); + await page.getByLabel(/password/i).fill(password); + await page.getByRole("button", { name: /sign up/i }).click(); + + // Wait for redirect after signup + await page.waitForURL((url) => !url.pathname.includes("/login"), { + timeout: 15000, + }); + } + } else { + // Fallback: just try filling in the form directly + const emailInput = page.getByLabel(/email/i); + const passwordInput = page.getByLabel(/password/i); + const nameInput = page.getByLabel(/name/i); + + // If name field exists, it's a sign-up form + if (await nameInput.isVisible()) { + await nameInput.fill(name); + } + + await emailInput.fill(email); + await passwordInput.fill(password); + + // Click submit + await page.getByRole("button", { name: /sign|submit/i }).click(); + } + + // Wait for successful authentication (should redirect away from login) + await expect(page).not.toHaveURL(/\/login/, { timeout: 15000 }); + + // Verify we're authenticated by checking for common UI elements + // The shell should show the user menu or organization selector + await page.waitForSelector( + '[data-testid="user-menu"], [data-testid="org-selector"], nav', + { + timeout: 10000, + }, + ); + + // Save the storage state (cookies + localStorage) + await page.context().storageState({ path: authFile }); + + console.log(`Auth state saved to ${authFile}`); +}); diff --git a/apps/mesh/package.json b/apps/mesh/package.json index c28005dc80..f1a30f9eb4 100644 --- a/apps/mesh/package.json +++ b/apps/mesh/package.json @@ -31,6 +31,10 @@ "start": "bun run ./dist/server/server.js", "migrate": "bun run src/database/migrate.ts", "test": "bun test", + "test:e2e": "bunx playwright test", + "test:e2e:ui": "bunx playwright test --ui", + "test:e2e:debug": "bunx playwright test --debug", + "test:e2e:setup": "bunx playwright test --project=setup", "better-auth:migrate": "bunx --bun @better-auth/cli migrate -y --config src/auth/index.ts", "prepublishOnly": "bun run build:client && bun run build:server" }, @@ -65,6 +69,7 @@ "@opentelemetry/sdk-metrics": "^2.2.0", "@opentelemetry/sdk-node": "^0.207.0", "@opentelemetry/sdk-trace-base": "^2.5.0", + "@playwright/test": "^1.58.1", "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", @@ -109,10 +114,12 @@ "jose": "^6.0.11", "lucide-react": "^0.468.0", "marked": "^15.0.6", - "mesh-plugin-user-sandbox": "workspace:*", "mesh-plugin-object-storage": "workspace:*", + "mesh-plugin-task-runner": "workspace:*", + "mesh-plugin-user-sandbox": "workspace:*", "nanoid": "^5.1.6", "pg": "^8.16.3", + "playwright": "^1.58.1", "prettier": "^3.4.2", "react": "^19.2.0", "react-dom": "^19.2.0", diff --git a/apps/mesh/playwright.config.ts b/apps/mesh/playwright.config.ts new file mode 100644 index 0000000000..6de51bd9e8 --- /dev/null +++ b/apps/mesh/playwright.config.ts @@ -0,0 +1,48 @@ +import { defineConfig, devices } from "@playwright/test"; + +/** + * Playwright E2E Test Configuration + * + * Uses storage state for authentication - run `bun run test:e2e:setup` first + * to create a test user and save the auth session. + */ +export default defineConfig({ + testDir: "./e2e", + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: "html", + + use: { + baseURL: process.env.BASE_URL || "http://localhost:4000", + trace: "on-first-retry", + // Use saved auth state for all tests + storageState: "./e2e/.auth/user.json", + }, + + projects: [ + // Setup project runs first to create auth state + { + name: "setup", + testMatch: /global\.setup\.ts/, + use: { storageState: undefined }, + }, + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + dependencies: ["setup"], + }, + ], + + // Run the dev server before tests (in CI or when TEST_START_SERVER=1) + webServer: + process.env.CI || process.env.TEST_START_SERVER + ? { + command: "bun run dev", + url: "http://localhost:4000", + reuseExistingServer: !process.env.CI, + timeout: 120 * 1000, + } + : undefined, +}); diff --git a/apps/mesh/src/web/components/chat/context.tsx b/apps/mesh/src/web/components/chat/context.tsx index 254833d978..ba35d04ce6 100644 --- a/apps/mesh/src/web/components/chat/context.tsx +++ b/apps/mesh/src/web/components/chat/context.tsx @@ -33,7 +33,9 @@ import { createContext, type PropsWithChildren, useContext, + useEffect, useReducer, + useRef, } from "react"; import { toast } from "sonner"; import { useModelConnections } from "../../hooks/collections/use-llm"; @@ -507,6 +509,53 @@ async function callUpdateThreadTool( const ChatContext = createContext(null); +// ============================================================================ +// Event System for Cross-Component Chat Messaging +// ============================================================================ + +/** + * Event name for sending chat messages from outside the chat context + */ +export const CHAT_SEND_MESSAGE_EVENT = "deco:send-chat-message"; + +/** + * Event detail structure for chat message events + */ +export interface ChatSendMessageEventDetail { + /** Plain text message to send */ + text: string; + /** Virtual MCP ID to select before sending (optional) */ + virtualMcpId?: string; +} + +/** + * Dispatch a chat message event from anywhere in the app + * The chat context will receive this and send the message + */ +export function dispatchChatMessage(detail: ChatSendMessageEventDetail): void { + window.dispatchEvent( + new CustomEvent(CHAT_SEND_MESSAGE_EVENT, { + detail, + }), + ); +} + +/** + * Build a minimal tiptapDoc from plain text + */ +function textToTiptapDoc(text: string): Metadata["tiptapDoc"] { + // Split text into paragraphs + const paragraphs = text.split("\n\n").filter((p) => p.trim()); + + return { + type: "doc", + content: paragraphs.map((paragraph) => ({ + type: "paragraph", + content: [{ type: "text", text: paragraph }], + })), + }; +} + /** * Provider component for chat context * Consolidates all chat-related state: interaction, threads, virtual MCP, model, and chat session @@ -747,7 +796,44 @@ export function ChatProvider({ children }: PropsWithChildren) { const clearFinishReason = () => chatDispatch({ type: "CLEAR_FINISH_REASON" }); // =========================================================================== - // 7. CONTEXT VALUE & RETURN + // 7. EVENT LISTENERS + // =========================================================================== + + // Use a ref to track the sendMessage function to avoid stale closures + const sendMessageRef = useRef(sendMessage); + sendMessageRef.current = sendMessage; + + // Listen for chat message events from plugins or other components + useEffect(() => { + const handleChatEvent = ( + event: CustomEvent, + ) => { + const { text, virtualMcpId } = event.detail; + + // Optionally select the virtual MCP before sending + if (virtualMcpId) { + setVirtualMcpId(virtualMcpId); + } + + // Build tiptapDoc and send + const tiptapDoc = textToTiptapDoc(text); + sendMessageRef.current(tiptapDoc); + }; + + window.addEventListener( + CHAT_SEND_MESSAGE_EVENT, + handleChatEvent as EventListener, + ); + return () => { + window.removeEventListener( + CHAT_SEND_MESSAGE_EVENT, + handleChatEvent as EventListener, + ); + }; + }, []); + + // =========================================================================== + // 8. CONTEXT VALUE & RETURN // =========================================================================== const value: ChatContextValue = { diff --git a/apps/mesh/src/web/components/chat/index.tsx b/apps/mesh/src/web/components/chat/index.tsx index 5ab4d5acba..9f2f36b26c 100644 --- a/apps/mesh/src/web/components/chat/index.tsx +++ b/apps/mesh/src/web/components/chat/index.tsx @@ -11,7 +11,8 @@ import { NoLlmBindingEmptyState } from "./no-llm-binding-empty-state"; import { ThreadHistoryPopover } from "./popover-threads"; import { DecoChatSkeleton } from "./skeleton"; import type { Metadata } from "./types.ts"; -export { useChat } from "./context"; +export { dispatchChatMessage, useChat } from "./context"; +export type { ChatSendMessageEventDetail } from "./context"; export { ModelSelector } from "./select-model"; export type { ModelChangePayload, SelectedModelState } from "./select-model"; export type { VirtualMCPInfo } from "./select-virtual-mcp"; diff --git a/apps/mesh/src/web/components/details/connection/index.tsx b/apps/mesh/src/web/components/details/connection/index.tsx index e07218178c..6f2b8d0f30 100644 --- a/apps/mesh/src/web/components/details/connection/index.tsx +++ b/apps/mesh/src/web/components/details/connection/index.tsx @@ -22,7 +22,7 @@ import { } from "@decocms/mesh-sdk"; import { Button } from "@deco/ui/components/button.tsx"; import { ResourceTabs } from "@deco/ui/components/resource-tabs.tsx"; -import { Loading01 } from "@untitledui/icons"; +import { Loading01, RefreshCcw01 } from "@untitledui/icons"; import { Link, useNavigate, @@ -50,6 +50,8 @@ function ConnectionInspectorViewWithConnection({ resources, tools, isLoadingTools, + onRefreshTools, + isRefreshingTools, }: { connection: ConnectionEntity; connectionId: string; @@ -72,6 +74,8 @@ function ConnectionInspectorViewWithConnection({ outputSchema?: Record; }>; isLoadingTools: boolean; + onRefreshTools?: () => void; + isRefreshingTools?: boolean; }) { const navigate = useNavigate({ from: "/$org/mcps/$connectionId" }); @@ -133,21 +137,37 @@ function ConnectionInspectorViewWithConnection({ ); const breadcrumb = ( - - - - - - Connections - - - - - - {connection.title} - - - +
+ + + + + + Connections + + + + + + {connection.title} + + + + {onRefreshTools && ( + + )} +
); return ( @@ -289,6 +309,14 @@ function ConnectionInspectorViewContent() { }); }; + // Refresh tools by triggering an empty update (which re-fetches tools) + const handleRefreshTools = async () => { + await actions.update.mutateAsync({ + id: connectionId, + data: {}, + }); + }; + if (!connection) { return (
@@ -326,6 +354,8 @@ function ConnectionInspectorViewContent() { resources={resources} tools={tools} isLoadingTools={isLoadingTools} + onRefreshTools={handleRefreshTools} + isRefreshingTools={actions.update.isPending} /> ); } diff --git a/apps/mesh/src/web/index.tsx b/apps/mesh/src/web/index.tsx index ab0d6ede08..1ab4ce57d5 100644 --- a/apps/mesh/src/web/index.tsx +++ b/apps/mesh/src/web/index.tsx @@ -232,18 +232,6 @@ const orgWorkflowRoute = createRoute({ component: lazyRouteComponent(() => import("./routes/orgs/workflow.tsx")), }); -/** - * Dynamic plugin route - * Routes to plugins based on $pluginId parameter - */ -const pluginLayoutRoute = createRoute({ - getParentRoute: () => shellLayout, - path: "/$org/$pluginId", - component: lazyRouteComponent( - () => import("./layouts/dynamic-plugin-layout.tsx"), - ), -}); - /** * In-memory state for plugins to register stuff via callbacks. */ @@ -253,14 +241,32 @@ export const pluginRootSidebarItems: { label: string; }[] = []; -const pluginRoutes: AnyRoute[] = []; +/** + * Create plugin routes with unique parent routes per plugin. + * Each plugin gets its own parent route with a static path based on the plugin ID. + */ +const pluginLayoutRoutes: AnyRoute[] = []; sourcePlugins.forEach((plugin: AnyClientPlugin) => { + // Create a unique parent route for this plugin + const pluginParentRoute = createRoute({ + getParentRoute: () => shellLayout, + path: `/$org/${plugin.id}`, + component: lazyRouteComponent( + () => import("./layouts/dynamic-plugin-layout.tsx"), + ), + }); + // Only invoke setup if the plugin provides it - if (!plugin.setup) return; + if (!plugin.setup) { + pluginLayoutRoutes.push(pluginParentRoute); + return; + } + + const pluginChildRoutes: AnyRoute[] = []; const context: PluginSetupContext = { - parentRoute: pluginLayoutRoute as AnyRoute, + parentRoute: pluginParentRoute as AnyRoute, routing: { createRoute: createRoute, lazyRouteComponent: lazyRouteComponent, @@ -268,15 +274,19 @@ sourcePlugins.forEach((plugin: AnyClientPlugin) => { registerRootSidebarItem: (item) => pluginRootSidebarItems.push({ pluginId: plugin.id, ...item }), registerPluginRoutes: (routes) => { - pluginRoutes.push(...routes); + pluginChildRoutes.push(...routes); }, }; plugin.setup(context); -}); -// Add all plugin routes as children of the plugin layout -const pluginLayoutWithChildren = pluginLayoutRoute.addChildren(pluginRoutes); + // Add child routes to this plugin's parent route + if (pluginChildRoutes.length > 0) { + pluginLayoutRoutes.push(pluginParentRoute.addChildren(pluginChildRoutes)); + } else { + pluginLayoutRoutes.push(pluginParentRoute); + } +}); const oauthCallbackRoute = createRoute({ getParentRoute: () => rootRoute, @@ -302,7 +312,7 @@ const shellRouteTree = shellLayout.addChildren([ orgWorkflowRoute, connectionLayoutRoute, collectionDetailsRoute, - pluginLayoutWithChildren, + ...pluginLayoutRoutes, ]); const routeTree = rootRoute.addChildren([ diff --git a/apps/mesh/src/web/layouts/dynamic-plugin-layout.tsx b/apps/mesh/src/web/layouts/dynamic-plugin-layout.tsx index a1951f3f54..f1de34a073 100644 --- a/apps/mesh/src/web/layouts/dynamic-plugin-layout.tsx +++ b/apps/mesh/src/web/layouts/dynamic-plugin-layout.tsx @@ -1,18 +1,22 @@ /** * Dynamic Plugin Layout * - * Routes to the appropriate plugin layout based on the $pluginId param. + * Routes to the appropriate plugin layout based on the URL path. * Uses the plugin's renderHeader/renderEmptyState if defined, otherwise falls back to Outlet. */ -import { Outlet, useParams } from "@tanstack/react-router"; +import { Outlet, useLocation } from "@tanstack/react-router"; import { Suspense } from "react"; import { Loading01 } from "@untitledui/icons"; import { sourcePlugins } from "../plugins"; import { PluginLayout } from "./plugin-layout"; export default function DynamicPluginLayout() { - const { pluginId } = useParams({ strict: false }) as { pluginId: string }; + const location = useLocation(); + + // Extract plugin ID from path: /$org/$pluginId/... -> pluginId + const pathParts = location.pathname.split("/").filter(Boolean); + const pluginId = pathParts.length >= 2 ? pathParts[1] : undefined; // Find the plugin by ID const plugin = sourcePlugins.find((p) => p.id === pluginId); diff --git a/apps/mesh/src/web/layouts/shell-layout.tsx b/apps/mesh/src/web/layouts/shell-layout.tsx index 553d263807..86dbe33d1a 100644 --- a/apps/mesh/src/web/layouts/shell-layout.tsx +++ b/apps/mesh/src/web/layouts/shell-layout.tsx @@ -31,7 +31,13 @@ import { import { useSuspenseQuery } from "@tanstack/react-query"; import { Outlet, useParams, useRouterState } from "@tanstack/react-router"; import { MessageChatSquare } from "@untitledui/icons"; -import { PropsWithChildren, Suspense, useRef, useTransition } from "react"; +import { + PropsWithChildren, + Suspense, + useEffect, + useRef, + useTransition, +} from "react"; import { KEYS } from "../lib/query-keys"; function Topbar({ @@ -138,12 +144,36 @@ function PersistentSidebarProvider({ children }: PropsWithChildren) { ); } +/** + * Event name for opening the chat panel from plugins or other components + */ +export const CHAT_OPEN_EVENT = "deco:open-chat"; + +/** + * Dispatch an event to open the chat panel + */ +export function dispatchOpenChat(): void { + window.dispatchEvent(new CustomEvent(CHAT_OPEN_EVENT)); +} + /** * This component renders the chat panel and the main content. * It's important to keep it like this to avoid unnecessary re-renders. */ function ChatPanels({ disableChat = false }: { disableChat?: boolean }) { - const [chatOpen] = useDecoChatOpen(); + const [chatOpen, setChatOpen] = useDecoChatOpen(); + + // Listen for open chat events from plugins + useEffect(() => { + const handleOpenChat = () => { + setChatOpen(true); + }; + + window.addEventListener(CHAT_OPEN_EVENT, handleOpenChat); + return () => { + window.removeEventListener(CHAT_OPEN_EVENT, handleOpenChat); + }; + }, [setChatOpen]); return ( diff --git a/apps/mesh/src/web/plugins.ts b/apps/mesh/src/web/plugins.ts index d2ad20bcdd..96ab127b08 100644 --- a/apps/mesh/src/web/plugins.ts +++ b/apps/mesh/src/web/plugins.ts @@ -1,9 +1,11 @@ import type { AnyClientPlugin } from "@decocms/bindings/plugins"; import { objectStoragePlugin } from "mesh-plugin-object-storage"; import { clientPlugin as userSandboxPlugin } from "mesh-plugin-user-sandbox/client"; +import { taskRunnerPlugin } from "mesh-plugin-task-runner"; // Registered plugins export const sourcePlugins: AnyClientPlugin[] = [ objectStoragePlugin, userSandboxPlugin, + taskRunnerPlugin, ]; diff --git a/packages/bindings/src/core/plugins.ts b/packages/bindings/src/core/plugins.ts index 9f6bad4808..757c9a9ff5 100644 --- a/packages/bindings/src/core/plugins.ts +++ b/packages/bindings/src/core/plugins.ts @@ -94,6 +94,12 @@ export { type AnyRoute, type RouteIds, type RouteById, + // TanStack router hooks for plugin use + useNavigate, + useParams, + useSearch, + useLocation, + Link, } from "./plugin-router"; // Re-export plugin context provider and hook (React components) diff --git a/packages/bindings/src/index.ts b/packages/bindings/src/index.ts index e3048c0fa5..600c5b5f73 100644 --- a/packages/bindings/src/index.ts +++ b/packages/bindings/src/index.ts @@ -101,3 +101,10 @@ export { type DeleteObjectsInput, type DeleteObjectsOutput, } from "./well-known/object-storage"; + +// Re-export task runner binding types +export { + TASK_RUNNER_BINDING, + type TaskRunnerBinding, + type Task, +} from "./well-known/task-runner"; diff --git a/packages/bindings/src/well-known/task-runner.ts b/packages/bindings/src/well-known/task-runner.ts new file mode 100644 index 0000000000..2b615cf08e --- /dev/null +++ b/packages/bindings/src/well-known/task-runner.ts @@ -0,0 +1,357 @@ +/** + * Task Runner Well-Known Binding + * + * Defines the interface for task orchestration with Beads integration + * and Ralph-style execution loops. + * + * This binding includes: + * - WORKSPACE_SET/GET: Manage working directory + * - BEADS_*: Task management via Beads CLI + * - LOOP_*: Ralph-style execution loop control + * - SKILL_*: Skill management and application + */ + +import { z } from "zod"; +import type { Binder, ToolBinder } from "../core/binder"; + +// ============================================================================ +// Task Schema +// ============================================================================ + +const TaskSchema = z.object({ + id: z.string().describe("Task ID (e.g., bd-abc or bd-abc.1)"), + title: z.string().describe("Task title"), + description: z.string().optional(), + status: z.enum(["open", "in_progress", "blocked", "closed"]).optional(), + priority: z.number().optional(), + issue_type: z.string().optional(), + owner: z.string().optional(), + created_at: z.string().optional(), + created_by: z.string().optional(), + updated_at: z.string().optional(), +}); + +export type Task = z.infer; + +// ============================================================================ +// Workspace Tools +// ============================================================================ + +const WorkspaceSetInputSchema = z.object({ + directory: z.string().describe("Absolute path to the workspace directory"), +}); + +const WorkspaceSetOutputSchema = z.object({ + success: z.boolean(), + workspace: z.string(), + hasBeads: z.boolean().describe("Whether .beads/ directory exists"), +}); + +const WorkspaceGetInputSchema = z.object({}); + +const WorkspaceGetOutputSchema = z.object({ + workspace: z.string().nullable(), + hasBeads: z.boolean().nullable().describe("Whether .beads/ directory exists"), +}); + +// ============================================================================ +// Beads Tools +// ============================================================================ + +const BeadsInitInputSchema = z.object({ + prefix: z.string().optional().describe("Custom prefix for task IDs"), + quiet: z.boolean().optional(), +}); + +const BeadsInitOutputSchema = z.object({ + success: z.boolean(), + message: z.string(), + workspace: z.string(), +}); + +const BeadsReadyInputSchema = z.object({ + limit: z.number().optional(), +}); + +const BeadsReadyOutputSchema = z.object({ + tasks: z.array(TaskSchema), + count: z.number(), +}); + +const BeadsCreateInputSchema = z.object({ + title: z.string(), + type: z.enum(["epic", "story", "task", "bug"]).optional(), + priority: z.number().optional(), + description: z.string().optional(), + epic: z.string().optional(), + blockedBy: z.array(z.string()).optional(), + labels: z.array(z.string()).optional(), +}); + +const BeadsCreateOutputSchema = z.object({ + success: z.boolean(), + taskId: z.string(), + message: z.string(), +}); + +const BeadsUpdateInputSchema = z.object({ + taskId: z.string(), + status: z.enum(["open", "in_progress", "blocked", "closed"]).optional(), + title: z.string().optional(), + description: z.string().optional(), + priority: z.number().optional(), + notes: z.string().optional(), +}); + +const BeadsUpdateOutputSchema = z.object({ + success: z.boolean(), + taskId: z.string(), + message: z.string(), +}); + +const BeadsCloseInputSchema = z.object({ + taskIds: z.array(z.string()).min(1), + reason: z.string().optional(), +}); + +const BeadsCloseOutputSchema = z.object({ + success: z.boolean(), + closedTasks: z.array(z.string()), + message: z.string(), +}); + +const BeadsListInputSchema = z.object({ + tree: z.boolean().optional(), + status: z.enum(["open", "in_progress", "blocked", "closed"]).optional(), + epic: z.string().optional(), +}); + +const BeadsListOutputSchema = z.object({ + tasks: z.array(TaskSchema), + count: z.number(), +}); + +// ============================================================================ +// Loop Tools +// ============================================================================ + +const LoopStartInputSchema = z.object({ + maxIterations: z.number().optional(), + maxTokens: z.number().optional(), + qualityGates: z.array(z.string()).optional(), + singleIteration: z.boolean().optional(), +}); + +const LoopStartOutputSchema = z.object({ + status: z.string(), + iterations: z.number(), + tasksCompleted: z.array(z.string()), + tasksFailed: z.array(z.string()), + totalTokens: z.number(), + message: z.string(), +}); + +const LoopStatusInputSchema = z.object({}); + +const LoopStatusOutputSchema = z.object({ + status: z.string(), + currentTask: z.string().nullable(), + iteration: z.number(), + maxIterations: z.number(), + totalTokens: z.number(), + maxTokens: z.number(), + tasksCompleted: z.array(z.string()), + tasksFailed: z.array(z.string()), + startedAt: z.string().nullable(), + lastActivity: z.string().nullable(), + error: z.string().nullable(), +}); + +const LoopPauseInputSchema = z.object({}); +const LoopPauseOutputSchema = z.object({ + success: z.boolean(), + message: z.string(), +}); + +const LoopStopInputSchema = z.object({}); +const LoopStopOutputSchema = z.object({ + success: z.boolean(), + finalState: z.object({ + iterations: z.number(), + tasksCompleted: z.array(z.string()), + tasksFailed: z.array(z.string()), + }), +}); + +// ============================================================================ +// Skill Tools +// ============================================================================ + +const SkillSummarySchema = z.object({ + id: z.string(), + name: z.string(), + description: z.string(), + stack: z.array(z.string()), + storyCount: z.number(), +}); + +const SkillListInputSchema = z.object({}); +const SkillListOutputSchema = z.object({ + skills: z.array(SkillSummarySchema), +}); + +const SkillApplyInputSchema = z.object({ + skillId: z.string(), + customization: z + .object({ + prefix: z.string().optional(), + extraContext: z.string().optional(), + }) + .optional(), +}); + +const SkillApplyOutputSchema = z.object({ + success: z.boolean(), + tasksCreated: z.array(z.string()), + message: z.string(), +}); + +// ============================================================================ +// Binding Definition +// ============================================================================ + +/** + * Task Runner Binding + * + * Defines the interface for task orchestration with Beads and Ralph loop. + */ +export const TASK_RUNNER_BINDING = [ + { + name: "WORKSPACE_SET" as const, + inputSchema: WorkspaceSetInputSchema, + outputSchema: WorkspaceSetOutputSchema, + } satisfies ToolBinder< + "WORKSPACE_SET", + z.infer, + z.infer + >, + { + name: "WORKSPACE_GET" as const, + inputSchema: WorkspaceGetInputSchema, + outputSchema: WorkspaceGetOutputSchema, + } satisfies ToolBinder< + "WORKSPACE_GET", + z.infer, + z.infer + >, + { + name: "BEADS_INIT" as const, + inputSchema: BeadsInitInputSchema, + outputSchema: BeadsInitOutputSchema, + } satisfies ToolBinder< + "BEADS_INIT", + z.infer, + z.infer + >, + { + name: "BEADS_READY" as const, + inputSchema: BeadsReadyInputSchema, + outputSchema: BeadsReadyOutputSchema, + } satisfies ToolBinder< + "BEADS_READY", + z.infer, + z.infer + >, + { + name: "BEADS_CREATE" as const, + inputSchema: BeadsCreateInputSchema, + outputSchema: BeadsCreateOutputSchema, + } satisfies ToolBinder< + "BEADS_CREATE", + z.infer, + z.infer + >, + { + name: "BEADS_UPDATE" as const, + inputSchema: BeadsUpdateInputSchema, + outputSchema: BeadsUpdateOutputSchema, + } satisfies ToolBinder< + "BEADS_UPDATE", + z.infer, + z.infer + >, + { + name: "BEADS_CLOSE" as const, + inputSchema: BeadsCloseInputSchema, + outputSchema: BeadsCloseOutputSchema, + } satisfies ToolBinder< + "BEADS_CLOSE", + z.infer, + z.infer + >, + { + name: "BEADS_LIST" as const, + inputSchema: BeadsListInputSchema, + outputSchema: BeadsListOutputSchema, + } satisfies ToolBinder< + "BEADS_LIST", + z.infer, + z.infer + >, + { + name: "LOOP_START" as const, + inputSchema: LoopStartInputSchema, + outputSchema: LoopStartOutputSchema, + } satisfies ToolBinder< + "LOOP_START", + z.infer, + z.infer + >, + { + name: "LOOP_STATUS" as const, + inputSchema: LoopStatusInputSchema, + outputSchema: LoopStatusOutputSchema, + } satisfies ToolBinder< + "LOOP_STATUS", + z.infer, + z.infer + >, + { + name: "LOOP_PAUSE" as const, + inputSchema: LoopPauseInputSchema, + outputSchema: LoopPauseOutputSchema, + } satisfies ToolBinder< + "LOOP_PAUSE", + z.infer, + z.infer + >, + { + name: "LOOP_STOP" as const, + inputSchema: LoopStopInputSchema, + outputSchema: LoopStopOutputSchema, + } satisfies ToolBinder< + "LOOP_STOP", + z.infer, + z.infer + >, + { + name: "SKILL_LIST" as const, + inputSchema: SkillListInputSchema, + outputSchema: SkillListOutputSchema, + } satisfies ToolBinder< + "SKILL_LIST", + z.infer, + z.infer + >, + { + name: "SKILL_APPLY" as const, + inputSchema: SkillApplyInputSchema, + outputSchema: SkillApplyOutputSchema, + } satisfies ToolBinder< + "SKILL_APPLY", + z.infer, + z.infer + >, +] as const satisfies Binder; + +export type TaskRunnerBinding = typeof TASK_RUNNER_BINDING; diff --git a/packages/mesh-plugin-task-runner/components/plugin-empty-state.tsx b/packages/mesh-plugin-task-runner/components/plugin-empty-state.tsx new file mode 100644 index 0000000000..b0659e3693 --- /dev/null +++ b/packages/mesh-plugin-task-runner/components/plugin-empty-state.tsx @@ -0,0 +1,27 @@ +/** + * Plugin Empty State Component + * + * Shown when no Task Runner connection is available. + */ + +import { File04 } from "@untitledui/icons"; + +export default function PluginEmptyState() { + return ( +
+
+ +
+

No Task Runner Connected

+

+ Connect a Task Runner MCP to manage tasks with Beads and run agent + execution loops. +

+

+ Install the{" "} + mcp-task-runner MCP from + the registry to get started. +

+
+ ); +} diff --git a/packages/mesh-plugin-task-runner/components/plugin-header.tsx b/packages/mesh-plugin-task-runner/components/plugin-header.tsx new file mode 100644 index 0000000000..d45be65fa5 --- /dev/null +++ b/packages/mesh-plugin-task-runner/components/plugin-header.tsx @@ -0,0 +1,104 @@ +/** + * Plugin Header Component + * + * Connection selector for the task runner plugin. + */ + +import type { PluginRenderHeaderProps } from "@decocms/bindings/plugins"; +import { File04, ChevronDown, Check } from "@untitledui/icons"; +import { useState, useRef } from "react"; + +/** + * Simple dropdown menu for connection selection. + */ +function ConnectionSelector({ + connections, + selectedConnectionId, + onConnectionChange, +}: PluginRenderHeaderProps) { + const [isOpen, setIsOpen] = useState(false); + const dropdownRef = useRef(null); + + const selectedConnection = connections.find( + (c) => c.id === selectedConnectionId, + ); + + const handleBlur = (e: React.FocusEvent) => { + if (!dropdownRef.current?.contains(e.relatedTarget as Node)) { + setIsOpen(false); + } + }; + + if (connections.length === 1) { + return ( +
+ {selectedConnection?.icon ? ( + + ) : ( + + )} + {selectedConnection?.title || "Task Runner"} +
+ ); + } + + return ( +
+ + + {isOpen && ( +
+ {connections.map((connection) => ( + + ))} +
+ )} +
+ ); +} + +export default function PluginHeader(props: PluginRenderHeaderProps) { + return ( +
+ +
+ ); +} diff --git a/packages/mesh-plugin-task-runner/components/task-board.tsx b/packages/mesh-plugin-task-runner/components/task-board.tsx new file mode 100644 index 0000000000..f4e4b29ad1 --- /dev/null +++ b/packages/mesh-plugin-task-runner/components/task-board.tsx @@ -0,0 +1,798 @@ +/** + * Task Board Component + * + * Main UI for the Task Runner plugin. Shows: + * - Workspace selector + * - Task/Skills tabs + * - Loop controls + */ + +import { + Folder, + AlertCircle, + Check, + File02, + MessageChatSquare, + BookOpen01, + Edit02, +} from "@untitledui/icons"; +import { useState } from "react"; +import { toast } from "sonner"; +import { useParams } from "@decocms/bindings/plugins"; +import { + useTasks, + useLoopStatus, + useSkills, + useWorkspace, + useBeadsStatus, + useInitBeads, + useCreateTask, + useUpdateTask, + type Task, + type Skill, +} from "../hooks/use-tasks"; + +// ============================================================================ +// Icons +// ============================================================================ + +const PauseIcon = ({ size = 14 }: { size?: number }) => ( + + + +); + +const PlusIcon = ({ size = 14 }: { size?: number }) => ( + + + +); + +const CircleIcon = ({ size = 14 }: { size?: number }) => ( + + + +); + +const ClockIcon = ({ size = 14 }: { size?: number }) => ( + + + + +); + +const LoadingIcon = ({ + size = 14, + className = "", +}: { + size?: number; + className?: string; +}) => ( + + + +); + +const RefreshIcon = ({ size = 14 }: { size?: number }) => ( + + + + +); + +// ============================================================================ +// Chat Integration +// ============================================================================ + +/** Event to open the chat panel */ +const CHAT_OPEN_EVENT = "deco:open-chat"; +/** Event to send a message to the chat */ +const CHAT_SEND_MESSAGE_EVENT = "deco:send-chat-message"; + +interface ChatSendMessageEventDetail { + text: string; + virtualMcpId?: string; +} + +/** + * Build a prompt message for the agent to work on a task + */ +function buildTaskPrompt( + task: { + id: string; + title: string; + description?: string; + }, + workspace: string, +): string { + // Use JSON to be unambiguous about tool parameters + const params = JSON.stringify({ + taskId: task.id, + taskTitle: task.title, + taskDescription: task.description || task.title, + workspace, + }); + return `AGENT_SPAWN ${params}`; +} + +/** + * Open the chat panel and send a message to the agent + */ +function sendChatMessage( + text: string, + options?: { virtualMcpId?: string }, +): void { + // Open the chat panel + window.dispatchEvent(new CustomEvent(CHAT_OPEN_EVENT)); + + // Send the message after a brief delay to allow panel to open + setTimeout(() => { + window.dispatchEvent( + new CustomEvent(CHAT_SEND_MESSAGE_EVENT, { + detail: { text, virtualMcpId: options?.virtualMcpId }, + }), + ); + }, 100); +} + +// ============================================================================ +// Workspace Display +// ============================================================================ + +function WorkspaceDisplay() { + const { data: workspaceData, isLoading } = useWorkspace(); + + if (isLoading) { + return ( +
+
+ Loading workspace... +
+
+ ); + } + + if (!workspaceData?.workspace) { + return ( +
+ +

No Workspace Available

+

+ This storage connection doesn't expose a GET_ROOT tool. +

+

+ Use a local-fs MCP with object storage tools to enable task + management. +

+
+ ); + } + + return ( +
+
+ + {workspaceData.workspace} +
+ + From storage connection + +
+ ); +} + +// ============================================================================ +// Beads Init Banner +// ============================================================================ + +function BeadsInitBanner() { + const initBeads = useInitBeads(); + + return ( +
+
+ +
+

Beads Not Initialized

+

+ This workspace doesn't have Beads set up yet. Initialize Beads to + start tracking tasks. +

+ + {initBeads.isError && ( +

+ Error: {String(initBeads.error)} +

+ )} +
+
+
+ ); +} + +// ============================================================================ +// Agent Status - Shows when the Task Runner Agent is actively working +// ============================================================================ + +function AgentStatus() { + const { data: status } = useLoopStatus(); + + const isRunning = status?.status === "running"; + const isPaused = status?.status === "paused"; + const isActive = isRunning || isPaused; + + // Don't show anything when idle - the agent controls happen in chat + if (!isActive) { + return null; + } + + return ( +
+
+ {isRunning ? ( + <> + +
+ + Agent is working + + {status?.currentTask && ( + + on{" "} + + {status.currentTask} + + + )} +
+
+ + Iteration {status?.iteration}/{status?.maxIterations} + + ✓ {status?.tasksCompleted.length ?? 0} + ✗ {status?.tasksFailed.length ?? 0} +
+ + ) : ( + <> + + + Agent paused + + + )} +
+
+ ); +} + +// ============================================================================ +// Task Card +// ============================================================================ + +function TaskCard({ + task, + onStartWithAgent, +}: { + task: Task; + onStartWithAgent: (task: Task) => void; +}) { + const updateTask = useUpdateTask(); + + const statusIcon = { + open: , + in_progress: , + blocked: , + closed: , + }; + + return ( +
+
+
+ {statusIcon[task.status]} +
+
+
+ + {task.id} + + {task.priority !== undefined && task.priority <= 1 && ( + + P{task.priority} + + )} +
+

{task.title}

+ {task.description && ( +

+ {task.description} +

+ )} +
+
+ {task.status !== "closed" && ( + <> + {task.status !== "in_progress" && ( + + )} + + + )} +
+
+
+ ); +} + +// ============================================================================ +// Tasks Tab Content +// ============================================================================ + +function TasksTabContent({ + onStartWithAgent, +}: { + onStartWithAgent: (task: Task) => void; +}) { + const { data: tasks, isLoading, error, refetch, isFetching } = useTasks(); + const { data: skills } = useSkills(); + const createTask = useCreateTask(); + const [newTaskTitle, setNewTaskTitle] = useState(""); + const [selectedSkillId, setSelectedSkillId] = useState(null); + const [isAdding, setIsAdding] = useState(false); + + const selectedSkill = skills?.find((s) => s.id === selectedSkillId); + + const handleAddTask = (e: React.FormEvent) => { + e.preventDefault(); + if (newTaskTitle.trim()) { + const description = selectedSkill + ? `Follow the instructions in skills/${selectedSkill.id}/SKILL.md` + : undefined; + + createTask.mutate( + { title: newTaskTitle.trim(), description }, + { + onSuccess: () => { + toast.success("Task created"); + setNewTaskTitle(""); + setSelectedSkillId(null); + setIsAdding(false); + }, + onError: (err) => { + toast.error(`Failed to create task: ${String(err)}`); + }, + }, + ); + } + }; + + if (isLoading) { + return ( +
+ + Loading tasks... +
+ ); + } + + if (error) { + return ( +
+ Error loading tasks: {String(error)} +
+ ); + } + + // Group tasks by status + const grouped = { + open: tasks?.filter((t) => t.status === "open") ?? [], + in_progress: tasks?.filter((t) => t.status === "in_progress") ?? [], + blocked: tasks?.filter((t) => t.status === "blocked") ?? [], + closed: tasks?.filter((t) => t.status === "closed") ?? [], + }; + + return ( +
+
+
+ {isFetching && !isLoading && ( + + )} +
+
+ + +
+
+ + {isAdding && ( +
+ setNewTaskTitle(e.target.value)} + placeholder="Task title..." + className="w-full px-3 py-2 text-sm border border-border rounded-md bg-background" + autoFocus + /> + + {skills && skills.length > 0 && ( +
+ Skill: + + {skills.map((skill) => ( + + ))} +
+ )} + +
+ + +
+
+ )} + + {/* Open & In Progress */} + {(grouped.open.length > 0 || grouped.in_progress.length > 0) && ( +
+

+ Open ({grouped.open.length + grouped.in_progress.length}) +

+
+ {grouped.in_progress.map((task) => ( + + ))} + {grouped.open.map((task) => ( + + ))} +
+
+ )} + + {/* Blocked */} + {grouped.blocked.length > 0 && ( +
+

+ + Blocked ({grouped.blocked.length}) +

+
+ {grouped.blocked.map((task) => ( + + ))} +
+
+ )} + + {/* Closed */} + {grouped.closed.length > 0 && ( +
+

+ + Completed ({grouped.closed.length}) +

+
+ {grouped.closed.slice(0, 5).map((task) => ( + + ))} + {grouped.closed.length > 5 && ( +
+ +{grouped.closed.length - 5} more completed +
+ )} +
+
+ )} + + {tasks?.length === 0 && ( +
+ +

No tasks yet

+

Create a task to get started

+
+ )} +
+ ); +} + +// ============================================================================ +// Skills Tab Content +// ============================================================================ + +function SkillsTabContent() { + const { data: skills, isLoading } = useSkills(); + + if (isLoading) { + return ( +
+ + Loading skills... +
+ ); + } + + if (!skills || skills.length === 0) { + return ( +
+ +

No skills found

+

+ Add a skills/ directory with SKILL.md files to define agent skills. +

+
+ ); + } + + return ( +
+

+ Skills are templates that provide context for tasks. Select a skill when + creating a task. +

+ {skills.map((skill: Skill) => ( +
+
+ +
+

{skill.name}

+

+ {skill.description} +

+ + skills/{skill.id}/SKILL.md + +
+
+
+ ))} +
+ ); +} + +// ============================================================================ +// Main Task Board +// ============================================================================ + +export default function TaskBoard() { + const { data: workspaceData } = useWorkspace(); + const { data: beadsStatus } = useBeadsStatus(); + const { org } = useParams({ strict: false }) as { org: string }; + const [activeTab, setActiveTab] = useState<"tasks" | "skills">("tasks"); + const updateTask = useUpdateTask(); + + const workspace = workspaceData?.workspace; + const hasBeads = beadsStatus?.initialized ?? false; + + /** + * Start task with agent - opens chat and sends the task to the agent + */ + const handleStartWithAgent = (task: Task) => { + if (!workspace) { + toast.error("No workspace set"); + return; + } + + // Mark task as in progress + updateTask.mutate( + { taskId: task.id, status: "in_progress" }, + { + onSuccess: () => { + // Build the prompt and send to chat + const prompt = buildTaskPrompt(task, workspace); + sendChatMessage(prompt); + toast.success(`Started task: ${task.title}`); + }, + }, + ); + }; + + return ( +
+ {/* Workspace Display */} + + + {workspace && ( + <> + {/* Show init banner if beads not initialized */} + {!hasBeads && } + + {/* Loop Controls */} + + + {/* Tabs */} +
+ {/* Tab Headers */} +
+ + +
+ + {/* Tab Content */} +
+ {activeTab === "tasks" && ( + + )} + {activeTab === "skills" && } +
+
+ + )} +
+ ); +} diff --git a/packages/mesh-plugin-task-runner/hooks/use-tasks.ts b/packages/mesh-plugin-task-runner/hooks/use-tasks.ts new file mode 100644 index 0000000000..3f8a56eba6 --- /dev/null +++ b/packages/mesh-plugin-task-runner/hooks/use-tasks.ts @@ -0,0 +1,473 @@ +/** + * Task Runner Hooks + * + * React Query hooks for the Task Runner plugin. + * Uses OBJECT_STORAGE_BINDING to share connections with File Storage. + */ + +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { usePluginContext } from "@decocms/bindings/plugins"; +import { OBJECT_STORAGE_BINDING } from "@decocms/bindings"; +import { QUERY_KEYS } from "../lib/query-keys"; + +/** + * Task type for Beads + */ +export interface Task { + id: string; + title: string; + description?: string; + status: "open" | "in_progress" | "blocked" | "closed"; + priority?: number; + createdAt?: string; + updatedAt?: string; + threadId?: string; // Chat thread ID for this task +} + +/** + * Hook to get current workspace (root path from the storage connection) + * GET_ROOT is an optional tool not in OBJECT_STORAGE_BINDING, so we check for it dynamically + */ +export function useWorkspace() { + const { connectionId, toolCaller, connection } = + usePluginContext(); + + return useQuery({ + queryKey: QUERY_KEYS.workspace, + queryFn: async () => { + // Check if GET_ROOT tool is available on this connection + const hasGetRoot = connection?.tools?.some((t) => t.name === "GET_ROOT"); + + if (!hasGetRoot) { + console.log("[Task Runner] Connection does not have GET_ROOT tool"); + return { workspace: null, hasBeads: false }; + } + + try { + // Call GET_ROOT to get the storage root path + // Cast to any since GET_ROOT is not part of the typed binding + const untypedToolCaller = toolCaller as ( + name: string, + args: Record, + ) => Promise<{ root: string }>; + const result = await untypedToolCaller("GET_ROOT", {}); + console.log("[Task Runner] GET_ROOT result:", result); + return { + workspace: result.root, + hasBeads: false, // Will be checked separately + }; + } catch (error) { + console.error("[Task Runner] GET_ROOT failed:", error); + return { workspace: null, hasBeads: false }; + } + }, + enabled: !!connectionId, + }); +} + +/** + * Hook to check if beads is initialized in the workspace + * This calls LIST_OBJECTS to check for .beads directory + */ +export function useBeadsStatus() { + const { connectionId, toolCaller } = + usePluginContext(); + + return useQuery({ + queryKey: QUERY_KEYS.beadsStatus, + queryFn: async () => { + try { + // Try to list .beads directory + const result = await toolCaller("LIST_OBJECTS", { + prefix: ".beads/", + maxKeys: 1, + }); + return { + initialized: + result.objects.length > 0 || + (result.commonPrefixes?.length ?? 0) > 0, + }; + } catch { + return { initialized: false }; + } + }, + enabled: !!connectionId, + }); +} + +/** + * Hook to list tasks from .beads directory + * Reads the beads tasks.json file to get task list + */ +export function useTasks() { + const { connectionId, toolCaller, connection } = + usePluginContext(); + + return useQuery({ + queryKey: QUERY_KEYS.tasks(connectionId ?? ""), + queryFn: async (): Promise => { + // Check if TASK_LIST tool is available + const hasTaskList = connection?.tools?.some( + (t) => t.name === "TASK_LIST", + ); + + if (!hasTaskList) { + console.log("[Task Runner] Connection does not have TASK_LIST tool"); + return []; + } + + try { + const untypedToolCaller = toolCaller as ( + name: string, + args: Record, + ) => Promise<{ tasks: Task[] }>; + + const result = await untypedToolCaller("TASK_LIST", { status: "all" }); + console.log("[Task Runner] TASK_LIST result:", result); + + // Handle different response formats + if (Array.isArray(result)) { + return result; + } + if (result && typeof result === "object" && "tasks" in result) { + return result.tasks; + } + return []; + } catch (error) { + console.error("[Task Runner] TASK_LIST failed:", error); + return []; + } + }, + enabled: !!connectionId, + staleTime: 0, // Always consider data stale + refetchOnMount: true, + refetchOnWindowFocus: true, + }); +} + +// Placeholder exports for components that still reference these +export function useLoopStatus() { + return useQuery({ + queryKey: ["task-runner", "loop-status"], + queryFn: async () => ({ + status: "idle", + currentTask: null, + iteration: 0, + maxIterations: 10, + totalTokens: 0, + maxTokens: 100000, + tasksCompleted: [], + tasksFailed: [], + startedAt: null, + lastActivity: null, + error: null, + }), + enabled: false, + }); +} + +export function useStartLoop() { + return useMutation({ + mutationFn: async (_params?: unknown) => { + throw new Error("Loop not implemented yet"); + }, + }); +} + +export function usePauseLoop() { + return useMutation({ + mutationFn: async () => { + throw new Error("Loop not implemented yet"); + }, + }); +} + +export function useStopLoop() { + return useMutation({ + mutationFn: async () => { + throw new Error("Loop not implemented yet"); + }, + }); +} + +export interface Skill { + id: string; + name: string; + description: string; + path: string; +} + +export function useSkills() { + const { connectionId, toolCaller, connection } = + usePluginContext(); + + return useQuery({ + queryKey: QUERY_KEYS.skills(connectionId ?? ""), + queryFn: async (): Promise => { + // Check if SKILLS_LIST tool is available + const hasSkillsList = connection?.tools?.some( + (t) => t.name === "SKILLS_LIST", + ); + + if (!hasSkillsList) { + console.log("[Task Runner] Connection does not have SKILLS_LIST tool"); + return []; + } + + try { + const untypedToolCaller = toolCaller as ( + name: string, + args: Record, + ) => Promise<{ skills: Skill[] }>; + + const result = await untypedToolCaller("SKILLS_LIST", {}); + return result.skills; + } catch (error) { + console.error("[Task Runner] SKILLS_LIST failed:", error); + return []; + } + }, + enabled: !!connectionId, + }); +} + +export function useApplySkill() { + const { connectionId, toolCaller, connection } = + usePluginContext(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (skillId: string) => { + // Read the skill file to get its content + const hasReadFile = connection?.tools?.some( + (t) => t.name === "read_file", + ); + + if (!hasReadFile) { + throw new Error("Connection doesn't support read_file"); + } + + const untypedToolCaller = toolCaller as ( + name: string, + args: Record, + ) => Promise<{ content: string } | string>; + + // Read the skill file + const skillPath = `skills/${skillId}/SKILL.md`; + const result = await untypedToolCaller("read_file", { path: skillPath }); + + // Handle both structured and text responses + const content = + typeof result === "string" + ? result + : typeof result === "object" && "content" in result + ? result.content + : String(result); + + // Create a task based on the skill + const hasTaskCreate = connection?.tools?.some( + (t) => t.name === "TASK_CREATE", + ); + + if (!hasTaskCreate) { + throw new Error("Connection doesn't support TASK_CREATE"); + } + + const createToolCaller = toolCaller as ( + name: string, + args: Record, + ) => Promise<{ task: Task }>; + + // Extract skill name from content (first heading after frontmatter) + const nameMatch = content.match(/^#\s+(.+)$/m); + const skillName = nameMatch?.[1] || skillId; + + const taskResult = await createToolCaller("TASK_CREATE", { + title: `Apply skill: ${skillName}`, + description: `Follow the instructions in skills/${skillId}/SKILL.md`, + priority: 1, + }); + + return taskResult.task; + }, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: QUERY_KEYS.tasks(connectionId ?? ""), + }); + }, + }); +} + +export function useSetWorkspace() { + // No-op for now - workspace is determined by the connection + return useMutation({ + mutationFn: async (_directory: string) => { + // Workspace is now determined by the storage connection + return { success: true }; + }, + }); +} + +export function useInitBeads() { + const { connectionId, toolCaller, connection } = + usePluginContext(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async () => { + // Check if BEADS_INIT tool is available + const hasBeadsInit = connection?.tools?.some( + (t) => t.name === "BEADS_INIT", + ); + + if (!hasBeadsInit) { + throw new Error( + "This storage connection doesn't support BEADS_INIT. Use a local-fs MCP.", + ); + } + + // Call BEADS_INIT to create .beads directory + const untypedToolCaller = toolCaller as ( + name: string, + args: Record, + ) => Promise<{ success: boolean; path: string; message: string }>; + + const result = await untypedToolCaller("BEADS_INIT", {}); + + if (!result.success) { + throw new Error("Failed to initialize Beads"); + } + + return result; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: QUERY_KEYS.beadsStatus }); + queryClient.invalidateQueries({ queryKey: QUERY_KEYS.workspace }); + }, + }); +} + +export function useCreateTask() { + const { connectionId, toolCaller, connection } = + usePluginContext(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (params: { + title: string; + description?: string; + priority?: number; + }) => { + // Check if TASK_CREATE tool is available + const hasTaskCreate = connection?.tools?.some( + (t) => t.name === "TASK_CREATE", + ); + + if (!hasTaskCreate) { + throw new Error( + "This storage connection doesn't support TASK_CREATE. Use a local-fs MCP.", + ); + } + + const untypedToolCaller = toolCaller as ( + name: string, + args: Record, + ) => Promise<{ task: Task }>; + + const result = await untypedToolCaller("TASK_CREATE", params); + return result.task; + }, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: QUERY_KEYS.tasks(connectionId ?? ""), + }); + }, + }); +} + +export function useUpdateTask() { + const { connectionId, toolCaller, connection } = + usePluginContext(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (params: { + taskId: string; + title?: string; + description?: string; + status?: "open" | "in_progress" | "blocked" | "closed"; + priority?: number; + threadId?: string; + }) => { + // Check if TASK_UPDATE tool is available + const hasTaskUpdate = connection?.tools?.some( + (t) => t.name === "TASK_UPDATE", + ); + + if (!hasTaskUpdate) { + throw new Error( + "This storage connection doesn't support TASK_UPDATE. Use a local-fs MCP.", + ); + } + + const untypedToolCaller = toolCaller as ( + name: string, + args: Record, + ) => Promise<{ task: Task }>; + + const result = await untypedToolCaller("TASK_UPDATE", { + id: params.taskId, + title: params.title, + description: params.description, + status: params.status, + priority: params.priority, + threadId: params.threadId, + }); + return result.task; + }, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: QUERY_KEYS.tasks(connectionId ?? ""), + }); + }, + }); +} + +export function useCloseTasks() { + const { connectionId, toolCaller, connection } = + usePluginContext(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (params: { taskIds: string[] }) => { + // Check if TASK_UPDATE tool is available + const hasTaskUpdate = connection?.tools?.some( + (t) => t.name === "TASK_UPDATE", + ); + + if (!hasTaskUpdate) { + throw new Error( + "This storage connection doesn't support TASK_UPDATE. Use a local-fs MCP.", + ); + } + + const untypedToolCaller = toolCaller as ( + name: string, + args: Record, + ) => Promise<{ task: Task }>; + + // Close each task + const results = await Promise.all( + params.taskIds.map((id) => + untypedToolCaller("TASK_UPDATE", { id, status: "closed" }), + ), + ); + return results.map((r) => r.task); + }, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: QUERY_KEYS.tasks(connectionId ?? ""), + }); + }, + }); +} diff --git a/packages/mesh-plugin-task-runner/index.tsx b/packages/mesh-plugin-task-runner/index.tsx new file mode 100644 index 0000000000..05eaea37d0 --- /dev/null +++ b/packages/mesh-plugin-task-runner/index.tsx @@ -0,0 +1,41 @@ +/** + * Task Runner Plugin + * + * Provides a task management UI with Beads integration and agent loops. + * Uses OBJECT_STORAGE_BINDING to share connections with the Files plugin. + * The workspace is derived from the storage connection's GET_ROOT tool. + */ + +import { OBJECT_STORAGE_BINDING } from "@decocms/bindings"; +import type { Plugin, PluginSetupContext } from "@decocms/bindings/plugins"; +import { File04 } from "@untitledui/icons"; +import { lazy } from "react"; +import { taskRunnerRouter } from "./lib/router"; + +// Lazy load components +const PluginHeader = lazy(() => import("./components/plugin-header")); +const PluginEmptyState = lazy(() => import("./components/plugin-empty-state")); + +/** + * Task Runner Plugin Definition + */ +export const taskRunnerPlugin: Plugin = { + id: "task-runner", + description: "Orchestrate AI agents with Beads tasks and agent loops", + binding: OBJECT_STORAGE_BINDING, + renderHeader: (props) => , + renderEmptyState: () => , + setup: (context: PluginSetupContext) => { + const { registerRootSidebarItem, registerPluginRoutes } = context; + + // Register sidebar item + registerRootSidebarItem({ + icon: , + label: "Tasks", + }); + + // Create and register plugin routes + const routes = taskRunnerRouter.createRoutes(context); + registerPluginRoutes(routes); + }, +}; diff --git a/packages/mesh-plugin-task-runner/lib/query-keys.ts b/packages/mesh-plugin-task-runner/lib/query-keys.ts new file mode 100644 index 0000000000..fdf58b612d --- /dev/null +++ b/packages/mesh-plugin-task-runner/lib/query-keys.ts @@ -0,0 +1,18 @@ +/** + * Query Keys for Task Runner + * + * Centralized query key definitions for React Query. + */ + +export const QUERY_KEYS = { + workspace: ["task-runner", "workspace"] as const, + beadsStatus: ["task-runner", "beads-status"] as const, + tasks: (connectionId: string) => + ["task-runner", "tasks", connectionId] as const, + readyTasks: (connectionId: string) => + ["task-runner", "ready", connectionId] as const, + loopStatus: (connectionId: string) => + ["task-runner", "loop", connectionId] as const, + skills: (connectionId: string) => + ["task-runner", "skills", connectionId] as const, +}; diff --git a/packages/mesh-plugin-task-runner/lib/router.ts b/packages/mesh-plugin-task-runner/lib/router.ts new file mode 100644 index 0000000000..d7af9a9362 --- /dev/null +++ b/packages/mesh-plugin-task-runner/lib/router.ts @@ -0,0 +1,37 @@ +/** + * Task Runner Plugin Router + * + * Provides typed routing utilities for the task runner plugin. + */ + +import { createPluginRouter } from "@decocms/bindings/plugins"; +import * as z from "zod"; + +/** + * Search schema for the task board route. + */ +const taskBoardSearchSchema = z.object({ + view: z.enum(["board", "list"]).optional().default("board"), + filter: z + .enum(["all", "ready", "in_progress", "blocked"]) + .optional() + .default("all"), +}); + +export type TaskBoardSearch = z.infer; + +/** + * Plugin router with typed hooks for navigation and search params. + */ +export const taskRunnerRouter = createPluginRouter((ctx) => { + const { createRoute, lazyRouteComponent } = ctx.routing; + + const indexRoute = createRoute({ + getParentRoute: () => ctx.parentRoute, + path: "/", + component: lazyRouteComponent(() => import("../components/task-board")), + validateSearch: taskBoardSearchSchema, + }); + + return [indexRoute]; +}); diff --git a/packages/mesh-plugin-task-runner/package.json b/packages/mesh-plugin-task-runner/package.json new file mode 100644 index 0000000000..7ac24a0c64 --- /dev/null +++ b/packages/mesh-plugin-task-runner/package.json @@ -0,0 +1,20 @@ +{ + "name": "mesh-plugin-task-runner", + "version": "0.1.0", + "private": true, + "type": "module", + "main": "./index.tsx", + "scripts": { + "check": "tsc --noEmit", + "test": "bun test" + }, + "dependencies": { + "@decocms/bindings": "workspace:*", + "@deco/ui": "workspace:*", + "@tanstack/react-query": "5.90.11", + "@untitledui/icons": "^0.0.19", + "react": "^19.2.0", + "sonner": "^2.0.7", + "zod": "^3.24.4" + } +} diff --git a/packages/mesh-plugin-task-runner/tsconfig.json b/packages/mesh-plugin-task-runner/tsconfig.json new file mode 100644 index 0000000000..1f06f5c860 --- /dev/null +++ b/packages/mesh-plugin-task-runner/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "noEmit": true + }, + "include": ["./**/*.ts", "./**/*.tsx"] +} diff --git a/skills/README.md b/skills/README.md new file mode 100644 index 0000000000..919f946e2c --- /dev/null +++ b/skills/README.md @@ -0,0 +1,31 @@ +# Mesh Skills + +This folder contains Agent Skills for AI-assisted development of the Mesh platform. + +## Available Skills + +| Skill | Description | +|-------|-------------| +| [mesh-development](mesh-development/SKILL.md) | Build features for MCP Mesh - coding conventions, plugins, tools, UI | + +## Skill Format + +Each skill is a folder containing: +- `SKILL.md` - Main skill file with YAML frontmatter (`name`, `description`) +- `references/` - Supporting documentation and examples + +## Using Skills + +Skills are automatically discovered by the Task Runner plugin when connected to this workspace via a local-fs MCP. + +1. Connect a local-fs MCP pointing to this repository +2. Open the Tasks plugin in Mesh +3. Skills appear in the Skills panel +4. Click "Apply" to create tasks based on a skill + +## Creating New Skills + +1. Copy an existing skill folder +2. Update the YAML frontmatter in `SKILL.md` +3. Add relevant content and references +4. The skill will auto-appear in the Tasks plugin diff --git a/skills/mesh-development/SKILL.md b/skills/mesh-development/SKILL.md new file mode 100644 index 0000000000..9e9b539407 --- /dev/null +++ b/skills/mesh-development/SKILL.md @@ -0,0 +1,174 @@ +--- +name: mesh-development +description: Build features for MCP Mesh - our full-stack MCP orchestration platform. Use when working on the mesh codebase, creating plugins, adding tools, or modifying the UI. +--- + +# Mesh Development Skill + +Build and maintain the MCP Mesh platform - a full-stack application for orchestrating MCP (Model Context Protocol) connections, tools, and AI agents. + +## When to Use This Skill + +- Building new features in the Mesh platform +- Creating or modifying plugins +- Adding MCP tools or bindings +- Working on the React client UI +- Modifying the Hono API server +- Database migrations or storage operations + +## Project Structure + +``` +mesh/ +├── apps/ +│ ├── mesh/ # Main application (Hono + Vite/React) +│ │ ├── src/ +│ │ │ ├── api/ # Hono server routes +│ │ │ ├── web/ # React client +│ │ │ ├── tools/ # MCP tool implementations +│ │ │ └── storage/# Database operations +│ │ └── migrations/ # Kysely migrations +│ └── docs/ # Astro documentation site +├── packages/ +│ ├── bindings/ # MCP bindings and connection abstractions +│ ├── runtime/ # MCP proxy, OAuth, tools runtime +│ ├── ui/ # Shared React components (shadcn-based) +│ ├── cli/ # CLI tooling +│ └── mesh-plugin-*/ # Plugin packages +└── plugins/ # Oxlint custom plugins +``` + +## Quick Start + +1. Run `bun install` to install dependencies +2. Run `bun run dev` to start development servers +3. Open `http://localhost:4000` for the client + +## Commands + +| Command | Description | +|---------|-------------| +| `bun run dev` | Start client + server with HMR | +| `bun run check` | TypeScript type checking | +| `bun run lint` | Run oxlint with custom plugins | +| `bun run fmt` | Format code with Biome | +| `bun test` | Run all tests | + +## Coding Conventions + +### TypeScript & React +- Use TypeScript types, avoid `any` +- React 19 with React Compiler (no manual memoization) +- Tailwind v4 for styling +- Use design system tokens (see [design-tokens.md](references/design-tokens.md)) + +### Naming +- Files: `kebab-case.ts` for shared packages +- Components/Classes: `PascalCase` +- Hooks/Utilities: `camelCase` +- Query keys: Use constants from `query-keys.ts` + +### Banned Patterns +- No `useEffect` - use alternatives (React Query, event handlers) +- No `useMemo`/`useCallback`/`memo` - React 19 compiler handles optimization +- No arbitrary Tailwind values - use design tokens + +### Formatting +- Two-space indentation +- Double quotes for strings +- Always run `bun run fmt` after changes + +## Creating Plugins + +Plugins extend Mesh with custom UI for MCP connections. See existing plugins in `packages/mesh-plugin-*/`. + +### Plugin Structure +```typescript +export const myPlugin: Plugin = { + id: "my-plugin", + description: "Description for users", + binding: MY_BINDING, + renderHeader: (props) => , + renderEmptyState: () => , + setup: (context) => { + context.registerRootSidebarItem({ + icon: , + label: "My Plugin", + }); + const routes = myRouter.createRoutes(context); + context.registerPluginRoutes(routes); + }, +}; +``` + +### Bindings +Bindings define which MCP connections a plugin can use. Create bindings in `packages/bindings/src/well-known/`. + +## Adding MCP Tools + +Tools are server-side functions exposed via MCP. Add tools in `apps/mesh/src/tools/`. + +### Tool Structure +```typescript +export function registerMyTool(server: McpServer) { + server.registerTool( + "MY_TOOL_NAME", + { + title: "My Tool", + description: "What this tool does", + inputSchema: { + param: z.string().describe("Parameter description"), + }, + }, + async (args) => { + // Tool implementation + return { + content: [{ type: "text", text: "Result" }], + structuredContent: { result: "data" }, + }; + }, + ); +} +``` + +## Database Operations + +Uses Kysely ORM. Migrations in `apps/mesh/migrations/`. + +### Creating Migrations +```typescript +// migrations/XXX-my-migration.ts +import { type Kysely } from "kysely"; + +export async function up(db: Kysely): Promise { + await db.schema + .createTable("my_table") + .addColumn("id", "text", (col) => col.primaryKey()) + .execute(); +} + +export async function down(db: Kysely): Promise { + await db.schema.dropTable("my_table").execute(); +} +``` + +## Testing + +- Co-locate tests: `my-file.test.ts` next to `my-file.ts` +- Use Bun's test framework +- Run `bun test` before PRs + +## Commit Guidelines + +Follow Conventional Commits: +- `feat(scope): add new feature` +- `fix(scope): fix bug` +- `refactor(scope): code improvement` +- `docs(scope): documentation update` +- `[chore]: maintenance task` + +## Related Resources + +- [Design Tokens](references/design-tokens.md) +- [UI Components](references/ui-components.md) +- AGENTS.md in repository root diff --git a/skills/mesh-development/references/design-tokens.md b/skills/mesh-development/references/design-tokens.md new file mode 100644 index 0000000000..4b292f9303 --- /dev/null +++ b/skills/mesh-development/references/design-tokens.md @@ -0,0 +1,123 @@ +# Design Tokens + +Use these Tailwind design system tokens for consistent styling. Avoid arbitrary values. + +## Colors + +### Semantic Colors (Preferred) +``` +background / foreground - Page background & text +card / card-foreground - Card surfaces +popover / popover-foreground - Dropdowns, tooltips +primary / primary-foreground - Primary actions +secondary / secondary-foreground - Secondary actions +muted / muted-foreground - Disabled, subtle text +accent / accent-foreground - Hover states +destructive / destructive-foreground - Delete actions +border - Borders +input - Input borders +ring - Focus rings +``` + +### Usage Examples +```tsx +// Good - uses design tokens +
+ + + + + + + +``` + +### Card +```tsx +import { Card, CardHeader, CardContent } from "@decocms/ui"; + + + +

Card Title

+
+ +

Card content goes here

+
+
+``` + +### Dialog +```tsx +import { + Dialog, + DialogTrigger, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, +} from "@decocms/ui"; + + + + + + + + Dialog Title + Dialog description + +
Dialog content
+
+
+``` + +### Select +```tsx +import { + Select, + SelectTrigger, + SelectValue, + SelectContent, + SelectItem, +} from "@decocms/ui"; + + +``` + +### Toast +```tsx +import { toast } from "sonner"; + +// Success +toast.success("Operation completed"); + +// Error +toast.error("Something went wrong"); + +// With description +toast("Title", { + description: "More details here", +}); +``` + +## Icons + +Use icons from `@untitledui/icons`: + +```tsx +import { Home, Settings, Plus, Trash02 } from "@untitledui/icons"; + + + +``` + +Common icons: +- Navigation: `Home`, `Menu`, `ChevronLeft`, `ChevronRight`, `ChevronDown` +- Actions: `Plus`, `Trash02`, `Edit02`, `Copy`, `Download`, `Upload` +- Status: `Check`, `X`, `AlertCircle`, `Info`, `Loading01` +- Files: `File04`, `Folder`, `FolderOpen` + +## Patterns + +### Loading States +```tsx +import { Loading01 } from "@untitledui/icons"; + +{isLoading ? ( +
+ + Loading... +
+) : ( + +)} +``` + +### Empty States +```tsx +
+ +

No items yet

+

+ Get started by creating your first item. +

+ +
+``` + +### Form Layout +```tsx +
+
+ + +
+
+ +