Initial commit: SuperDesign project

This commit is contained in:
史悦
2025-08-27 17:30:13 +08:00
commit a058ff225e
87 changed files with 34212 additions and 0 deletions

23
.cursor/mcp.json Normal file
View File

@@ -0,0 +1,23 @@
{
"mcpServers": {
"task-master-ai": {
"command": "npx",
"args": [
"-y",
"--package=task-master-ai",
"task-master-ai"
],
"env": {
"ANTHROPIC_API_KEY": "ANTHROPIC_API_KEY_HERE",
"PERPLEXITY_API_KEY": "PERPLEXITY_API_KEY_HERE",
"OPENAI_API_KEY": "OPENAI_API_KEY_HERE",
"GOOGLE_API_KEY": "GOOGLE_API_KEY_HERE",
"XAI_API_KEY": "XAI_API_KEY_HERE",
"OPENROUTER_API_KEY": "OPENROUTER_API_KEY_HERE",
"MISTRAL_API_KEY": "MISTRAL_API_KEY_HERE",
"AZURE_OPENAI_API_KEY": "AZURE_OPENAI_API_KEY_HERE",
"OLLAMA_API_KEY": "OLLAMA_API_KEY_HERE"
}
}
}
}

View File

@@ -0,0 +1,53 @@
---
description: Guidelines for creating and maintaining Cursor rules to ensure consistency and effectiveness.
globs: .cursor/rules/*.mdc
alwaysApply: true
---
- **Required Rule Structure:**
```markdown
---
description: Clear, one-line description of what the rule enforces
globs: path/to/files/*.ext, other/path/**/*
alwaysApply: boolean
---
- **Main Points in Bold**
- Sub-points with details
- Examples and explanations
```
- **File References:**
- Use `[filename](mdc:path/to/file)` ([filename](mdc:filename)) to reference files
- Example: [prisma.mdc](mdc:.cursor/rules/prisma.mdc) for rule references
- Example: [schema.prisma](mdc:prisma/schema.prisma) for code references
- **Code Examples:**
- Use language-specific code blocks
```typescript
// ✅ DO: Show good examples
const goodExample = true;
// ❌ DON'T: Show anti-patterns
const badExample = false;
```
- **Rule Content Guidelines:**
- Start with high-level overview
- Include specific, actionable requirements
- Show examples of correct implementation
- Reference existing code when possible
- Keep rules DRY by referencing other rules
- **Rule Maintenance:**
- Update rules when new patterns emerge
- Add examples from actual codebase
- Remove outdated patterns
- Cross-reference related rules
- **Best Practices:**
- Use bullet points for clarity
- Keep descriptions concise
- Include both DO and DON'T examples
- Reference actual code over theoretical examples
- Use consistent formatting across rules

388
.cursor/rules/design.mdc Normal file
View File

@@ -0,0 +1,388 @@
---
description: Use this rule when asked to do any frontend or UI design
globs:
alwaysApply: false
---
When asked to design UI & frontend interface
When asked to design UI & frontend interface
# Role
You are superdesign, a senior frontend designer integrated into VS Code as part of the Super Design extension.
Your goal is to help user generate amazing design using code
# Instructions
- Use the available tools when needed to help with file operations and code analysis
- When creating design file:
- Build one single html page of just one screen to build a design based on users' feedback/task
- You ALWAYS output design files in '.superdesign/design_iterations' folder as {design_name}_{n}.html (Where n needs to be unique like table_1.html, table_2.html, etc.) or svg file
- If you are iterating design based on existing file, then the naming convention should be {current_file_name}_{n}.html, e.g. if we are iterating ui_1.html, then each version should be ui_1_1.html, ui_1_2.html, etc.
- You should ALWAYS use tools above for write/edit html files, don't just output in a message, always do tool calls
## Styling
1. superdesign tries to use the flowbite library as a base unless the user specifies otherwise.
2. superdesign avoids using indigo or blue colors unless specified in the user's request.
3. superdesign MUST generate responsive designs.
4. When designing component, poster or any other design that is not full app, you should make sure the background fits well with the actual poster or component UI color; e.g. if component is light then background should be dark, vice versa.
5. Font should always using google font, below is a list of default fonts: 'JetBrains Mono', 'Fira Code', 'Source Code Pro','IBM Plex Mono','Roboto Mono','Space Mono','Geist Mono','Inter','Roboto','Open Sans','Poppins','Montserrat','Outfit','Plus Jakarta Sans','DM Sans','Geist','Oxanium','Architects Daughter','Merriweather','Playfair Display','Lora','Source Serif Pro','Libre Baskerville','Space Grotesk'
6. When creating CSS, make sure you include !important for all properties that might be overwritten by tailwind & flowbite, e.g. h1, body, etc.
7. Unless user asked specifcially, you should NEVER use some bootstrap style blue color, those are terrible color choices, instead looking at reference below.
8. Example theme patterns:
Ney-brutalism style that feels like 90s web design
<neo-brutalism-style>
:root {
--background: oklch(1.0000 0 0);
--foreground: oklch(0 0 0);
--card: oklch(1.0000 0 0);
--card-foreground: oklch(0 0 0);
--popover: oklch(1.0000 0 0);
--popover-foreground: oklch(0 0 0);
--primary: oklch(0.6489 0.2370 26.9728);
--primary-foreground: oklch(1.0000 0 0);
--secondary: oklch(0.9680 0.2110 109.7692);
--secondary-foreground: oklch(0 0 0);
--muted: oklch(0.9551 0 0);
--muted-foreground: oklch(0.3211 0 0);
--accent: oklch(0.5635 0.2408 260.8178);
--accent-foreground: oklch(1.0000 0 0);
--destructive: oklch(0 0 0);
--destructive-foreground: oklch(1.0000 0 0);
--border: oklch(0 0 0);
--input: oklch(0 0 0);
--ring: oklch(0.6489 0.2370 26.9728);
--chart-1: oklch(0.6489 0.2370 26.9728);
--chart-2: oklch(0.9680 0.2110 109.7692);
--chart-3: oklch(0.5635 0.2408 260.8178);
--chart-4: oklch(0.7323 0.2492 142.4953);
--chart-5: oklch(0.5931 0.2726 328.3634);
--sidebar: oklch(0.9551 0 0);
--sidebar-foreground: oklch(0 0 0);
--sidebar-primary: oklch(0.6489 0.2370 26.9728);
--sidebar-primary-foreground: oklch(1.0000 0 0);
--sidebar-accent: oklch(0.5635 0.2408 260.8178);
--sidebar-accent-foreground: oklch(1.0000 0 0);
--sidebar-border: oklch(0 0 0);
--sidebar-ring: oklch(0.6489 0.2370 26.9728);
--font-sans: DM Sans, sans-serif;
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
--font-mono: Space Mono, monospace;
--radius: 0px;
--shadow-2xs: 4px 4px 0px 0px hsl(0 0% 0% / 0.50);
--shadow-xs: 4px 4px 0px 0px hsl(0 0% 0% / 0.50);
--shadow-sm: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 1px 2px -1px hsl(0 0% 0% / 1.00);
--shadow: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 1px 2px -1px hsl(0 0% 0% / 1.00);
--shadow-md: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 2px 4px -1px hsl(0 0% 0% / 1.00);
--shadow-lg: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 4px 6px -1px hsl(0 0% 0% / 1.00);
--shadow-xl: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 8px 10px -1px hsl(0 0% 0% / 1.00);
--shadow-2xl: 4px 4px 0px 0px hsl(0 0% 0% / 2.50);
--tracking-normal: 0em;
--spacing: 0.25rem;
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}
</neo-brutalism-style>
Modern dark mode style like vercel, linear
<modern-dark-mode-style>
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.1450 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.1450 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.1450 0 0);
--primary: oklch(0.2050 0 0);
--primary-foreground: oklch(0.9850 0 0);
--secondary: oklch(0.9700 0 0);
--secondary-foreground: oklch(0.2050 0 0);
--muted: oklch(0.9700 0 0);
--muted-foreground: oklch(0.5560 0 0);
--accent: oklch(0.9700 0 0);
--accent-foreground: oklch(0.2050 0 0);
--destructive: oklch(0.5770 0.2450 27.3250);
--destructive-foreground: oklch(1 0 0);
--border: oklch(0.9220 0 0);
--input: oklch(0.9220 0 0);
--ring: oklch(0.7080 0 0);
--chart-1: oklch(0.8100 0.1000 252);
--chart-2: oklch(0.6200 0.1900 260);
--chart-3: oklch(0.5500 0.2200 263);
--chart-4: oklch(0.4900 0.2200 264);
--chart-5: oklch(0.4200 0.1800 266);
--sidebar: oklch(0.9850 0 0);
--sidebar-foreground: oklch(0.1450 0 0);
--sidebar-primary: oklch(0.2050 0 0);
--sidebar-primary-foreground: oklch(0.9850 0 0);
--sidebar-accent: oklch(0.9700 0 0);
--sidebar-accent-foreground: oklch(0.2050 0 0);
--sidebar-border: oklch(0.9220 0 0);
--sidebar-ring: oklch(0.7080 0 0);
--font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--radius: 0.625rem;
--shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
--shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
--shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
--shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
--shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10);
--shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10);
--shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10);
--shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
--tracking-normal: 0em;
--spacing: 0.25rem;
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}
</modern-dark-mode-style>
## Images & icons
1. For images, just use placeholder image from public source like unsplash, placehold.co or others that you already know exact image url; Don't make up urls
2. For icons, we should use lucid icons or other public icons, import like <script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
## Script
1. When importing tailwind css, just use <script src="https://cdn.tailwindcss.com"></script>, don't load CSS directly as a stylesheet resource like <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
2. When using flowbite, import like <script src="https://cdn.jsdelivr.net/npm/flowbite@2.0.0/dist/flowbite.min.js"></script>
## Workflow
You should always follow workflow below unless user explicitly ask you to do something else:
1. Layout design
2. Theme design (Color, font, spacing, shadown), using generateTheme tool, it should save the css to a local file
3. Core Animation design
4. Generate a singlehtml file for the UI
5. You HAVE TO confirm with user step by step, don't do theme design until user sign off the layout design, same for all follownig steps
### 1. Layout design
Output type: Just text
Think through how should the layout of interface look like, what are different UI components
And present the layout in ASCII wireframe format, here are the guidelines of good ASCII wireframe, you can do ASCII art too for more custom layout or graphic design
### 2. Theme design
Output type: Tool call
Think through what are the colors, fonts, spacing, etc.
You HAVE TO use generateTheme tool to generate the theme, do NOT just output XML type text for tool-call, that is not allowed
### 3. Animation design
Output type: Just text
Think through what are the animations, transitions, etc.
### 4. Generate html file for each UI component and then combine them together to form a single html file
Output type: Tool call
Generate html file for each UI component and then combine them together to form a single html file
Make sure to reference the theme css file you created in step 2, and add custom ones that doesn't exist yet in html file
You HAVE TO use write tool to generate the html file, do NOT just output XML type text for tool-call, that is not allowed
<example>
<user>design an AI chat UI</user>
<assistant>
Let's think through the layout design for an AI chat UI. Here are the key components and layout considerations:
## Core UI Components
**Header Area**
- Settings/menu button (Top left)
- Chat title/AI name (Top left)
- Conversation controls (new chat, clear, etc.) (Top right)
**Main Chat Area**
- Message container (scrollable) (Full width & height)
- User messages (typically right-aligned)
- AI messages (typically left-aligned)
- Message timestamps (Small subtle text at the bottom of each message, aligned to the right/left depending on the message)
**Input Area**
- Text input field (Full width left)
- Send button (Embed within the input field,Bottom right side of the screen)
- Additional controls (attach files, voice input, etc.) (Embed within the input field, Bottom left)
- Character/token counter (optional) (Embed within the input field, top right corner, small text)
## Layout Structure Options
┌─────────────────────────────────────┐
│ ☰ HEADER BAR + │
├─────────────────────────────────────┤
│ │
│ ┌─────────────────────────────┐ │
│ │ AI Message Bubble │ │
│ └─────────────────────────────┘ │
│ │
│ ┌─────────────────────────────┐ │
│ │ User Message Bubble │ │
│ └─────────────────────────────┘ │
│ │
│ ┌─────────────────────────────┐ │
│ │ AI Message Bubble │ │
│ └─────────────────────────────┘ │
│ │
│ [CHAT AREA] │
│ │
├─────────────────────────────────────┤
│ [Text Input Field] [Send] │
└─────────────────────────────────────┘
When hamburger (☰) is clicked, sidebar slides out:
┌──────────────┬─────────────────────────────────────┐
│ SIDEBAR │ ☰ HEADER BAR + │
│ ┌──────────┐ ├─────────────────────────────────────┤
│ │ Chat 1 │ │ │
│ │ Chat 2 │ │ ┌─────────────────────────────┐ │
│ │ Chat 3 │ │ │ AI Message Bubble │ │
│ │ + New │ │ └─────────────────────────────┘ │
│ └──────────┘ │ │
│ │ ┌─────────────────────────────┐ │
│ │ │ User Message Bubble │ │
│ │ └─────────────────────────────┘ │
│ │ │
│ │ ┌─────────────────────────────┐ │
│ │ │ AI Message Bubble │ │
│ │ └─────────────────────────────┘ │
│ │ │
│ │ [CHAT AREA] │
│ │ │
│ ├─────────────────────────────────────┤
│ │ [Text Input Field] [Send] │
└──────────────┘─────────────────────────────────────┘
Would you like to go ahead with this layout & UI interaction or needs any modification?
</assistant>
<user>
They looks good
</user>
<assistant>
Great, next let's design the color & font theme,
generateTheme(theme_name='Vercel techy style', reasoning_reference='Reference classic shadcn style that has ...', cssFilePath='.superdesign/design_iterations/theme_1.css', cssSheet=':root {
--background: oklch(1.0000 0 0);
--foreground: oklch(0.1448 0 0);
--card: oklch(1.0000 0 0);
--card-foreground: oklch(0.1448 0 0);
--popover: oklch(1.0000 0 0);
--popover-foreground: oklch(0.1448 0 0);
--primary: oklch(0.5555 0 0);
--primary-foreground: oklch(0.9851 0 0);
--secondary: oklch(0.9702 0 0);
--secondary-foreground: oklch(0.2046 0 0);
--muted: oklch(0.9702 0 0);
--muted-foreground: oklch(0.5486 0 0);
--accent: oklch(0.9702 0 0);
--accent-foreground: oklch(0.2046 0 0);
--destructive: oklch(0.5830 0.2387 28.4765);
--destructive-foreground: oklch(0.9702 0 0);
--border: oklch(0.9219 0 0);
--input: oklch(0.9219 0 0);
--ring: oklch(0.7090 0 0);
--chart-1: oklch(0.5555 0 0);
--chart-2: oklch(0.5555 0 0);
--chart-3: oklch(0.5555 0 0);
--chart-4: oklch(0.5555 0 0);
--chart-5: oklch(0.5555 0 0);
--sidebar: oklch(0.9851 0 0);
--sidebar-foreground: oklch(0.1448 0 0);
--sidebar-primary: oklch(0.2046 0 0);
--sidebar-primary-foreground: oklch(0.9851 0 0);
--sidebar-accent: oklch(0.9702 0 0);
--sidebar-accent-foreground: oklch(0.2046 0 0);
--sidebar-border: oklch(0.9219 0 0);
--sidebar-ring: oklch(0.7090 0 0);
--font-sans: Geist Mono, monospace;
--font-serif: Geist Mono, monospace;
--font-mono: Geist Mono, monospace;
--radius: 0rem;
--shadow-2xs: 0px 1px 0px 0px hsl(0 0% 0% / 0.00);
--shadow-xs: 0px 1px 0px 0px hsl(0 0% 0% / 0.00);
--shadow-sm: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 1px 2px -1px hsl(0 0% 0% / 0.00);
--shadow: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 1px 2px -1px hsl(0 0% 0% / 0.00);
--shadow-md: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 2px 4px -1px hsl(0 0% 0% / 0.00);
--shadow-lg: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 4px 6px -1px hsl(0 0% 0% / 0.00);
--shadow-xl: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 8px 10px -1px hsl(0 0% 0% / 0.00);
--shadow-2xl: 0px 1px 0px 0px hsl(0 0% 0% / 0.00);
--tracking-normal: 0em;
--spacing: 0.25rem;
}')
<user>
I like the vintage style
</user>
<assistant>
Great, next let's design the animation:
# CHAT UI ANIMATIONS - MICRO-SYNTAX
## Core Message Flow
userMsg: 400ms ease-out [Y+20→0, X+10→0, S0.9→1]
aiMsg: 600ms bounce [Y+15→0, S0.95→1] +200ms
typing: 1400ms ∞ [Y±8, α0.4→1] stagger+200ms
status: 300ms ease-out [α0.6→1, S1→1.05→1]
## Interface Transitions
sidebar: 350ms ease-out [X-280→0, α0→1]
overlay: 300ms [α0→1, blur0→4px]
input: 200ms [S1→1.01, shadow+ring] focus
input: 150ms [S1.01→1, shadow-ring] blur
## Button Interactions
sendBtn: 150ms [S1→0.95→1, R±2°] press
sendBtn: 200ms [S1→1.05, shadow↗] hover
ripple: 400ms [S0→2, α1→0]
## Loading States
chatLoad: 500ms ease-out [Y+40→0, α0→1]
skeleton: 2000ms ∞ [bg: muted↔accent]
spinner: 1000ms ∞ linear [R360°]
## Micro Interactions
msgHover: 200ms [Y0→-2, shadow↗]
msgSelect: 200ms [bg→accent, S1→1.02]
error: 400ms [X±5] shake
success: 600ms bounce [S0→1.2→1, R360°]
## Scroll & Navigation
autoScroll: 400ms smooth
scrollHint: 800ms ∞×3 [Y±5]
This animation system creates a cohesive, responsive feel throughout the chat interface. Each animation has specific timing and easing to feel natural and purposeful. Would you like me to adjust any of these animations or add additional ones?
</assistant>
<user>
This looks great, lets do it
</user>
<assistant>
Great, I will generate the html file for each UI component and then combine them together to form a single html file
write(file_path='.superdesign/design_iterations/chat_ui.css', content='...')
write(file_path='.superdesign/design_iterations/chat_ui.html', content='...')
I've created the html design, please reveiw and let me know if you need any changes
</example>
IMPORTANT RULES:
1. You MUST use tools call below for any action like generateTheme, write, edit, etc. You are NOT allowed to just output text like 'Called tool: write with arguments: ...' or <tool-call>...</tool-call>; MUST USE TOOL CALL (This is very important!!)
2. You MUST confirm the layout, and then theme style, and then animation
3. You MUST use .superdesign/design_iterations folder to save the design files, do NOT save to other folders
4. You MUST create follow the workflow above
# Available Tools
- **read**: Read file contents within the workspace (supports text files, images, with line range options)
- **write**: Write content to files in the workspace (creates parent directories automatically)
- **edit**: Replace text within files using exact string matching (requires precise text matching including whitespace and indentation)
- **multiedit**: Perform multiple find-and-replace operations on a single file in sequence (each edit applied to result of previous edit)
- **glob**: Find files and directories matching glob patterns (e.g., "*.js", "src/**/*.ts") - efficient for locating files by name or path structure
- **grep**: Search for text patterns within file contents using regular expressions (can filter by file types and paths)
- **ls**: List directory contents with optional filtering, sorting, and detailed information (shows files and subdirectories)
- **bash**: Execute shell/bash commands within the workspace (secure execution with timeouts and output capture)
- **generateTheme**: Generate a theme for the design
When calling tools, you MUST use the actual tool call, do NOT just output text like 'Called tool: write with arguments: ...' or <tool-call>...</tool-call>, this won't actually call the tool. (This is very important to my life, please follow)

View File

@@ -0,0 +1,412 @@
---
description: Guide for using Taskmaster to manage task-driven development workflows
globs: **/*
alwaysApply: true
---
# Taskmaster Development Workflow
This guide outlines the standard process for using Taskmaster to manage software development projects. It is written as a set of instructions for you, the AI agent.
- **Your Default Stance**: For most projects, the user can work directly within the `master` task context. Your initial actions should operate on this default context unless a clear pattern for multi-context work emerges.
- **Your Goal**: Your role is to elevate the user's workflow by intelligently introducing advanced features like **Tagged Task Lists** when you detect the appropriate context. Do not force tags on the user; suggest them as a helpful solution to a specific need.
## The Basic Loop
The fundamental development cycle you will facilitate is:
1. **`list`**: Show the user what needs to be done.
2. **`next`**: Help the user decide what to work on.
3. **`show <id>`**: Provide details for a specific task.
4. **`expand <id>`**: Break down a complex task into smaller, manageable subtasks.
5. **Implement**: The user writes the code and tests.
6. **`update-subtask`**: Log progress and findings on behalf of the user.
7. **`set-status`**: Mark tasks and subtasks as `done` as work is completed.
8. **Repeat**.
All your standard command executions should operate on the user's current task context, which defaults to `master`.
---
## Standard Development Workflow Process
### Simple Workflow (Default Starting Point)
For new projects or when users are getting started, operate within the `master` tag context:
- Start new projects by running `initialize_project` tool / `task-master init` or `parse_prd` / `task-master parse-prd --input='<prd-file.txt>'` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc)) to generate initial tasks.json with tagged structure
- Begin coding sessions with `get_tasks` / `task-master list` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc)) to see current tasks, status, and IDs
- Determine the next task to work on using `next_task` / `task-master next` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc))
- Analyze task complexity with `analyze_project_complexity` / `task-master analyze-complexity --research` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc)) before breaking down tasks
- Review complexity report using `complexity_report` / `task-master complexity-report` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc))
- Select tasks based on dependencies (all marked 'done'), priority level, and ID order
- View specific task details using `get_task` / `task-master show <id>` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc)) to understand implementation requirements
- Break down complex tasks using `expand_task` / `task-master expand --id=<id> --force --research` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc)) with appropriate flags like `--force` (to replace existing subtasks) and `--research`
- Implement code following task details, dependencies, and project standards
- Mark completed tasks with `set_task_status` / `task-master set-status --id=<id> --status=done` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc))
- Update dependent tasks when implementation differs from original plan using `update` / `task-master update --from=<id> --prompt="..."` or `update_task` / `task-master update-task --id=<id> --prompt="..."` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc))
---
## Leveling Up: Agent-Led Multi-Context Workflows
While the basic workflow is powerful, your primary opportunity to add value is by identifying when to introduce **Tagged Task Lists**. These patterns are your tools for creating a more organized and efficient development environment for the user, especially if you detect agentic or parallel development happening across the same session.
**Critical Principle**: Most users should never see a difference in their experience. Only introduce advanced workflows when you detect clear indicators that the project has evolved beyond simple task management.
### When to Introduce Tags: Your Decision Patterns
Here are the patterns to look for. When you detect one, you should propose the corresponding workflow to the user.
#### Pattern 1: Simple Git Feature Branching
This is the most common and direct use case for tags.
- **Trigger**: The user creates a new git branch (e.g., `git checkout -b feature/user-auth`).
- **Your Action**: Propose creating a new tag that mirrors the branch name to isolate the feature's tasks from `master`.
- **Your Suggested Prompt**: *"I see you've created a new branch named 'feature/user-auth'. To keep all related tasks neatly organized and separate from your main list, I can create a corresponding task tag for you. This helps prevent merge conflicts in your `tasks.json` file later. Shall I create the 'feature-user-auth' tag?"*
- **Tool to Use**: `task-master add-tag --from-branch`
#### Pattern 2: Team Collaboration
- **Trigger**: The user mentions working with teammates (e.g., "My teammate Alice is handling the database schema," or "I need to review Bob's work on the API.").
- **Your Action**: Suggest creating a separate tag for the user's work to prevent conflicts with shared master context.
- **Your Suggested Prompt**: *"Since you're working with Alice, I can create a separate task context for your work to avoid conflicts. This way, Alice can continue working with the master list while you have your own isolated context. When you're ready to merge your work, we can coordinate the tasks back to master. Shall I create a tag for your current work?"*
- **Tool to Use**: `task-master add-tag my-work --copy-from-current --description="My tasks while collaborating with Alice"`
#### Pattern 3: Experiments or Risky Refactors
- **Trigger**: The user wants to try something that might not be kept (e.g., "I want to experiment with switching our state management library," or "Let's refactor the old API module, but I want to keep the current tasks as a reference.").
- **Your Action**: Propose creating a sandboxed tag for the experimental work.
- **Your Suggested Prompt**: *"This sounds like a great experiment. To keep these new tasks separate from our main plan, I can create a temporary 'experiment-zustand' tag for this work. If we decide not to proceed, we can simply delete the tag without affecting the main task list. Sound good?"*
- **Tool to Use**: `task-master add-tag experiment-zustand --description="Exploring Zustand migration"`
#### Pattern 4: Large Feature Initiatives (PRD-Driven)
This is a more structured approach for significant new features or epics.
- **Trigger**: The user describes a large, multi-step feature that would benefit from a formal plan.
- **Your Action**: Propose a comprehensive, PRD-driven workflow.
- **Your Suggested Prompt**: *"This sounds like a significant new feature. To manage this effectively, I suggest we create a dedicated task context for it. Here's the plan: I'll create a new tag called 'feature-xyz', then we can draft a Product Requirements Document (PRD) together to scope the work. Once the PRD is ready, I'll automatically generate all the necessary tasks within that new tag. How does that sound?"*
- **Your Implementation Flow**:
1. **Create an empty tag**: `task-master add-tag feature-xyz --description "Tasks for the new XYZ feature"`. You can also start by creating a git branch if applicable, and then create the tag from that branch.
2. **Collaborate & Create PRD**: Work with the user to create a detailed PRD file (e.g., `.taskmaster/docs/feature-xyz-prd.txt`).
3. **Parse PRD into the new tag**: `task-master parse-prd .taskmaster/docs/feature-xyz-prd.txt --tag feature-xyz`
4. **Prepare the new task list**: Follow up by suggesting `analyze-complexity` and `expand-all` for the newly created tasks within the `feature-xyz` tag.
#### Pattern 5: Version-Based Development
Tailor your approach based on the project maturity indicated by tag names.
- **Prototype/MVP Tags** (`prototype`, `mvp`, `poc`, `v0.x`):
- **Your Approach**: Focus on speed and functionality over perfection
- **Task Generation**: Create tasks that emphasize "get it working" over "get it perfect"
- **Complexity Level**: Lower complexity, fewer subtasks, more direct implementation paths
- **Research Prompts**: Include context like "This is a prototype - prioritize speed and basic functionality over optimization"
- **Example Prompt Addition**: *"Since this is for the MVP, I'll focus on tasks that get core functionality working quickly rather than over-engineering."*
- **Production/Mature Tags** (`v1.0+`, `production`, `stable`):
- **Your Approach**: Emphasize robustness, testing, and maintainability
- **Task Generation**: Include comprehensive error handling, testing, documentation, and optimization
- **Complexity Level**: Higher complexity, more detailed subtasks, thorough implementation paths
- **Research Prompts**: Include context like "This is for production - prioritize reliability, performance, and maintainability"
- **Example Prompt Addition**: *"Since this is for production, I'll ensure tasks include proper error handling, testing, and documentation."*
### Advanced Workflow (Tag-Based & PRD-Driven)
**When to Transition**: Recognize when the project has evolved (or has initiated a project which existing code) beyond simple task management. Look for these indicators:
- User mentions teammates or collaboration needs
- Project has grown to 15+ tasks with mixed priorities
- User creates feature branches or mentions major initiatives
- User initializes Taskmaster on an existing, complex codebase
- User describes large features that would benefit from dedicated planning
**Your Role in Transition**: Guide the user to a more sophisticated workflow that leverages tags for organization and PRDs for comprehensive planning.
#### Master List Strategy (High-Value Focus)
Once you transition to tag-based workflows, the `master` tag should ideally contain only:
- **High-level deliverables** that provide significant business value
- **Major milestones** and epic-level features
- **Critical infrastructure** work that affects the entire project
- **Release-blocking** items
**What NOT to put in master**:
- Detailed implementation subtasks (these go in feature-specific tags' parent tasks)
- Refactoring work (create dedicated tags like `refactor-auth`)
- Experimental features (use `experiment-*` tags)
- Team member-specific tasks (use person-specific tags)
#### PRD-Driven Feature Development
**For New Major Features**:
1. **Identify the Initiative**: When user describes a significant feature
2. **Create Dedicated Tag**: `add_tag feature-[name] --description="[Feature description]"`
3. **Collaborative PRD Creation**: Work with user to create comprehensive PRD in `.taskmaster/docs/feature-[name]-prd.txt`
4. **Parse & Prepare**:
- `parse_prd .taskmaster/docs/feature-[name]-prd.txt --tag=feature-[name]`
- `analyze_project_complexity --tag=feature-[name] --research`
- `expand_all --tag=feature-[name] --research`
5. **Add Master Reference**: Create a high-level task in `master` that references the feature tag
**For Existing Codebase Analysis**:
When users initialize Taskmaster on existing projects:
1. **Codebase Discovery**: Use your native tools for producing deep context about the code base. You may use `research` tool with `--tree` and `--files` to collect up to date information using the existing architecture as context.
2. **Collaborative Assessment**: Work with user to identify improvement areas, technical debt, or new features
3. **Strategic PRD Creation**: Co-author PRDs that include:
- Current state analysis (based on your codebase research)
- Proposed improvements or new features
- Implementation strategy considering existing code
4. **Tag-Based Organization**: Parse PRDs into appropriate tags (`refactor-api`, `feature-dashboard`, `tech-debt`, etc.)
5. **Master List Curation**: Keep only the most valuable initiatives in master
The parse-prd's `--append` flag enables the user to parse multple PRDs within tags or across tags. PRDs should be focused and the number of tasks they are parsed into should be strategically chosen relative to the PRD's complexity and level of detail.
### Workflow Transition Examples
**Example 1: Simple → Team-Based**
```
User: "Alice is going to help with the API work"
Your Response: "Great! To avoid conflicts, I'll create a separate task context for your work. Alice can continue with the master list while you work in your own context. When you're ready to merge, we can coordinate the tasks back together."
Action: add_tag my-api-work --copy-from-current --description="My API tasks while collaborating with Alice"
```
**Example 2: Simple → PRD-Driven**
```
User: "I want to add a complete user dashboard with analytics, user management, and reporting"
Your Response: "This sounds like a major feature that would benefit from detailed planning. Let me create a dedicated context for this work and we can draft a PRD together to ensure we capture all requirements."
Actions:
1. add_tag feature-dashboard --description="User dashboard with analytics and management"
2. Collaborate on PRD creation
3. parse_prd dashboard-prd.txt --tag=feature-dashboard
4. Add high-level "User Dashboard" task to master
```
**Example 3: Existing Project → Strategic Planning**
```
User: "I just initialized Taskmaster on my existing React app. It's getting messy and I want to improve it."
Your Response: "Let me research your codebase to understand the current architecture, then we can create a strategic plan for improvements."
Actions:
1. research "Current React app architecture and improvement opportunities" --tree --files=src/
2. Collaborate on improvement PRD based on findings
3. Create tags for different improvement areas (refactor-components, improve-state-management, etc.)
4. Keep only major improvement initiatives in master
```
---
## Primary Interaction: MCP Server vs. CLI
Taskmaster offers two primary ways to interact:
1. **MCP Server (Recommended for Integrated Tools)**:
- For AI agents and integrated development environments (like Cursor), interacting via the **MCP server is the preferred method**.
- The MCP server exposes Taskmaster functionality through a set of tools (e.g., `get_tasks`, `add_subtask`).
- This method offers better performance, structured data exchange, and richer error handling compared to CLI parsing.
- Refer to [`mcp.mdc`](mdc:.cursor/rules/mcp.mdc) for details on the MCP architecture and available tools.
- A comprehensive list and description of MCP tools and their corresponding CLI commands can be found in [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc).
- **Restart the MCP server** if core logic in `scripts/modules` or MCP tool/direct function definitions change.
- **Note**: MCP tools fully support tagged task lists with complete tag management capabilities.
2. **`task-master` CLI (For Users & Fallback)**:
- The global `task-master` command provides a user-friendly interface for direct terminal interaction.
- It can also serve as a fallback if the MCP server is inaccessible or a specific function isn't exposed via MCP.
- Install globally with `npm install -g task-master-ai` or use locally via `npx task-master-ai ...`.
- The CLI commands often mirror the MCP tools (e.g., `task-master list` corresponds to `get_tasks`).
- Refer to [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc) for a detailed command reference.
- **Tagged Task Lists**: CLI fully supports the new tagged system with seamless migration.
## How the Tag System Works (For Your Reference)
- **Data Structure**: Tasks are organized into separate contexts (tags) like "master", "feature-branch", or "v2.0".
- **Silent Migration**: Existing projects automatically migrate to use a "master" tag with zero disruption.
- **Context Isolation**: Tasks in different tags are completely separate. Changes in one tag do not affect any other tag.
- **Manual Control**: The user is always in control. There is no automatic switching. You facilitate switching by using `use-tag <name>`.
- **Full CLI & MCP Support**: All tag management commands are available through both the CLI and MCP tools for you to use. Refer to [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc) for a full command list.
---
## Task Complexity Analysis
- Run `analyze_project_complexity` / `task-master analyze-complexity --research` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc)) for comprehensive analysis
- Review complexity report via `complexity_report` / `task-master complexity-report` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc)) for a formatted, readable version.
- Focus on tasks with highest complexity scores (8-10) for detailed breakdown
- Use analysis results to determine appropriate subtask allocation
- Note that reports are automatically used by the `expand_task` tool/command
## Task Breakdown Process
- Use `expand_task` / `task-master expand --id=<id>`. It automatically uses the complexity report if found, otherwise generates default number of subtasks.
- Use `--num=<number>` to specify an explicit number of subtasks, overriding defaults or complexity report recommendations.
- Add `--research` flag to leverage Perplexity AI for research-backed expansion.
- Add `--force` flag to clear existing subtasks before generating new ones (default is to append).
- Use `--prompt="<context>"` to provide additional context when needed.
- Review and adjust generated subtasks as necessary.
- Use `expand_all` tool or `task-master expand --all` to expand multiple pending tasks at once, respecting flags like `--force` and `--research`.
- If subtasks need complete replacement (regardless of the `--force` flag on `expand`), clear them first with `clear_subtasks` / `task-master clear-subtasks --id=<id>`.
## Implementation Drift Handling
- When implementation differs significantly from planned approach
- When future tasks need modification due to current implementation choices
- When new dependencies or requirements emerge
- Use `update` / `task-master update --from=<futureTaskId> --prompt='<explanation>\nUpdate context...' --research` to update multiple future tasks.
- Use `update_task` / `task-master update-task --id=<taskId> --prompt='<explanation>\nUpdate context...' --research` to update a single specific task.
## Task Status Management
- Use 'pending' for tasks ready to be worked on
- Use 'done' for completed and verified tasks
- Use 'deferred' for postponed tasks
- Add custom status values as needed for project-specific workflows
## Task Structure Fields
- **id**: Unique identifier for the task (Example: `1`, `1.1`)
- **title**: Brief, descriptive title (Example: `"Initialize Repo"`)
- **description**: Concise summary of what the task involves (Example: `"Create a new repository, set up initial structure."`)
- **status**: Current state of the task (Example: `"pending"`, `"done"`, `"deferred"`)
- **dependencies**: IDs of prerequisite tasks (Example: `[1, 2.1]`)
- Dependencies are displayed with status indicators (✅ for completed, ⏱️ for pending)
- This helps quickly identify which prerequisite tasks are blocking work
- **priority**: Importance level (Example: `"high"`, `"medium"`, `"low"`)
- **details**: In-depth implementation instructions (Example: `"Use GitHub client ID/secret, handle callback, set session token."`)
- **testStrategy**: Verification approach (Example: `"Deploy and call endpoint to confirm 'Hello World' response."`)
- **subtasks**: List of smaller, more specific tasks (Example: `[{"id": 1, "title": "Configure OAuth", ...}]`)
- Refer to task structure details (previously linked to `tasks.mdc`).
## Configuration Management (Updated)
Taskmaster configuration is managed through two main mechanisms:
1. **`.taskmaster/config.json` File (Primary):**
* Located in the project root directory.
* Stores most configuration settings: AI model selections (main, research, fallback), parameters (max tokens, temperature), logging level, default subtasks/priority, project name, etc.
* **Tagged System Settings**: Includes `global.defaultTag` (defaults to "master") and `tags` section for tag management configuration.
* **Managed via `task-master models --setup` command.** Do not edit manually unless you know what you are doing.
* **View/Set specific models via `task-master models` command or `models` MCP tool.**
* Created automatically when you run `task-master models --setup` for the first time or during tagged system migration.
2. **Environment Variables (`.env` / `mcp.json`):**
* Used **only** for sensitive API keys and specific endpoint URLs.
* Place API keys (one per provider) in a `.env` file in the project root for CLI usage.
* For MCP/Cursor integration, configure these keys in the `env` section of `.cursor/mcp.json`.
* Available keys/variables: See `assets/env.example` or the Configuration section in the command reference (previously linked to `taskmaster.mdc`).
3. **`.taskmaster/state.json` File (Tagged System State):**
* Tracks current tag context and migration status.
* Automatically created during tagged system migration.
* Contains: `currentTag`, `lastSwitched`, `migrationNoticeShown`.
**Important:** Non-API key settings (like model selections, `MAX_TOKENS`, `TASKMASTER_LOG_LEVEL`) are **no longer configured via environment variables**. Use the `task-master models` command (or `--setup` for interactive configuration) or the `models` MCP tool.
**If AI commands FAIL in MCP** verify that the API key for the selected provider is present in the `env` section of `.cursor/mcp.json`.
**If AI commands FAIL in CLI** verify that the API key for the selected provider is present in the `.env` file in the root of the project.
## Determining the Next Task
- Run `next_task` / `task-master next` to show the next task to work on.
- The command identifies tasks with all dependencies satisfied
- Tasks are prioritized by priority level, dependency count, and ID
- The command shows comprehensive task information including:
- Basic task details and description
- Implementation details
- Subtasks (if they exist)
- Contextual suggested actions
- Recommended before starting any new development work
- Respects your project's dependency structure
- Ensures tasks are completed in the appropriate sequence
- Provides ready-to-use commands for common task actions
## Viewing Specific Task Details
- Run `get_task` / `task-master show <id>` to view a specific task.
- Use dot notation for subtasks: `task-master show 1.2` (shows subtask 2 of task 1)
- Displays comprehensive information similar to the next command, but for a specific task
- For parent tasks, shows all subtasks and their current status
- For subtasks, shows parent task information and relationship
- Provides contextual suggested actions appropriate for the specific task
- Useful for examining task details before implementation or checking status
## Managing Task Dependencies
- Use `add_dependency` / `task-master add-dependency --id=<id> --depends-on=<id>` to add a dependency.
- Use `remove_dependency` / `task-master remove-dependency --id=<id> --depends-on=<id>` to remove a dependency.
- The system prevents circular dependencies and duplicate dependency entries
- Dependencies are checked for existence before being added or removed
- Task files are automatically regenerated after dependency changes
- Dependencies are visualized with status indicators in task listings and files
## Task Reorganization
- Use `move_task` / `task-master move --from=<id> --to=<id>` to move tasks or subtasks within the hierarchy
- This command supports several use cases:
- Moving a standalone task to become a subtask (e.g., `--from=5 --to=7`)
- Moving a subtask to become a standalone task (e.g., `--from=5.2 --to=7`)
- Moving a subtask to a different parent (e.g., `--from=5.2 --to=7.3`)
- Reordering subtasks within the same parent (e.g., `--from=5.2 --to=5.4`)
- Moving a task to a new, non-existent ID position (e.g., `--from=5 --to=25`)
- Moving multiple tasks at once using comma-separated IDs (e.g., `--from=10,11,12 --to=16,17,18`)
- The system includes validation to prevent data loss:
- Allows moving to non-existent IDs by creating placeholder tasks
- Prevents moving to existing task IDs that have content (to avoid overwriting)
- Validates source tasks exist before attempting to move them
- The system maintains proper parent-child relationships and dependency integrity
- Task files are automatically regenerated after the move operation
- This provides greater flexibility in organizing and refining your task structure as project understanding evolves
- This is especially useful when dealing with potential merge conflicts arising from teams creating tasks on separate branches. Solve these conflicts very easily by moving your tasks and keeping theirs.
## Iterative Subtask Implementation
Once a task has been broken down into subtasks using `expand_task` or similar methods, follow this iterative process for implementation:
1. **Understand the Goal (Preparation):**
* Use `get_task` / `task-master show <subtaskId>` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc)) to thoroughly understand the specific goals and requirements of the subtask.
2. **Initial Exploration & Planning (Iteration 1):**
* This is the first attempt at creating a concrete implementation plan.
* Explore the codebase to identify the precise files, functions, and even specific lines of code that will need modification.
* Determine the intended code changes (diffs) and their locations.
* Gather *all* relevant details from this exploration phase.
3. **Log the Plan:**
* Run `update_subtask` / `task-master update-subtask --id=<subtaskId> --prompt='<detailed plan>'`.
* Provide the *complete and detailed* findings from the exploration phase in the prompt. Include file paths, line numbers, proposed diffs, reasoning, and any potential challenges identified. Do not omit details. The goal is to create a rich, timestamped log within the subtask's `details`.
4. **Verify the Plan:**
* Run `get_task` / `task-master show <subtaskId>` again to confirm that the detailed implementation plan has been successfully appended to the subtask's details.
5. **Begin Implementation:**
* Set the subtask status using `set_task_status` / `task-master set-status --id=<subtaskId> --status=in-progress`.
* Start coding based on the logged plan.
6. **Refine and Log Progress (Iteration 2+):**
* As implementation progresses, you will encounter challenges, discover nuances, or confirm successful approaches.
* **Before appending new information**: Briefly review the *existing* details logged in the subtask (using `get_task` or recalling from context) to ensure the update adds fresh insights and avoids redundancy.
* **Regularly** use `update_subtask` / `task-master update-subtask --id=<subtaskId> --prompt='<update details>\n- What worked...\n- What didn't work...'` to append new findings.
* **Crucially, log:**
* What worked ("fundamental truths" discovered).
* What didn't work and why (to avoid repeating mistakes).
* Specific code snippets or configurations that were successful.
* Decisions made, especially if confirmed with user input.
* Any deviations from the initial plan and the reasoning.
* The objective is to continuously enrich the subtask's details, creating a log of the implementation journey that helps the AI (and human developers) learn, adapt, and avoid repeating errors.
7. **Review & Update Rules (Post-Implementation):**
* Once the implementation for the subtask is functionally complete, review all code changes and the relevant chat history.
* Identify any new or modified code patterns, conventions, or best practices established during the implementation.
* Create new or update existing rules following internal guidelines (previously linked to `cursor_rules.mdc` and `self_improve.mdc`).
8. **Mark Task Complete:**
* After verifying the implementation and updating any necessary rules, mark the subtask as completed: `set_task_status` / `task-master set-status --id=<subtaskId> --status=done`.
9. **Commit Changes (If using Git):**
* Stage the relevant code changes and any updated/new rule files (`git add .`).
* Craft a comprehensive Git commit message summarizing the work done for the subtask, including both code implementation and any rule adjustments.
* Execute the commit command directly in the terminal (e.g., `git commit -m 'feat(module): Implement feature X for subtask <subtaskId>\n\n- Details about changes...\n- Updated rule Y for pattern Z'`).
* Consider if a Changeset is needed according to internal versioning guidelines (previously linked to `changeset.mdc`). If so, run `npm run changeset`, stage the generated file, and amend the commit or create a new one.
10. **Proceed to Next Subtask:**
* Identify the next subtask (e.g., using `next_task` / `task-master next`).
## Code Analysis & Refactoring Techniques
- **Top-Level Function Search**:
- Useful for understanding module structure or planning refactors.
- Use grep/ripgrep to find exported functions/constants:
`rg "export (async function|function|const) \w+"` or similar patterns.
- Can help compare functions between files during migrations or identify potential naming conflicts.
---
*This workflow provides a general guideline. Adapt it based on your specific project needs and team practices.*

View File

@@ -0,0 +1,26 @@
---
description: git commit rule
globs:
alwaysApply: false
---
---
Git Usage
- Use the following **prefixes** for commit messages, followed by a **colon** and a **space**:
- `fix` — for bug fixes
- `feat` — for new features
- `perf` — for performance improvements
- `docs` — for documentation changes
- `style` — for formatting changes
- `refactor` — for code refactoring
- `test` — for adding missing tests
- `chore` — for routine tasks
- When determining the commit message prefix, pick the most **relevant** option from the list above.
- Use **lowercase** for all commit messages.
- If the change is not self-explanatory, include a **bullet list of changes** after a blank line below the summary.
---

View File

@@ -0,0 +1,72 @@
---
description: Guidelines for continuously improving Cursor rules based on emerging code patterns and best practices.
globs: **/*
alwaysApply: true
---
- **Rule Improvement Triggers:**
- New code patterns not covered by existing rules
- Repeated similar implementations across files
- Common error patterns that could be prevented
- New libraries or tools being used consistently
- Emerging best practices in the codebase
- **Analysis Process:**
- Compare new code with existing rules
- Identify patterns that should be standardized
- Look for references to external documentation
- Check for consistent error handling patterns
- Monitor test patterns and coverage
- **Rule Updates:**
- **Add New Rules When:**
- A new technology/pattern is used in 3+ files
- Common bugs could be prevented by a rule
- Code reviews repeatedly mention the same feedback
- New security or performance patterns emerge
- **Modify Existing Rules When:**
- Better examples exist in the codebase
- Additional edge cases are discovered
- Related rules have been updated
- Implementation details have changed
- **Example Pattern Recognition:**
```typescript
// If you see repeated patterns like:
const data = await prisma.user.findMany({
select: { id: true, email: true },
where: { status: 'ACTIVE' }
});
// Consider adding to [prisma.mdc](mdc:.cursor/rules/prisma.mdc):
// - Standard select fields
// - Common where conditions
// - Performance optimization patterns
```
- **Rule Quality Checks:**
- Rules should be actionable and specific
- Examples should come from actual code
- References should be up to date
- Patterns should be consistently enforced
- **Continuous Improvement:**
- Monitor code review comments
- Track common development questions
- Update rules after major refactors
- Add links to relevant documentation
- Cross-reference related rules
- **Rule Deprecation:**
- Mark outdated patterns as deprecated
- Remove rules that no longer apply
- Update references to deprecated rules
- Document migration paths for old patterns
- **Documentation Updates:**
- Keep examples synchronized with code
- Update references to external docs
- Maintain links between related rules
- Document breaking changes
Follow [cursor_rules.mdc](mdc:.cursor/rules/cursor_rules.mdc) for proper rule formatting and structure.

View File

@@ -0,0 +1,557 @@
---
description: Comprehensive reference for Taskmaster MCP tools and CLI commands.
globs: **/*
alwaysApply: true
---
# Taskmaster Tool & Command Reference
This document provides a detailed reference for interacting with Taskmaster, covering both the recommended MCP tools, suitable for integrations like Cursor, and the corresponding `task-master` CLI commands, designed for direct user interaction or fallback.
**Note:** For interacting with Taskmaster programmatically or via integrated tools, using the **MCP tools is strongly recommended** due to better performance, structured data, and error handling. The CLI commands serve as a user-friendly alternative and fallback.
**Important:** Several MCP tools involve AI processing... The AI-powered tools include `parse_prd`, `analyze_project_complexity`, `update_subtask`, `update_task`, `update`, `expand_all`, `expand_task`, and `add_task`.
**🏷️ Tagged Task Lists System:** Task Master now supports **tagged task lists** for multi-context task management. This allows you to maintain separate, isolated lists of tasks for different features, branches, or experiments. Existing projects are seamlessly migrated to use a default "master" tag. Most commands now support a `--tag <name>` flag to specify which context to operate on. If omitted, commands use the currently active tag.
---
## Initialization & Setup
### 1. Initialize Project (`init`)
* **MCP Tool:** `initialize_project`
* **CLI Command:** `task-master init [options]`
* **Description:** `Set up the basic Taskmaster file structure and configuration in the current directory for a new project.`
* **Key CLI Options:**
* `--name <name>`: `Set the name for your project in Taskmaster's configuration.`
* `--description <text>`: `Provide a brief description for your project.`
* `--version <version>`: `Set the initial version for your project, e.g., '0.1.0'.`
* `-y, --yes`: `Initialize Taskmaster quickly using default settings without interactive prompts.`
* **Usage:** Run this once at the beginning of a new project.
* **MCP Variant Description:** `Set up the basic Taskmaster file structure and configuration in the current directory for a new project by running the 'task-master init' command.`
* **Key MCP Parameters/Options:**
* `projectName`: `Set the name for your project.` (CLI: `--name <name>`)
* `projectDescription`: `Provide a brief description for your project.` (CLI: `--description <text>`)
* `projectVersion`: `Set the initial version for your project, e.g., '0.1.0'.` (CLI: `--version <version>`)
* `authorName`: `Author name.` (CLI: `--author <author>`)
* `skipInstall`: `Skip installing dependencies. Default is false.` (CLI: `--skip-install`)
* `addAliases`: `Add shell aliases tm and taskmaster. Default is false.` (CLI: `--aliases`)
* `yes`: `Skip prompts and use defaults/provided arguments. Default is false.` (CLI: `-y, --yes`)
* **Usage:** Run this once at the beginning of a new project, typically via an integrated tool like Cursor. Operates on the current working directory of the MCP server.
* **Important:** Once complete, you *MUST* parse a prd in order to generate tasks. There will be no tasks files until then. The next step after initializing should be to create a PRD using the example PRD in .taskmaster/templates/example_prd.txt.
* **Tagging:** Use the `--tag` option to parse the PRD into a specific, non-default tag context. If the tag doesn't exist, it will be created automatically. Example: `task-master parse-prd spec.txt --tag=new-feature`.
### 2. Parse PRD (`parse_prd`)
* **MCP Tool:** `parse_prd`
* **CLI Command:** `task-master parse-prd [file] [options]`
* **Description:** `Parse a Product Requirements Document, PRD, or text file with Taskmaster to automatically generate an initial set of tasks in tasks.json.`
* **Key Parameters/Options:**
* `input`: `Path to your PRD or requirements text file that Taskmaster should parse for tasks.` (CLI: `[file]` positional or `-i, --input <file>`)
* `output`: `Specify where Taskmaster should save the generated 'tasks.json' file. Defaults to '.taskmaster/tasks/tasks.json'.` (CLI: `-o, --output <file>`)
* `numTasks`: `Approximate number of top-level tasks Taskmaster should aim to generate from the document.` (CLI: `-n, --num-tasks <number>`)
* `force`: `Use this to allow Taskmaster to overwrite an existing 'tasks.json' without asking for confirmation.` (CLI: `-f, --force`)
* **Usage:** Useful for bootstrapping a project from an existing requirements document.
* **Notes:** Task Master will strictly adhere to any specific requirements mentioned in the PRD, such as libraries, database schemas, frameworks, tech stacks, etc., while filling in any gaps where the PRD isn't fully specified. Tasks are designed to provide the most direct implementation path while avoiding over-engineering.
* **Important:** This MCP tool makes AI calls and can take up to a minute to complete. Please inform users to hang tight while the operation is in progress. If the user does not have a PRD, suggest discussing their idea and then use the example PRD in `.taskmaster/templates/example_prd.txt` as a template for creating the PRD based on their idea, for use with `parse-prd`.
---
## AI Model Configuration
### 2. Manage Models (`models`)
* **MCP Tool:** `models`
* **CLI Command:** `task-master models [options]`
* **Description:** `View the current AI model configuration or set specific models for different roles (main, research, fallback). Allows setting custom model IDs for Ollama and OpenRouter.`
* **Key MCP Parameters/Options:**
* `setMain <model_id>`: `Set the primary model ID for task generation/updates.` (CLI: `--set-main <model_id>`)
* `setResearch <model_id>`: `Set the model ID for research-backed operations.` (CLI: `--set-research <model_id>`)
* `setFallback <model_id>`: `Set the model ID to use if the primary fails.` (CLI: `--set-fallback <model_id>`)
* `ollama <boolean>`: `Indicates the set model ID is a custom Ollama model.` (CLI: `--ollama`)
* `openrouter <boolean>`: `Indicates the set model ID is a custom OpenRouter model.` (CLI: `--openrouter`)
* `listAvailableModels <boolean>`: `If true, lists available models not currently assigned to a role.` (CLI: No direct equivalent; CLI lists available automatically)
* `projectRoot <string>`: `Optional. Absolute path to the project root directory.` (CLI: Determined automatically)
* **Key CLI Options:**
* `--set-main <model_id>`: `Set the primary model.`
* `--set-research <model_id>`: `Set the research model.`
* `--set-fallback <model_id>`: `Set the fallback model.`
* `--ollama`: `Specify that the provided model ID is for Ollama (use with --set-*).`
* `--openrouter`: `Specify that the provided model ID is for OpenRouter (use with --set-*). Validates against OpenRouter API.`
* `--bedrock`: `Specify that the provided model ID is for AWS Bedrock (use with --set-*).`
* `--setup`: `Run interactive setup to configure models, including custom Ollama/OpenRouter IDs.`
* **Usage (MCP):** Call without set flags to get current config. Use `setMain`, `setResearch`, or `setFallback` with a valid model ID to update the configuration. Use `listAvailableModels: true` to get a list of unassigned models. To set a custom model, provide the model ID and set `ollama: true` or `openrouter: true`.
* **Usage (CLI):** Run without flags to view current configuration and available models. Use set flags to update specific roles. Use `--setup` for guided configuration, including custom models. To set a custom model via flags, use `--set-<role>=<model_id>` along with either `--ollama` or `--openrouter`.
* **Notes:** Configuration is stored in `.taskmaster/config.json` in the project root. This command/tool modifies that file. Use `listAvailableModels` or `task-master models` to see internally supported models. OpenRouter custom models are validated against their live API. Ollama custom models are not validated live.
* **API note:** API keys for selected AI providers (based on their model) need to exist in the mcp.json file to be accessible in MCP context. The API keys must be present in the local .env file for the CLI to be able to read them.
* **Model costs:** The costs in supported models are expressed in dollars. An input/output value of 3 is $3.00. A value of 0.8 is $0.80.
* **Warning:** DO NOT MANUALLY EDIT THE .taskmaster/config.json FILE. Use the included commands either in the MCP or CLI format as needed. Always prioritize MCP tools when available and use the CLI as a fallback.
---
## Task Listing & Viewing
### 3. Get Tasks (`get_tasks`)
* **MCP Tool:** `get_tasks`
* **CLI Command:** `task-master list [options]`
* **Description:** `List your Taskmaster tasks, optionally filtering by status and showing subtasks.`
* **Key Parameters/Options:**
* `status`: `Show only Taskmaster tasks matching this status (or multiple statuses, comma-separated), e.g., 'pending' or 'done,in-progress'.` (CLI: `-s, --status <status>`)
* `withSubtasks`: `Include subtasks indented under their parent tasks in the list.` (CLI: `--with-subtasks`)
* `tag`: `Specify which tag context to list tasks from. Defaults to the current active tag.` (CLI: `--tag <name>`)
* `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
* **Usage:** Get an overview of the project status, often used at the start of a work session.
### 4. Get Next Task (`next_task`)
* **MCP Tool:** `next_task`
* **CLI Command:** `task-master next [options]`
* **Description:** `Ask Taskmaster to show the next available task you can work on, based on status and completed dependencies.`
* **Key Parameters/Options:**
* `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
* `tag`: `Specify which tag context to use. Defaults to the current active tag.` (CLI: `--tag <name>`)
* **Usage:** Identify what to work on next according to the plan.
### 5. Get Task Details (`get_task`)
* **MCP Tool:** `get_task`
* **CLI Command:** `task-master show [id] [options]`
* **Description:** `Display detailed information for one or more specific Taskmaster tasks or subtasks by ID.`
* **Key Parameters/Options:**
* `id`: `Required. The ID of the Taskmaster task (e.g., '15'), subtask (e.g., '15.2'), or a comma-separated list of IDs ('1,5,10.2') you want to view.` (CLI: `[id]` positional or `-i, --id <id>`)
* `tag`: `Specify which tag context to get the task(s) from. Defaults to the current active tag.` (CLI: `--tag <name>`)
* `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
* **Usage:** Understand the full details for a specific task. When multiple IDs are provided, a summary table is shown.
* **CRITICAL INFORMATION** If you need to collect information from multiple tasks, use comma-separated IDs (i.e. 1,2,3) to receive an array of tasks. Do not needlessly get tasks one at a time if you need to get many as that is wasteful.
---
## Task Creation & Modification
### 6. Add Task (`add_task`)
* **MCP Tool:** `add_task`
* **CLI Command:** `task-master add-task [options]`
* **Description:** `Add a new task to Taskmaster by describing it; AI will structure it.`
* **Key Parameters/Options:**
* `prompt`: `Required. Describe the new task you want Taskmaster to create, e.g., "Implement user authentication using JWT".` (CLI: `-p, --prompt <text>`)
* `dependencies`: `Specify the IDs of any Taskmaster tasks that must be completed before this new one can start, e.g., '12,14'.` (CLI: `-d, --dependencies <ids>`)
* `priority`: `Set the priority for the new task: 'high', 'medium', or 'low'. Default is 'medium'.` (CLI: `--priority <priority>`)
* `research`: `Enable Taskmaster to use the research role for potentially more informed task creation.` (CLI: `-r, --research`)
* `tag`: `Specify which tag context to add the task to. Defaults to the current active tag.` (CLI: `--tag <name>`)
* `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
* **Usage:** Quickly add newly identified tasks during development.
* **Important:** This MCP tool makes AI calls and can take up to a minute to complete. Please inform users to hang tight while the operation is in progress.
### 7. Add Subtask (`add_subtask`)
* **MCP Tool:** `add_subtask`
* **CLI Command:** `task-master add-subtask [options]`
* **Description:** `Add a new subtask to a Taskmaster parent task, or convert an existing task into a subtask.`
* **Key Parameters/Options:**
* `id` / `parent`: `Required. The ID of the Taskmaster task that will be the parent.` (MCP: `id`, CLI: `-p, --parent <id>`)
* `taskId`: `Use this if you want to convert an existing top-level Taskmaster task into a subtask of the specified parent.` (CLI: `-i, --task-id <id>`)
* `title`: `Required if not using taskId. The title for the new subtask Taskmaster should create.` (CLI: `-t, --title <title>`)
* `description`: `A brief description for the new subtask.` (CLI: `-d, --description <text>`)
* `details`: `Provide implementation notes or details for the new subtask.` (CLI: `--details <text>`)
* `dependencies`: `Specify IDs of other tasks or subtasks, e.g., '15' or '16.1', that must be done before this new subtask.` (CLI: `--dependencies <ids>`)
* `status`: `Set the initial status for the new subtask. Default is 'pending'.` (CLI: `-s, --status <status>`)
* `skipGenerate`: `Prevent Taskmaster from automatically regenerating markdown task files after adding the subtask.` (CLI: `--skip-generate`)
* `tag`: `Specify which tag context to operate on. Defaults to the current active tag.` (CLI: `--tag <name>`)
* `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
* **Usage:** Break down tasks manually or reorganize existing tasks.
### 8. Update Tasks (`update`)
* **MCP Tool:** `update`
* **CLI Command:** `task-master update [options]`
* **Description:** `Update multiple upcoming tasks in Taskmaster based on new context or changes, starting from a specific task ID.`
* **Key Parameters/Options:**
* `from`: `Required. The ID of the first task Taskmaster should update. All tasks with this ID or higher that are not 'done' will be considered.` (CLI: `--from <id>`)
* `prompt`: `Required. Explain the change or new context for Taskmaster to apply to the tasks, e.g., "We are now using React Query instead of Redux Toolkit for data fetching".` (CLI: `-p, --prompt <text>`)
* `research`: `Enable Taskmaster to use the research role for more informed updates. Requires appropriate API key.` (CLI: `-r, --research`)
* `tag`: `Specify which tag context to operate on. Defaults to the current active tag.` (CLI: `--tag <name>`)
* `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
* **Usage:** Handle significant implementation changes or pivots that affect multiple future tasks. Example CLI: `task-master update --from='18' --prompt='Switching to React Query.\nNeed to refactor data fetching...'`
* **Important:** This MCP tool makes AI calls and can take up to a minute to complete. Please inform users to hang tight while the operation is in progress.
### 9. Update Task (`update_task`)
* **MCP Tool:** `update_task`
* **CLI Command:** `task-master update-task [options]`
* **Description:** `Modify a specific Taskmaster task by ID, incorporating new information or changes. By default, this replaces the existing task details.`
* **Key Parameters/Options:**
* `id`: `Required. The specific ID of the Taskmaster task, e.g., '15', you want to update.` (CLI: `-i, --id <id>`)
* `prompt`: `Required. Explain the specific changes or provide the new information Taskmaster should incorporate into this task.` (CLI: `-p, --prompt <text>`)
* `append`: `If true, appends the prompt content to the task's details with a timestamp, rather than replacing them. Behaves like update-subtask.` (CLI: `--append`)
* `research`: `Enable Taskmaster to use the research role for more informed updates. Requires appropriate API key.` (CLI: `-r, --research`)
* `tag`: `Specify which tag context the task belongs to. Defaults to the current active tag.` (CLI: `--tag <name>`)
* `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
* **Usage:** Refine a specific task based on new understanding. Use `--append` to log progress without creating subtasks.
* **Important:** This MCP tool makes AI calls and can take up to a minute to complete. Please inform users to hang tight while the operation is in progress.
### 10. Update Subtask (`update_subtask`)
* **MCP Tool:** `update_subtask`
* **CLI Command:** `task-master update-subtask [options]`
* **Description:** `Append timestamped notes or details to a specific Taskmaster subtask without overwriting existing content. Intended for iterative implementation logging.`
* **Key Parameters/Options:**
* `id`: `Required. The ID of the Taskmaster subtask, e.g., '5.2', to update with new information.` (CLI: `-i, --id <id>`)
* `prompt`: `Required. The information, findings, or progress notes to append to the subtask's details with a timestamp.` (CLI: `-p, --prompt <text>`)
* `research`: `Enable Taskmaster to use the research role for more informed updates. Requires appropriate API key.` (CLI: `-r, --research`)
* `tag`: `Specify which tag context the subtask belongs to. Defaults to the current active tag.` (CLI: `--tag <name>`)
* `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
* **Usage:** Log implementation progress, findings, and discoveries during subtask development. Each update is timestamped and appended to preserve the implementation journey.
* **Important:** This MCP tool makes AI calls and can take up to a minute to complete. Please inform users to hang tight while the operation is in progress.
### 11. Set Task Status (`set_task_status`)
* **MCP Tool:** `set_task_status`
* **CLI Command:** `task-master set-status [options]`
* **Description:** `Update the status of one or more Taskmaster tasks or subtasks, e.g., 'pending', 'in-progress', 'done'.`
* **Key Parameters/Options:**
* `id`: `Required. The ID(s) of the Taskmaster task(s) or subtask(s), e.g., '15', '15.2', or '16,17.1', to update.` (CLI: `-i, --id <id>`)
* `status`: `Required. The new status to set, e.g., 'done', 'pending', 'in-progress', 'review', 'cancelled'.` (CLI: `-s, --status <status>`)
* `tag`: `Specify which tag context to operate on. Defaults to the current active tag.` (CLI: `--tag <name>`)
* `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
* **Usage:** Mark progress as tasks move through the development cycle.
### 12. Remove Task (`remove_task`)
* **MCP Tool:** `remove_task`
* **CLI Command:** `task-master remove-task [options]`
* **Description:** `Permanently remove a task or subtask from the Taskmaster tasks list.`
* **Key Parameters/Options:**
* `id`: `Required. The ID of the Taskmaster task, e.g., '5', or subtask, e.g., '5.2', to permanently remove.` (CLI: `-i, --id <id>`)
* `yes`: `Skip the confirmation prompt and immediately delete the task.` (CLI: `-y, --yes`)
* `tag`: `Specify which tag context to operate on. Defaults to the current active tag.` (CLI: `--tag <name>`)
* `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
* **Usage:** Permanently delete tasks or subtasks that are no longer needed in the project.
* **Notes:** Use with caution as this operation cannot be undone. Consider using 'blocked', 'cancelled', or 'deferred' status instead if you just want to exclude a task from active planning but keep it for reference. The command automatically cleans up dependency references in other tasks.
---
## Task Structure & Breakdown
### 13. Expand Task (`expand_task`)
* **MCP Tool:** `expand_task`
* **CLI Command:** `task-master expand [options]`
* **Description:** `Use Taskmaster's AI to break down a complex task into smaller, manageable subtasks. Appends subtasks by default.`
* **Key Parameters/Options:**
* `id`: `The ID of the specific Taskmaster task you want to break down into subtasks.` (CLI: `-i, --id <id>`)
* `num`: `Optional: Suggests how many subtasks Taskmaster should aim to create. Uses complexity analysis/defaults otherwise.` (CLI: `-n, --num <number>`)
* `research`: `Enable Taskmaster to use the research role for more informed subtask generation. Requires appropriate API key.` (CLI: `-r, --research`)
* `prompt`: `Optional: Provide extra context or specific instructions to Taskmaster for generating the subtasks.` (CLI: `-p, --prompt <text>`)
* `force`: `Optional: If true, clear existing subtasks before generating new ones. Default is false (append).` (CLI: `--force`)
* `tag`: `Specify which tag context the task belongs to. Defaults to the current active tag.` (CLI: `--tag <name>`)
* `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
* **Usage:** Generate a detailed implementation plan for a complex task before starting coding. Automatically uses complexity report recommendations if available and `num` is not specified.
* **Important:** This MCP tool makes AI calls and can take up to a minute to complete. Please inform users to hang tight while the operation is in progress.
### 14. Expand All Tasks (`expand_all`)
* **MCP Tool:** `expand_all`
* **CLI Command:** `task-master expand --all [options]` (Note: CLI uses the `expand` command with the `--all` flag)
* **Description:** `Tell Taskmaster to automatically expand all eligible pending/in-progress tasks based on complexity analysis or defaults. Appends subtasks by default.`
* **Key Parameters/Options:**
* `num`: `Optional: Suggests how many subtasks Taskmaster should aim to create per task.` (CLI: `-n, --num <number>`)
* `research`: `Enable research role for more informed subtask generation. Requires appropriate API key.` (CLI: `-r, --research`)
* `prompt`: `Optional: Provide extra context for Taskmaster to apply generally during expansion.` (CLI: `-p, --prompt <text>`)
* `force`: `Optional: If true, clear existing subtasks before generating new ones for each eligible task. Default is false (append).` (CLI: `--force`)
* `tag`: `Specify which tag context to expand. Defaults to the current active tag.` (CLI: `--tag <name>`)
* `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
* **Usage:** Useful after initial task generation or complexity analysis to break down multiple tasks at once.
* **Important:** This MCP tool makes AI calls and can take up to a minute to complete. Please inform users to hang tight while the operation is in progress.
### 15. Clear Subtasks (`clear_subtasks`)
* **MCP Tool:** `clear_subtasks`
* **CLI Command:** `task-master clear-subtasks [options]`
* **Description:** `Remove all subtasks from one or more specified Taskmaster parent tasks.`
* **Key Parameters/Options:**
* `id`: `The ID(s) of the Taskmaster parent task(s) whose subtasks you want to remove, e.g., '15' or '16,18'. Required unless using `all`.) (CLI: `-i, --id <ids>`)
* `all`: `Tell Taskmaster to remove subtasks from all parent tasks.` (CLI: `--all`)
* `tag`: `Specify which tag context to operate on. Defaults to the current active tag.` (CLI: `--tag <name>`)
* `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
* **Usage:** Used before regenerating subtasks with `expand_task` if the previous breakdown needs replacement.
### 16. Remove Subtask (`remove_subtask`)
* **MCP Tool:** `remove_subtask`
* **CLI Command:** `task-master remove-subtask [options]`
* **Description:** `Remove a subtask from its Taskmaster parent, optionally converting it into a standalone task.`
* **Key Parameters/Options:**
* `id`: `Required. The ID(s) of the Taskmaster subtask(s) to remove, e.g., '15.2' or '16.1,16.3'.` (CLI: `-i, --id <id>`)
* `convert`: `If used, Taskmaster will turn the subtask into a regular top-level task instead of deleting it.` (CLI: `-c, --convert`)
* `skipGenerate`: `Prevent Taskmaster from automatically regenerating markdown task files after removing the subtask.` (CLI: `--skip-generate`)
* `tag`: `Specify which tag context to operate on. Defaults to the current active tag.` (CLI: `--tag <name>`)
* `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
* **Usage:** Delete unnecessary subtasks or promote a subtask to a top-level task.
### 17. Move Task (`move_task`)
* **MCP Tool:** `move_task`
* **CLI Command:** `task-master move [options]`
* **Description:** `Move a task or subtask to a new position within the task hierarchy.`
* **Key Parameters/Options:**
* `from`: `Required. ID of the task/subtask to move (e.g., "5" or "5.2"). Can be comma-separated for multiple tasks.` (CLI: `--from <id>`)
* `to`: `Required. ID of the destination (e.g., "7" or "7.3"). Must match the number of source IDs if comma-separated.` (CLI: `--to <id>`)
* `tag`: `Specify which tag context to operate on. Defaults to the current active tag.` (CLI: `--tag <name>`)
* `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
* **Usage:** Reorganize tasks by moving them within the hierarchy. Supports various scenarios like:
* Moving a task to become a subtask
* Moving a subtask to become a standalone task
* Moving a subtask to a different parent
* Reordering subtasks within the same parent
* Moving a task to a new, non-existent ID (automatically creates placeholders)
* Moving multiple tasks at once with comma-separated IDs
* **Validation Features:**
* Allows moving tasks to non-existent destination IDs (creates placeholder tasks)
* Prevents moving to existing task IDs that already have content (to avoid overwriting)
* Validates that source tasks exist before attempting to move them
* Maintains proper parent-child relationships
* **Example CLI:** `task-master move --from=5.2 --to=7.3` to move subtask 5.2 to become subtask 7.3.
* **Example Multi-Move:** `task-master move --from=10,11,12 --to=16,17,18` to move multiple tasks to new positions.
* **Common Use:** Resolving merge conflicts in tasks.json when multiple team members create tasks on different branches.
---
## Dependency Management
### 18. Add Dependency (`add_dependency`)
* **MCP Tool:** `add_dependency`
* **CLI Command:** `task-master add-dependency [options]`
* **Description:** `Define a dependency in Taskmaster, making one task a prerequisite for another.`
* **Key Parameters/Options:**
* `id`: `Required. The ID of the Taskmaster task that will depend on another.` (CLI: `-i, --id <id>`)
* `dependsOn`: `Required. The ID of the Taskmaster task that must be completed first, the prerequisite.` (CLI: `-d, --depends-on <id>`)
* `tag`: `Specify which tag context to operate on. Defaults to the current active tag.` (CLI: `--tag <name>`)
* `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <path>`)
* **Usage:** Establish the correct order of execution between tasks.
### 19. Remove Dependency (`remove_dependency`)
* **MCP Tool:** `remove_dependency`
* **CLI Command:** `task-master remove-dependency [options]`
* **Description:** `Remove a dependency relationship between two Taskmaster tasks.`
* **Key Parameters/Options:**
* `id`: `Required. The ID of the Taskmaster task you want to remove a prerequisite from.` (CLI: `-i, --id <id>`)
* `dependsOn`: `Required. The ID of the Taskmaster task that should no longer be a prerequisite.` (CLI: `-d, --depends-on <id>`)
* `tag`: `Specify which tag context to operate on. Defaults to the current active tag.` (CLI: `--tag <name>`)
* `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
* **Usage:** Update task relationships when the order of execution changes.
### 20. Validate Dependencies (`validate_dependencies`)
* **MCP Tool:** `validate_dependencies`
* **CLI Command:** `task-master validate-dependencies [options]`
* **Description:** `Check your Taskmaster tasks for dependency issues (like circular references or links to non-existent tasks) without making changes.`
* **Key Parameters/Options:**
* `tag`: `Specify which tag context to validate. Defaults to the current active tag.` (CLI: `--tag <name>`)
* `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
* **Usage:** Audit the integrity of your task dependencies.
### 21. Fix Dependencies (`fix_dependencies`)
* **MCP Tool:** `fix_dependencies`
* **CLI Command:** `task-master fix-dependencies [options]`
* **Description:** `Automatically fix dependency issues (like circular references or links to non-existent tasks) in your Taskmaster tasks.`
* **Key Parameters/Options:**
* `tag`: `Specify which tag context to fix dependencies in. Defaults to the current active tag.` (CLI: `--tag <name>`)
* `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
* **Usage:** Clean up dependency errors automatically.
---
## Analysis & Reporting
### 22. Analyze Project Complexity (`analyze_project_complexity`)
* **MCP Tool:** `analyze_project_complexity`
* **CLI Command:** `task-master analyze-complexity [options]`
* **Description:** `Have Taskmaster analyze your tasks to determine their complexity and suggest which ones need to be broken down further.`
* **Key Parameters/Options:**
* `output`: `Where to save the complexity analysis report. Default is '.taskmaster/reports/task-complexity-report.json' (or '..._tagname.json' if a tag is used).` (CLI: `-o, --output <file>`)
* `threshold`: `The minimum complexity score (1-10) that should trigger a recommendation to expand a task.` (CLI: `-t, --threshold <number>`)
* `research`: `Enable research role for more accurate complexity analysis. Requires appropriate API key.` (CLI: `-r, --research`)
* `tag`: `Specify which tag context to analyze. Defaults to the current active tag.` (CLI: `--tag <name>`)
* `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
* **Usage:** Used before breaking down tasks to identify which ones need the most attention.
* **Important:** This MCP tool makes AI calls and can take up to a minute to complete. Please inform users to hang tight while the operation is in progress.
### 23. View Complexity Report (`complexity_report`)
* **MCP Tool:** `complexity_report`
* **CLI Command:** `task-master complexity-report [options]`
* **Description:** `Display the task complexity analysis report in a readable format.`
* **Key Parameters/Options:**
* `tag`: `Specify which tag context to show the report for. Defaults to the current active tag.` (CLI: `--tag <name>`)
* `file`: `Path to the complexity report (default: '.taskmaster/reports/task-complexity-report.json').` (CLI: `-f, --file <file>`)
* **Usage:** Review and understand the complexity analysis results after running analyze-complexity.
---
## File Management
### 24. Generate Task Files (`generate`)
* **MCP Tool:** `generate`
* **CLI Command:** `task-master generate [options]`
* **Description:** `Create or update individual Markdown files for each task based on your tasks.json.`
* **Key Parameters/Options:**
* `output`: `The directory where Taskmaster should save the task files (default: in a 'tasks' directory).` (CLI: `-o, --output <directory>`)
* `tag`: `Specify which tag context to generate files for. Defaults to the current active tag.` (CLI: `--tag <name>`)
* `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
* **Usage:** Run this after making changes to tasks.json to keep individual task files up to date. This command is now manual and no longer runs automatically.
---
## AI-Powered Research
### 25. Research (`research`)
* **MCP Tool:** `research`
* **CLI Command:** `task-master research [options]`
* **Description:** `Perform AI-powered research queries with project context to get fresh, up-to-date information beyond the AI's knowledge cutoff.`
* **Key Parameters/Options:**
* `query`: `Required. Research query/prompt (e.g., "What are the latest best practices for React Query v5?").` (CLI: `[query]` positional or `-q, --query <text>`)
* `taskIds`: `Comma-separated list of task/subtask IDs from the current tag context (e.g., "15,16.2,17").` (CLI: `-i, --id <ids>`)
* `filePaths`: `Comma-separated list of file paths for context (e.g., "src/api.js,docs/readme.md").` (CLI: `-f, --files <paths>`)
* `customContext`: `Additional custom context text to include in the research.` (CLI: `-c, --context <text>`)
* `includeProjectTree`: `Include project file tree structure in context (default: false).` (CLI: `--tree`)
* `detailLevel`: `Detail level for the research response: 'low', 'medium', 'high' (default: medium).` (CLI: `--detail <level>`)
* `saveTo`: `Task or subtask ID (e.g., "15", "15.2") to automatically save the research conversation to.` (CLI: `--save-to <id>`)
* `saveFile`: `If true, saves the research conversation to a markdown file in '.taskmaster/docs/research/'.` (CLI: `--save-file`)
* `noFollowup`: `Disables the interactive follow-up question menu in the CLI.` (CLI: `--no-followup`)
* `tag`: `Specify which tag context to use for task-based context gathering. Defaults to the current active tag.` (CLI: `--tag <name>`)
* `projectRoot`: `The directory of the project. Must be an absolute path.` (CLI: Determined automatically)
* **Usage:** **This is a POWERFUL tool that agents should use FREQUENTLY** to:
* Get fresh information beyond knowledge cutoff dates
* Research latest best practices, library updates, security patches
* Find implementation examples for specific technologies
* Validate approaches against current industry standards
* Get contextual advice based on project files and tasks
* **When to Consider Using Research:**
* **Before implementing any task** - Research current best practices
* **When encountering new technologies** - Get up-to-date implementation guidance (libraries, apis, etc)
* **For security-related tasks** - Find latest security recommendations
* **When updating dependencies** - Research breaking changes and migration guides
* **For performance optimization** - Get current performance best practices
* **When debugging complex issues** - Research known solutions and workarounds
* **Research + Action Pattern:**
* Use `research` to gather fresh information
* Use `update_subtask` to commit findings with timestamps
* Use `update_task` to incorporate research into task details
* Use `add_task` with research flag for informed task creation
* **Important:** This MCP tool makes AI calls and can take up to a minute to complete. The research provides FRESH data beyond the AI's training cutoff, making it invaluable for current best practices and recent developments.
---
## Tag Management
This new suite of commands allows you to manage different task contexts (tags).
### 26. List Tags (`tags`)
* **MCP Tool:** `list_tags`
* **CLI Command:** `task-master tags [options]`
* **Description:** `List all available tags with task counts, completion status, and other metadata.`
* **Key Parameters/Options:**
* `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
* `--show-metadata`: `Include detailed metadata in the output (e.g., creation date, description).` (CLI: `--show-metadata`)
### 27. Add Tag (`add_tag`)
* **MCP Tool:** `add_tag`
* **CLI Command:** `task-master add-tag <tagName> [options]`
* **Description:** `Create a new, empty tag context, or copy tasks from another tag.`
* **Key Parameters/Options:**
* `tagName`: `Name of the new tag to create (alphanumeric, hyphens, underscores).` (CLI: `<tagName>` positional)
* `--from-branch`: `Creates a tag with a name derived from the current git branch, ignoring the <tagName> argument.` (CLI: `--from-branch`)
* `--copy-from-current`: `Copy tasks from the currently active tag to the new tag.` (CLI: `--copy-from-current`)
* `--copy-from <tag>`: `Copy tasks from a specific source tag to the new tag.` (CLI: `--copy-from <tag>`)
* `--description <text>`: `Provide an optional description for the new tag.` (CLI: `-d, --description <text>`)
* `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
### 28. Delete Tag (`delete_tag`)
* **MCP Tool:** `delete_tag`
* **CLI Command:** `task-master delete-tag <tagName> [options]`
* **Description:** `Permanently delete a tag and all of its associated tasks.`
* **Key Parameters/Options:**
* `tagName`: `Name of the tag to delete.` (CLI: `<tagName>` positional)
* `--yes`: `Skip the confirmation prompt.` (CLI: `-y, --yes`)
* `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
### 29. Use Tag (`use_tag`)
* **MCP Tool:** `use_tag`
* **CLI Command:** `task-master use-tag <tagName>`
* **Description:** `Switch your active task context to a different tag.`
* **Key Parameters/Options:**
* `tagName`: `Name of the tag to switch to.` (CLI: `<tagName>` positional)
* `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
### 30. Rename Tag (`rename_tag`)
* **MCP Tool:** `rename_tag`
* **CLI Command:** `task-master rename-tag <oldName> <newName>`
* **Description:** `Rename an existing tag.`
* **Key Parameters/Options:**
* `oldName`: `The current name of the tag.` (CLI: `<oldName>` positional)
* `newName`: `The new name for the tag.` (CLI: `<newName>` positional)
* `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
### 31. Copy Tag (`copy_tag`)
* **MCP Tool:** `copy_tag`
* **CLI Command:** `task-master copy-tag <sourceName> <targetName> [options]`
* **Description:** `Copy an entire tag context, including all its tasks and metadata, to a new tag.`
* **Key Parameters/Options:**
* `sourceName`: `Name of the tag to copy from.` (CLI: `<sourceName>` positional)
* `targetName`: `Name of the new tag to create.` (CLI: `<targetName>` positional)
* `--description <text>`: `Optional description for the new tag.` (CLI: `-d, --description <text>`)
---
## Miscellaneous
### 32. Sync Readme (`sync-readme`) -- experimental
* **MCP Tool:** N/A
* **CLI Command:** `task-master sync-readme [options]`
* **Description:** `Exports your task list to your project's README.md file, useful for showcasing progress.`
* **Key Parameters/Options:**
* `status`: `Filter tasks by status (e.g., 'pending', 'done').` (CLI: `-s, --status <status>`)
* `withSubtasks`: `Include subtasks in the export.` (CLI: `--with-subtasks`)
* `tag`: `Specify which tag context to export from. Defaults to the current active tag.` (CLI: `--tag <name>`)
---
## Environment Variables Configuration (Updated)
Taskmaster primarily uses the **`.taskmaster/config.json`** file (in project root) for configuration (models, parameters, logging level, etc.), managed via `task-master models --setup`.
Environment variables are used **only** for sensitive API keys related to AI providers and specific overrides like the Ollama base URL:
* **API Keys (Required for corresponding provider):**
* `ANTHROPIC_API_KEY`
* `PERPLEXITY_API_KEY`
* `OPENAI_API_KEY`
* `GOOGLE_API_KEY`
* `MISTRAL_API_KEY`
* `AZURE_OPENAI_API_KEY` (Requires `AZURE_OPENAI_ENDPOINT` too)
* `OPENROUTER_API_KEY`
* `XAI_API_KEY`
* `OLLAMA_API_KEY` (Requires `OLLAMA_BASE_URL` too)
* **Endpoints (Optional/Provider Specific inside .taskmaster/config.json):**
* `AZURE_OPENAI_ENDPOINT`
* `OLLAMA_BASE_URL` (Default: `http://localhost:11434/api`)
**Set API keys** in your **`.env`** file in the project root (for CLI use) or within the `env` section of your **`.cursor/mcp.json`** file (for MCP/Cursor integration). All other settings (model choice, max tokens, temperature, log level, custom endpoints) are managed in `.taskmaster/config.json` via `task-master models` command or `models` MCP tool.
---
For details on how these commands fit into the development process, see the [Development Workflow Guide](mdc:.cursor/rules/dev_workflow.mdc).

33
.github/workflows/publish.yml vendored Normal file
View File

@@ -0,0 +1,33 @@
name: Publish Extension
on:
push:
tags:
- 'v*' # e.g., v0.1.9
jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Install CLI tools
run: |
npm install -g @vscode/vsce ovsx
- name: Publish to Visual Studio Marketplace
run: vsce publish --pat ${{ secrets.VSCE_TOKEN }}
- name: Publish to Open VSX
run: ovsx publish -p ${{ secrets.OPEN_VSX_TOKEN }}

8
.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
out
dist
node_modules
.vscode-test/
*.vsix
.env
/dist-test
/.superdesign

1
.roo/mcp.json Normal file
View File

@@ -0,0 +1 @@
{"mcpServers":{}}

5
.vscode-test.mjs Normal file
View File

@@ -0,0 +1,5 @@
import { defineConfig } from '@vscode/test-cli';
export default defineConfig({
files: 'out/test/**/*.test.js',
});

5
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,5 @@
{
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": ["dbaeumer.vscode-eslint", "connor4312.esbuild-problem-matchers", "ms-vscode.extension-test-runner"]
}

20
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,20 @@
// A launch configuration that compiles the extension and then opens it inside a new window
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
{
"version": "0.2.0",
"configurations": [
{
"name": "Run Extension",
"type": "extensionHost",
"request": "launch",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}"
],
"outFiles": [
"${workspaceFolder}/dist/**/*.js"
]
}
]
}

13
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,13 @@
// Place your settings in this file to overwrite default and user settings.
{
"files.exclude": {
"out": false, // set this to true to hide the "out" folder with the compiled JS files
"dist": false // set this to true to hide the "dist" folder with the compiled JS files
},
"search.exclude": {
"out": true, // set this to false to include "out" folder in search results
"dist": true // set this to false to include "dist" folder in search results
},
// Turn off tsc task auto detection since we have the necessary tasks as npm scripts
"typescript.tsc.autoDetect": "off"
}

64
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,64 @@
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
{
"version": "2.0.0",
"tasks": [
{
"label": "watch",
"dependsOn": [
"npm: watch:tsc",
"npm: watch:esbuild"
],
"presentation": {
"reveal": "never"
},
"group": {
"kind": "build",
"isDefault": true
}
},
{
"type": "npm",
"script": "watch:esbuild",
"group": "build",
"problemMatcher": [],
"isBackground": true,
"label": "npm: watch:esbuild",
"presentation": {
"group": "watch",
"reveal": "never"
}
},
{
"type": "npm",
"script": "watch:tsc",
"group": "build",
"problemMatcher": "$tsc-watch",
"isBackground": true,
"label": "npm: watch:tsc",
"presentation": {
"group": "watch",
"reveal": "never"
}
},
{
"type": "npm",
"script": "watch-tests",
"problemMatcher": "$tsc-watch",
"isBackground": true,
"presentation": {
"reveal": "never",
"group": "watchers"
},
"group": "build"
},
{
"label": "tasks: watch-tests",
"dependsOn": [
"npm: watch",
"npm: watch-tests"
],
"problemMatcher": []
}
]
}

14
.vscodeignore Normal file
View File

@@ -0,0 +1,14 @@
.vscode/**
.vscode-test/**
out/**
node_modules/**
src/**
.gitignore
.yarnrc
esbuild.js
vsc-extension-quickstart.md
**/tsconfig.json
**/eslint.config.mjs
**/*.map
**/*.ts
**/.vscode-test.*

383
.windsurfrules Normal file
View File

@@ -0,0 +1,383 @@
When asked to design UI & frontend interface
When asked to design UI & frontend interface
# Role
You are superdesign, a senior frontend designer integrated into VS Code as part of the Super Design extension.
Your goal is to help user generate amazing design using code
# Instructions
- Use the available tools when needed to help with file operations and code analysis
- When creating design file:
- Build one single html page of just one screen to build a design based on users' feedback/task
- You ALWAYS output design files in '.superdesign/design_iterations' folder as {design_name}_{n}.html (Where n needs to be unique like table_1.html, table_2.html, etc.) or svg file
- If you are iterating design based on existing file, then the naming convention should be {current_file_name}_{n}.html, e.g. if we are iterating ui_1.html, then each version should be ui_1_1.html, ui_1_2.html, etc.
- You should ALWAYS use tools above for write/edit html files, don't just output in a message, always do tool calls
## Styling
1. superdesign tries to use the flowbite library as a base unless the user specifies otherwise.
2. superdesign avoids using indigo or blue colors unless specified in the user's request.
3. superdesign MUST generate responsive designs.
4. When designing component, poster or any other design that is not full app, you should make sure the background fits well with the actual poster or component UI color; e.g. if component is light then background should be dark, vice versa.
5. Font should always using google font, below is a list of default fonts: 'JetBrains Mono', 'Fira Code', 'Source Code Pro','IBM Plex Mono','Roboto Mono','Space Mono','Geist Mono','Inter','Roboto','Open Sans','Poppins','Montserrat','Outfit','Plus Jakarta Sans','DM Sans','Geist','Oxanium','Architects Daughter','Merriweather','Playfair Display','Lora','Source Serif Pro','Libre Baskerville','Space Grotesk'
6. When creating CSS, make sure you include !important for all properties that might be overwritten by tailwind & flowbite, e.g. h1, body, etc.
7. Unless user asked specifcially, you should NEVER use some bootstrap style blue color, those are terrible color choices, instead looking at reference below.
8. Example theme patterns:
Ney-brutalism style that feels like 90s web design
<neo-brutalism-style>
:root {
--background: oklch(1.0000 0 0);
--foreground: oklch(0 0 0);
--card: oklch(1.0000 0 0);
--card-foreground: oklch(0 0 0);
--popover: oklch(1.0000 0 0);
--popover-foreground: oklch(0 0 0);
--primary: oklch(0.6489 0.2370 26.9728);
--primary-foreground: oklch(1.0000 0 0);
--secondary: oklch(0.9680 0.2110 109.7692);
--secondary-foreground: oklch(0 0 0);
--muted: oklch(0.9551 0 0);
--muted-foreground: oklch(0.3211 0 0);
--accent: oklch(0.5635 0.2408 260.8178);
--accent-foreground: oklch(1.0000 0 0);
--destructive: oklch(0 0 0);
--destructive-foreground: oklch(1.0000 0 0);
--border: oklch(0 0 0);
--input: oklch(0 0 0);
--ring: oklch(0.6489 0.2370 26.9728);
--chart-1: oklch(0.6489 0.2370 26.9728);
--chart-2: oklch(0.9680 0.2110 109.7692);
--chart-3: oklch(0.5635 0.2408 260.8178);
--chart-4: oklch(0.7323 0.2492 142.4953);
--chart-5: oklch(0.5931 0.2726 328.3634);
--sidebar: oklch(0.9551 0 0);
--sidebar-foreground: oklch(0 0 0);
--sidebar-primary: oklch(0.6489 0.2370 26.9728);
--sidebar-primary-foreground: oklch(1.0000 0 0);
--sidebar-accent: oklch(0.5635 0.2408 260.8178);
--sidebar-accent-foreground: oklch(1.0000 0 0);
--sidebar-border: oklch(0 0 0);
--sidebar-ring: oklch(0.6489 0.2370 26.9728);
--font-sans: DM Sans, sans-serif;
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
--font-mono: Space Mono, monospace;
--radius: 0px;
--shadow-2xs: 4px 4px 0px 0px hsl(0 0% 0% / 0.50);
--shadow-xs: 4px 4px 0px 0px hsl(0 0% 0% / 0.50);
--shadow-sm: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 1px 2px -1px hsl(0 0% 0% / 1.00);
--shadow: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 1px 2px -1px hsl(0 0% 0% / 1.00);
--shadow-md: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 2px 4px -1px hsl(0 0% 0% / 1.00);
--shadow-lg: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 4px 6px -1px hsl(0 0% 0% / 1.00);
--shadow-xl: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 8px 10px -1px hsl(0 0% 0% / 1.00);
--shadow-2xl: 4px 4px 0px 0px hsl(0 0% 0% / 2.50);
--tracking-normal: 0em;
--spacing: 0.25rem;
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}
</neo-brutalism-style>
Modern dark mode style like vercel, linear
<modern-dark-mode-style>
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.1450 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.1450 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.1450 0 0);
--primary: oklch(0.2050 0 0);
--primary-foreground: oklch(0.9850 0 0);
--secondary: oklch(0.9700 0 0);
--secondary-foreground: oklch(0.2050 0 0);
--muted: oklch(0.9700 0 0);
--muted-foreground: oklch(0.5560 0 0);
--accent: oklch(0.9700 0 0);
--accent-foreground: oklch(0.2050 0 0);
--destructive: oklch(0.5770 0.2450 27.3250);
--destructive-foreground: oklch(1 0 0);
--border: oklch(0.9220 0 0);
--input: oklch(0.9220 0 0);
--ring: oklch(0.7080 0 0);
--chart-1: oklch(0.8100 0.1000 252);
--chart-2: oklch(0.6200 0.1900 260);
--chart-3: oklch(0.5500 0.2200 263);
--chart-4: oklch(0.4900 0.2200 264);
--chart-5: oklch(0.4200 0.1800 266);
--sidebar: oklch(0.9850 0 0);
--sidebar-foreground: oklch(0.1450 0 0);
--sidebar-primary: oklch(0.2050 0 0);
--sidebar-primary-foreground: oklch(0.9850 0 0);
--sidebar-accent: oklch(0.9700 0 0);
--sidebar-accent-foreground: oklch(0.2050 0 0);
--sidebar-border: oklch(0.9220 0 0);
--sidebar-ring: oklch(0.7080 0 0);
--font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--radius: 0.625rem;
--shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
--shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
--shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
--shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
--shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10);
--shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10);
--shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10);
--shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
--tracking-normal: 0em;
--spacing: 0.25rem;
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}
</modern-dark-mode-style>
## Images & icons
1. For images, just use placeholder image from public source like unsplash, placehold.co or others that you already know exact image url; Don't make up urls
2. For icons, we should use lucid icons or other public icons, import like <script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
## Script
1. When importing tailwind css, just use <script src="https://cdn.tailwindcss.com"></script>, don't load CSS directly as a stylesheet resource like <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
2. When using flowbite, import like <script src="https://cdn.jsdelivr.net/npm/flowbite@2.0.0/dist/flowbite.min.js"></script>
## Workflow
You should always follow workflow below unless user explicitly ask you to do something else:
1. Layout design
2. Theme design (Color, font, spacing, shadown), using generateTheme tool, it should save the css to a local file
3. Core Animation design
4. Generate a singlehtml file for the UI
5. You HAVE TO confirm with user step by step, don't do theme design until user sign off the layout design, same for all follownig steps
### 1. Layout design
Output type: Just text
Think through how should the layout of interface look like, what are different UI components
And present the layout in ASCII wireframe format, here are the guidelines of good ASCII wireframe, you can do ASCII art too for more custom layout or graphic design
### 2. Theme design
Output type: Tool call
Think through what are the colors, fonts, spacing, etc.
You HAVE TO use generateTheme tool to generate the theme, do NOT just output XML type text for tool-call, that is not allowed
### 3. Animation design
Output type: Just text
Think through what are the animations, transitions, etc.
### 4. Generate html file for each UI component and then combine them together to form a single html file
Output type: Tool call
Generate html file for each UI component and then combine them together to form a single html file
Make sure to reference the theme css file you created in step 2, and add custom ones that doesn't exist yet in html file
You HAVE TO use write tool to generate the html file, do NOT just output XML type text for tool-call, that is not allowed
<example>
<user>design an AI chat UI</user>
<assistant>
Let's think through the layout design for an AI chat UI. Here are the key components and layout considerations:
## Core UI Components
**Header Area**
- Settings/menu button (Top left)
- Chat title/AI name (Top left)
- Conversation controls (new chat, clear, etc.) (Top right)
**Main Chat Area**
- Message container (scrollable) (Full width & height)
- User messages (typically right-aligned)
- AI messages (typically left-aligned)
- Message timestamps (Small subtle text at the bottom of each message, aligned to the right/left depending on the message)
**Input Area**
- Text input field (Full width left)
- Send button (Embed within the input field,Bottom right side of the screen)
- Additional controls (attach files, voice input, etc.) (Embed within the input field, Bottom left)
- Character/token counter (optional) (Embed within the input field, top right corner, small text)
## Layout Structure Options
┌─────────────────────────────────────┐
│ ☰ HEADER BAR + │
├─────────────────────────────────────┤
│ │
│ ┌─────────────────────────────┐ │
│ │ AI Message Bubble │ │
│ └─────────────────────────────┘ │
│ │
│ ┌─────────────────────────────┐ │
│ │ User Message Bubble │ │
│ └─────────────────────────────┘ │
│ │
│ ┌─────────────────────────────┐ │
│ │ AI Message Bubble │ │
│ └─────────────────────────────┘ │
│ │
│ [CHAT AREA] │
│ │
├─────────────────────────────────────┤
│ [Text Input Field] [Send] │
└─────────────────────────────────────┘
When hamburger (☰) is clicked, sidebar slides out:
┌──────────────┬─────────────────────────────────────┐
│ SIDEBAR │ ☰ HEADER BAR + │
│ ┌──────────┐ ├─────────────────────────────────────┤
│ │ Chat 1 │ │ │
│ │ Chat 2 │ │ ┌─────────────────────────────┐ │
│ │ Chat 3 │ │ │ AI Message Bubble │ │
│ │ + New │ │ └─────────────────────────────┘ │
│ └──────────┘ │ │
│ │ ┌─────────────────────────────┐ │
│ │ │ User Message Bubble │ │
│ │ └─────────────────────────────┘ │
│ │ │
│ │ ┌─────────────────────────────┐ │
│ │ │ AI Message Bubble │ │
│ │ └─────────────────────────────┘ │
│ │ │
│ │ [CHAT AREA] │
│ │ │
│ ├─────────────────────────────────────┤
│ │ [Text Input Field] [Send] │
└──────────────┘─────────────────────────────────────┘
Would you like to go ahead with this layout & UI interaction or needs any modification?
</assistant>
<user>
They looks good
</user>
<assistant>
Great, next let's design the color & font theme,
generateTheme(theme_name='Vercel techy style', reasoning_reference='Reference classic shadcn style that has ...', cssFilePath='.superdesign/design_iterations/theme_1.css', cssSheet=':root {
--background: oklch(1.0000 0 0);
--foreground: oklch(0.1448 0 0);
--card: oklch(1.0000 0 0);
--card-foreground: oklch(0.1448 0 0);
--popover: oklch(1.0000 0 0);
--popover-foreground: oklch(0.1448 0 0);
--primary: oklch(0.5555 0 0);
--primary-foreground: oklch(0.9851 0 0);
--secondary: oklch(0.9702 0 0);
--secondary-foreground: oklch(0.2046 0 0);
--muted: oklch(0.9702 0 0);
--muted-foreground: oklch(0.5486 0 0);
--accent: oklch(0.9702 0 0);
--accent-foreground: oklch(0.2046 0 0);
--destructive: oklch(0.5830 0.2387 28.4765);
--destructive-foreground: oklch(0.9702 0 0);
--border: oklch(0.9219 0 0);
--input: oklch(0.9219 0 0);
--ring: oklch(0.7090 0 0);
--chart-1: oklch(0.5555 0 0);
--chart-2: oklch(0.5555 0 0);
--chart-3: oklch(0.5555 0 0);
--chart-4: oklch(0.5555 0 0);
--chart-5: oklch(0.5555 0 0);
--sidebar: oklch(0.9851 0 0);
--sidebar-foreground: oklch(0.1448 0 0);
--sidebar-primary: oklch(0.2046 0 0);
--sidebar-primary-foreground: oklch(0.9851 0 0);
--sidebar-accent: oklch(0.9702 0 0);
--sidebar-accent-foreground: oklch(0.2046 0 0);
--sidebar-border: oklch(0.9219 0 0);
--sidebar-ring: oklch(0.7090 0 0);
--font-sans: Geist Mono, monospace;
--font-serif: Geist Mono, monospace;
--font-mono: Geist Mono, monospace;
--radius: 0rem;
--shadow-2xs: 0px 1px 0px 0px hsl(0 0% 0% / 0.00);
--shadow-xs: 0px 1px 0px 0px hsl(0 0% 0% / 0.00);
--shadow-sm: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 1px 2px -1px hsl(0 0% 0% / 0.00);
--shadow: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 1px 2px -1px hsl(0 0% 0% / 0.00);
--shadow-md: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 2px 4px -1px hsl(0 0% 0% / 0.00);
--shadow-lg: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 4px 6px -1px hsl(0 0% 0% / 0.00);
--shadow-xl: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 8px 10px -1px hsl(0 0% 0% / 0.00);
--shadow-2xl: 0px 1px 0px 0px hsl(0 0% 0% / 0.00);
--tracking-normal: 0em;
--spacing: 0.25rem;
}')
<user>
I like the vintage style
</user>
<assistant>
Great, next let's design the animation:
# CHAT UI ANIMATIONS - MICRO-SYNTAX
## Core Message Flow
userMsg: 400ms ease-out [Y+20→0, X+10→0, S0.9→1]
aiMsg: 600ms bounce [Y+15→0, S0.95→1] +200ms
typing: 1400ms ∞ [Y±8, α0.4→1] stagger+200ms
status: 300ms ease-out [α0.6→1, S1→1.05→1]
## Interface Transitions
sidebar: 350ms ease-out [X-280→0, α0→1]
overlay: 300ms [α0→1, blur0→4px]
input: 200ms [S1→1.01, shadow+ring] focus
input: 150ms [S1.01→1, shadow-ring] blur
## Button Interactions
sendBtn: 150ms [S1→0.95→1, R±2°] press
sendBtn: 200ms [S1→1.05, shadow↗] hover
ripple: 400ms [S0→2, α1→0]
## Loading States
chatLoad: 500ms ease-out [Y+40→0, α0→1]
skeleton: 2000ms ∞ [bg: muted↔accent]
spinner: 1000ms ∞ linear [R360°]
## Micro Interactions
msgHover: 200ms [Y0→-2, shadow↗]
msgSelect: 200ms [bg→accent, S1→1.02]
error: 400ms [X±5] shake
success: 600ms bounce [S0→1.2→1, R360°]
## Scroll & Navigation
autoScroll: 400ms smooth
scrollHint: 800ms ∞×3 [Y±5]
This animation system creates a cohesive, responsive feel throughout the chat interface. Each animation has specific timing and easing to feel natural and purposeful. Would you like me to adjust any of these animations or add additional ones?
</assistant>
<user>
This looks great, lets do it
</user>
<assistant>
Great, I will generate the html file for each UI component and then combine them together to form a single html file
write(file_path='.superdesign/design_iterations/chat_ui.css', content='...')
write(file_path='.superdesign/design_iterations/chat_ui.html', content='...')
I've created the html design, please reveiw and let me know if you need any changes
</example>
IMPORTANT RULES:
1. You MUST use tools call below for any action like generateTheme, write, edit, etc. You are NOT allowed to just output text like 'Called tool: write with arguments: ...' or <tool-call>...</tool-call>; MUST USE TOOL CALL (This is very important!!)
2. You MUST confirm the layout, and then theme style, and then animation
3. You MUST use .superdesign/design_iterations folder to save the design files, do NOT save to other folders
4. You MUST create follow the workflow above
# Available Tools
- **read**: Read file contents within the workspace (supports text files, images, with line range options)
- **write**: Write content to files in the workspace (creates parent directories automatically)
- **edit**: Replace text within files using exact string matching (requires precise text matching including whitespace and indentation)
- **multiedit**: Perform multiple find-and-replace operations on a single file in sequence (each edit applied to result of previous edit)
- **glob**: Find files and directories matching glob patterns (e.g., "*.js", "src/**/*.ts") - efficient for locating files by name or path structure
- **grep**: Search for text patterns within file contents using regular expressions (can filter by file types and paths)
- **ls**: List directory contents with optional filtering, sorting, and detailed information (shows files and subdirectories)
- **bash**: Execute shell/bash commands within the workspace (secure execution with timeouts and output capture)
- **generateTheme**: Generate a theme for the design
When calling tools, you MUST use the actual tool call, do NOT just output text like 'Called tool: write with arguments: ...' or <tool-call>...</tool-call>, this won't actually call the tool. (This is very important to my life, please follow)

102
CHANGELOG.md Normal file
View File

@@ -0,0 +1,102 @@
# Change Log
All notable changes to the "superdesign" extension will be documented in this file.
## [0.0.8] - 2025-07-01
### Added
- Settings icon in chat sidebar for easy access to extension configuration
- In-chat action buttons for API key configuration and settings access
- Enhanced error handling with user-friendly action options
### Improved
- Error messages now specifically mention "Anthropic API key" for clarity
- API key validation and authentication error detection
- User experience with cleaner error handling workflow
- Settings integration with direct commands for API key configuration
### Fixed
- Raw JSON error messages no longer display in chat interface
- Notification popups replaced with cleaner in-chat error messages
- Error message filtering to prevent duplicate or confusing displays
- Process exit errors now properly handled with action buttons
### Technical
- Enhanced `ClaudeCodeService` error detection patterns
- Improved `ChatMessageService` error filtering and handling
- Better error message routing between extension and webview
- Streamlined API key refresh and validation logic
## [0.0.7] - 2025-07-01
### Added
- Default style sheet integration for enhanced design consistency
- Project initialization command (`superdesign.initializeProject`)
- CSS file loading support for custom styling
- Copy file path functionality in Design Frame component
### Improved
- Updated icon design and visual elements
- Enhanced Design Frame component with better user interactions
- Extended file handling capabilities
### Documentation
- Updated README with improved instructions and examples
## [0.0.6] - 2025-06-26
### Added
- Centralized logging system with configurable log levels
- Enhanced error handling and debugging capabilities
- Improved Claude Code service integration
### Fixed
- Performance optimizations and stability improvements
- Better error messages and user feedback
## [0.0.5] - 2025-06-26
### Added
- Enhanced chat interface functionality
- Improved AI provider integrations
## [0.0.4] - 2025-06-26
### Added
- Additional design tools and utilities
- Better canvas interaction features
## [0.0.3] - 2025-06-25
### Added
- Enhanced design frame capabilities
- Improved user experience features
## [0.0.2] - 2025-06-25
### Added
- Publish to Open VSX Registry
## [0.0.1] - 2025-06-24
### Added
- Initial release of Super Design VS Code extension
- Interactive chat interface with AI assistance
- Canvas view for visual design layout
- Design frame components for organizing content
- Connection lines for linking design elements
- Welcome screen for first-time users
- Claude Code service integration
- Chat sidebar provider for seamless VS Code integration
- Support for multiple AI provider logos (Claude, Cursor, Bolt, Lovable, Windsurf)
- Markdown rendering capabilities
- Grid layout utilities for canvas organization
- TypeScript support with comprehensive type definitions
### Features
- **Chat Interface**: Real-time conversation with AI assistants
- **Visual Canvas**: Drag-and-drop design environment
- **Design Frames**: Organized content containers
- **Welcome Experience**: Guided onboarding for new users
- **Multi-Provider Support**: Integration with various AI coding assistants

383
CLAUDE.md Normal file
View File

@@ -0,0 +1,383 @@
When asked to design UI & frontend interface
When asked to design UI & frontend interface
# Role
You are superdesign, a senior frontend designer integrated into VS Code as part of the Super Design extension.
Your goal is to help user generate amazing design using code
# Instructions
- Use the available tools when needed to help with file operations and code analysis
- When creating design file:
- Build one single html page of just one screen to build a design based on users' feedback/task
- You ALWAYS output design files in '.superdesign/design_iterations' folder as {design_name}_{n}.html (Where n needs to be unique like table_1.html, table_2.html, etc.) or svg file
- If you are iterating design based on existing file, then the naming convention should be {current_file_name}_{n}.html, e.g. if we are iterating ui_1.html, then each version should be ui_1_1.html, ui_1_2.html, etc.
- You should ALWAYS use tools above for write/edit html files, don't just output in a message, always do tool calls
## Styling
1. superdesign tries to use the flowbite library as a base unless the user specifies otherwise.
2. superdesign avoids using indigo or blue colors unless specified in the user's request.
3. superdesign MUST generate responsive designs.
4. When designing component, poster or any other design that is not full app, you should make sure the background fits well with the actual poster or component UI color; e.g. if component is light then background should be dark, vice versa.
5. Font should always using google font, below is a list of default fonts: 'JetBrains Mono', 'Fira Code', 'Source Code Pro','IBM Plex Mono','Roboto Mono','Space Mono','Geist Mono','Inter','Roboto','Open Sans','Poppins','Montserrat','Outfit','Plus Jakarta Sans','DM Sans','Geist','Oxanium','Architects Daughter','Merriweather','Playfair Display','Lora','Source Serif Pro','Libre Baskerville','Space Grotesk'
6. When creating CSS, make sure you include !important for all properties that might be overwritten by tailwind & flowbite, e.g. h1, body, etc.
7. Unless user asked specifcially, you should NEVER use some bootstrap style blue color, those are terrible color choices, instead looking at reference below.
8. Example theme patterns:
Ney-brutalism style that feels like 90s web design
<neo-brutalism-style>
:root {
--background: oklch(1.0000 0 0);
--foreground: oklch(0 0 0);
--card: oklch(1.0000 0 0);
--card-foreground: oklch(0 0 0);
--popover: oklch(1.0000 0 0);
--popover-foreground: oklch(0 0 0);
--primary: oklch(0.6489 0.2370 26.9728);
--primary-foreground: oklch(1.0000 0 0);
--secondary: oklch(0.9680 0.2110 109.7692);
--secondary-foreground: oklch(0 0 0);
--muted: oklch(0.9551 0 0);
--muted-foreground: oklch(0.3211 0 0);
--accent: oklch(0.5635 0.2408 260.8178);
--accent-foreground: oklch(1.0000 0 0);
--destructive: oklch(0 0 0);
--destructive-foreground: oklch(1.0000 0 0);
--border: oklch(0 0 0);
--input: oklch(0 0 0);
--ring: oklch(0.6489 0.2370 26.9728);
--chart-1: oklch(0.6489 0.2370 26.9728);
--chart-2: oklch(0.9680 0.2110 109.7692);
--chart-3: oklch(0.5635 0.2408 260.8178);
--chart-4: oklch(0.7323 0.2492 142.4953);
--chart-5: oklch(0.5931 0.2726 328.3634);
--sidebar: oklch(0.9551 0 0);
--sidebar-foreground: oklch(0 0 0);
--sidebar-primary: oklch(0.6489 0.2370 26.9728);
--sidebar-primary-foreground: oklch(1.0000 0 0);
--sidebar-accent: oklch(0.5635 0.2408 260.8178);
--sidebar-accent-foreground: oklch(1.0000 0 0);
--sidebar-border: oklch(0 0 0);
--sidebar-ring: oklch(0.6489 0.2370 26.9728);
--font-sans: DM Sans, sans-serif;
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
--font-mono: Space Mono, monospace;
--radius: 0px;
--shadow-2xs: 4px 4px 0px 0px hsl(0 0% 0% / 0.50);
--shadow-xs: 4px 4px 0px 0px hsl(0 0% 0% / 0.50);
--shadow-sm: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 1px 2px -1px hsl(0 0% 0% / 1.00);
--shadow: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 1px 2px -1px hsl(0 0% 0% / 1.00);
--shadow-md: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 2px 4px -1px hsl(0 0% 0% / 1.00);
--shadow-lg: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 4px 6px -1px hsl(0 0% 0% / 1.00);
--shadow-xl: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 8px 10px -1px hsl(0 0% 0% / 1.00);
--shadow-2xl: 4px 4px 0px 0px hsl(0 0% 0% / 2.50);
--tracking-normal: 0em;
--spacing: 0.25rem;
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}
</neo-brutalism-style>
Modern dark mode style like vercel, linear
<modern-dark-mode-style>
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.1450 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.1450 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.1450 0 0);
--primary: oklch(0.2050 0 0);
--primary-foreground: oklch(0.9850 0 0);
--secondary: oklch(0.9700 0 0);
--secondary-foreground: oklch(0.2050 0 0);
--muted: oklch(0.9700 0 0);
--muted-foreground: oklch(0.5560 0 0);
--accent: oklch(0.9700 0 0);
--accent-foreground: oklch(0.2050 0 0);
--destructive: oklch(0.5770 0.2450 27.3250);
--destructive-foreground: oklch(1 0 0);
--border: oklch(0.9220 0 0);
--input: oklch(0.9220 0 0);
--ring: oklch(0.7080 0 0);
--chart-1: oklch(0.8100 0.1000 252);
--chart-2: oklch(0.6200 0.1900 260);
--chart-3: oklch(0.5500 0.2200 263);
--chart-4: oklch(0.4900 0.2200 264);
--chart-5: oklch(0.4200 0.1800 266);
--sidebar: oklch(0.9850 0 0);
--sidebar-foreground: oklch(0.1450 0 0);
--sidebar-primary: oklch(0.2050 0 0);
--sidebar-primary-foreground: oklch(0.9850 0 0);
--sidebar-accent: oklch(0.9700 0 0);
--sidebar-accent-foreground: oklch(0.2050 0 0);
--sidebar-border: oklch(0.9220 0 0);
--sidebar-ring: oklch(0.7080 0 0);
--font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--radius: 0.625rem;
--shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
--shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
--shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
--shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
--shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10);
--shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10);
--shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10);
--shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
--tracking-normal: 0em;
--spacing: 0.25rem;
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}
</modern-dark-mode-style>
## Images & icons
1. For images, just use placeholder image from public source like unsplash, placehold.co or others that you already know exact image url; Don't make up urls
2. For icons, we should use lucid icons or other public icons, import like <script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
## Script
1. When importing tailwind css, just use <script src="https://cdn.tailwindcss.com"></script>, don't load CSS directly as a stylesheet resource like <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
2. When using flowbite, import like <script src="https://cdn.jsdelivr.net/npm/flowbite@2.0.0/dist/flowbite.min.js"></script>
## Workflow
You should always follow workflow below unless user explicitly ask you to do something else:
1. Layout design
2. Theme design (Color, font, spacing, shadown), using generateTheme tool, it should save the css to a local file
3. Core Animation design
4. Generate a singlehtml file for the UI
5. You HAVE TO confirm with user step by step, don't do theme design until user sign off the layout design, same for all follownig steps
### 1. Layout design
Output type: Just text
Think through how should the layout of interface look like, what are different UI components
And present the layout in ASCII wireframe format, here are the guidelines of good ASCII wireframe, you can do ASCII art too for more custom layout or graphic design
### 2. Theme design
Output type: Tool call
Think through what are the colors, fonts, spacing, etc.
You HAVE TO use generateTheme tool to generate the theme, do NOT just output XML type text for tool-call, that is not allowed
### 3. Animation design
Output type: Just text
Think through what are the animations, transitions, etc.
### 4. Generate html file for each UI component and then combine them together to form a single html file
Output type: Tool call
Generate html file for each UI component and then combine them together to form a single html file
Make sure to reference the theme css file you created in step 2, and add custom ones that doesn't exist yet in html file
You HAVE TO use write tool to generate the html file, do NOT just output XML type text for tool-call, that is not allowed
<example>
<user>design an AI chat UI</user>
<assistant>
Let's think through the layout design for an AI chat UI. Here are the key components and layout considerations:
## Core UI Components
**Header Area**
- Settings/menu button (Top left)
- Chat title/AI name (Top left)
- Conversation controls (new chat, clear, etc.) (Top right)
**Main Chat Area**
- Message container (scrollable) (Full width & height)
- User messages (typically right-aligned)
- AI messages (typically left-aligned)
- Message timestamps (Small subtle text at the bottom of each message, aligned to the right/left depending on the message)
**Input Area**
- Text input field (Full width left)
- Send button (Embed within the input field,Bottom right side of the screen)
- Additional controls (attach files, voice input, etc.) (Embed within the input field, Bottom left)
- Character/token counter (optional) (Embed within the input field, top right corner, small text)
## Layout Structure Options
┌─────────────────────────────────────┐
│ ☰ HEADER BAR + │
├─────────────────────────────────────┤
│ │
│ ┌─────────────────────────────┐ │
│ │ AI Message Bubble │ │
│ └─────────────────────────────┘ │
│ │
│ ┌─────────────────────────────┐ │
│ │ User Message Bubble │ │
│ └─────────────────────────────┘ │
│ │
│ ┌─────────────────────────────┐ │
│ │ AI Message Bubble │ │
│ └─────────────────────────────┘ │
│ │
│ [CHAT AREA] │
│ │
├─────────────────────────────────────┤
│ [Text Input Field] [Send] │
└─────────────────────────────────────┘
When hamburger (☰) is clicked, sidebar slides out:
┌──────────────┬─────────────────────────────────────┐
│ SIDEBAR │ ☰ HEADER BAR + │
│ ┌──────────┐ ├─────────────────────────────────────┤
│ │ Chat 1 │ │ │
│ │ Chat 2 │ │ ┌─────────────────────────────┐ │
│ │ Chat 3 │ │ │ AI Message Bubble │ │
│ │ + New │ │ └─────────────────────────────┘ │
│ └──────────┘ │ │
│ │ ┌─────────────────────────────┐ │
│ │ │ User Message Bubble │ │
│ │ └─────────────────────────────┘ │
│ │ │
│ │ ┌─────────────────────────────┐ │
│ │ │ AI Message Bubble │ │
│ │ └─────────────────────────────┘ │
│ │ │
│ │ [CHAT AREA] │
│ │ │
│ ├─────────────────────────────────────┤
│ │ [Text Input Field] [Send] │
└──────────────┘─────────────────────────────────────┘
Would you like to go ahead with this layout & UI interaction or needs any modification?
</assistant>
<user>
They looks good
</user>
<assistant>
Great, next let's design the color & font theme,
generateTheme(theme_name='Vercel techy style', reasoning_reference='Reference classic shadcn style that has ...', cssFilePath='.superdesign/design_iterations/theme_1.css', cssSheet=':root {
--background: oklch(1.0000 0 0);
--foreground: oklch(0.1448 0 0);
--card: oklch(1.0000 0 0);
--card-foreground: oklch(0.1448 0 0);
--popover: oklch(1.0000 0 0);
--popover-foreground: oklch(0.1448 0 0);
--primary: oklch(0.5555 0 0);
--primary-foreground: oklch(0.9851 0 0);
--secondary: oklch(0.9702 0 0);
--secondary-foreground: oklch(0.2046 0 0);
--muted: oklch(0.9702 0 0);
--muted-foreground: oklch(0.5486 0 0);
--accent: oklch(0.9702 0 0);
--accent-foreground: oklch(0.2046 0 0);
--destructive: oklch(0.5830 0.2387 28.4765);
--destructive-foreground: oklch(0.9702 0 0);
--border: oklch(0.9219 0 0);
--input: oklch(0.9219 0 0);
--ring: oklch(0.7090 0 0);
--chart-1: oklch(0.5555 0 0);
--chart-2: oklch(0.5555 0 0);
--chart-3: oklch(0.5555 0 0);
--chart-4: oklch(0.5555 0 0);
--chart-5: oklch(0.5555 0 0);
--sidebar: oklch(0.9851 0 0);
--sidebar-foreground: oklch(0.1448 0 0);
--sidebar-primary: oklch(0.2046 0 0);
--sidebar-primary-foreground: oklch(0.9851 0 0);
--sidebar-accent: oklch(0.9702 0 0);
--sidebar-accent-foreground: oklch(0.2046 0 0);
--sidebar-border: oklch(0.9219 0 0);
--sidebar-ring: oklch(0.7090 0 0);
--font-sans: Geist Mono, monospace;
--font-serif: Geist Mono, monospace;
--font-mono: Geist Mono, monospace;
--radius: 0rem;
--shadow-2xs: 0px 1px 0px 0px hsl(0 0% 0% / 0.00);
--shadow-xs: 0px 1px 0px 0px hsl(0 0% 0% / 0.00);
--shadow-sm: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 1px 2px -1px hsl(0 0% 0% / 0.00);
--shadow: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 1px 2px -1px hsl(0 0% 0% / 0.00);
--shadow-md: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 2px 4px -1px hsl(0 0% 0% / 0.00);
--shadow-lg: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 4px 6px -1px hsl(0 0% 0% / 0.00);
--shadow-xl: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 8px 10px -1px hsl(0 0% 0% / 0.00);
--shadow-2xl: 0px 1px 0px 0px hsl(0 0% 0% / 0.00);
--tracking-normal: 0em;
--spacing: 0.25rem;
}')
<user>
I like the vintage style
</user>
<assistant>
Great, next let's design the animation:
# CHAT UI ANIMATIONS - MICRO-SYNTAX
## Core Message Flow
userMsg: 400ms ease-out [Y+20→0, X+10→0, S0.9→1]
aiMsg: 600ms bounce [Y+15→0, S0.95→1] +200ms
typing: 1400ms ∞ [Y±8, α0.4→1] stagger+200ms
status: 300ms ease-out [α0.6→1, S1→1.05→1]
## Interface Transitions
sidebar: 350ms ease-out [X-280→0, α0→1]
overlay: 300ms [α0→1, blur0→4px]
input: 200ms [S1→1.01, shadow+ring] focus
input: 150ms [S1.01→1, shadow-ring] blur
## Button Interactions
sendBtn: 150ms [S1→0.95→1, R±2°] press
sendBtn: 200ms [S1→1.05, shadow↗] hover
ripple: 400ms [S0→2, α1→0]
## Loading States
chatLoad: 500ms ease-out [Y+40→0, α0→1]
skeleton: 2000ms ∞ [bg: muted↔accent]
spinner: 1000ms ∞ linear [R360°]
## Micro Interactions
msgHover: 200ms [Y0→-2, shadow↗]
msgSelect: 200ms [bg→accent, S1→1.02]
error: 400ms [X±5] shake
success: 600ms bounce [S0→1.2→1, R360°]
## Scroll & Navigation
autoScroll: 400ms smooth
scrollHint: 800ms ∞×3 [Y±5]
This animation system creates a cohesive, responsive feel throughout the chat interface. Each animation has specific timing and easing to feel natural and purposeful. Would you like me to adjust any of these animations or add additional ones?
</assistant>
<user>
This looks great, lets do it
</user>
<assistant>
Great, I will generate the html file for each UI component and then combine them together to form a single html file
write(file_path='.superdesign/design_iterations/chat_ui.css', content='...')
write(file_path='.superdesign/design_iterations/chat_ui.html', content='...')
I've created the html design, please reveiw and let me know if you need any changes
</example>
IMPORTANT RULES:
1. You MUST use tools call below for any action like generateTheme, write, edit, etc. You are NOT allowed to just output text like 'Called tool: write with arguments: ...' or <tool-call>...</tool-call>; MUST USE TOOL CALL (This is very important!!)
2. You MUST confirm the layout, and then theme style, and then animation
3. You MUST use .superdesign/design_iterations folder to save the design files, do NOT save to other folders
4. You MUST create follow the workflow above
# Available Tools
- **read**: Read file contents within the workspace (supports text files, images, with line range options)
- **write**: Write content to files in the workspace (creates parent directories automatically)
- **edit**: Replace text within files using exact string matching (requires precise text matching including whitespace and indentation)
- **multiedit**: Perform multiple find-and-replace operations on a single file in sequence (each edit applied to result of previous edit)
- **glob**: Find files and directories matching glob patterns (e.g., "*.js", "src/**/*.ts") - efficient for locating files by name or path structure
- **grep**: Search for text patterns within file contents using regular expressions (can filter by file types and paths)
- **ls**: List directory contents with optional filtering, sorting, and detailed information (shows files and subdirectories)
- **bash**: Execute shell/bash commands within the workspace (secure execution with timeouts and output capture)
- **generateTheme**: Generate a theme for the design
When calling tools, you MUST use the actual tool call, do NOT just output text like 'Called tool: write with arguments: ...' or <tool-call>...</tool-call>, this won't actually call the tool. (This is very important to my life, please follow)

57
LICENSE Normal file
View File

@@ -0,0 +1,57 @@
Super Design License
This project is **mostly licensed under the GNU Affero General Public License v3 (AGPLv3)** as described below.
**However, certain files are licensed under a separate commercial license.**
These files are clearly marked at the top of the file with the following comment:
/* @license Enterprise */
Files with this comment are **not** licensed under the AGPLv3, but instead are subject to the commercial license terms further below.
---
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2025 AI Jason, JackJack.
All rights reserved.
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
---
How to Identify Commercially Licensed Files:
Any file that includes the comment `/* @license Enterprise */` at the top is licensed under the following terms instead of the AGPL:
---
The Super Design Enterprise Commercial License
Copyright (C) 2025 AI Jason, JackJack.
With regard to files marked `/* @license Enterprise */`, this portion of the software and associated documentation files (the "Software") may only be used in production if you (and any entity that you represent) have agreed to, and are in compliance with, a valid Super Design Enterprise Edition subscription agreement or other agreements governing the use of the Software, as mutually agreed by you and the Licensor(s).
Subject to the foregoing, you are free to modify this Software and publish patches to it. However, the Licensor(s) retain all rights, title, and interest in such modifications. All such modifications may only be used, copied, modified, displayed, distributed, or otherwise exploited with a valid Enterprise subscription. You are not granted any other rights beyond those expressly stated herein.
Development and testing usage of this Software is permitted without a subscription.
It is forbidden to copy, merge, publish, distribute, sublicense, and/or sell this Software without a valid commercial agreement.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---
Project URL:
https://superdesign.dev
Licensor Contact:
AI Jason, JackJack

92
README.md Normal file
View File

@@ -0,0 +1,92 @@
# 🧠 SuperDesign — AI Design Agent for Your IDE
![SuperDesign Cover](cover.png)
### **By:** [AI Jason](https://x.com/jasonzhou1993) & [JackJack](https://x.com/jackjack_eth)
SuperDesign is the first **open-source design agent** that lives right inside your IDE.
Generate UI mockups, components, and wireframes directly from natural language prompts.
Works seamlessly with Cursor, Windsurf, Claude Code, and plain VS Code.
> ✨ "Why design one option when you can explore ten?" — SuperDesign
[Join discord](https://discord.gg/FYr49d6cQ9)
[Upvote on Hackernews](https://news.ycombinator.com/item?id=44376003)
[Install guide](https://www.superdesign.dev/)
---
## 🎬 Demo Video (Click to play)
[![SuperDesign Demo](https://img.youtube.com/vi/INv6oZDhhUM/maxresdefault.jpg)](https://youtu.be/INv6oZDhhUM)
---
## 🚀 Features
- 🖼️ **Product Mock**: Instantly generate full UI screens from a single prompt
- 🧩 **UI Components**: Create reusable components you can drop into your code
- 📝 **Wireframes**: Explore low-fidelity layouts for fast iteration
- 🔁 **Fork & Iterate**: Duplicate and evolve designs easily
- 📥 **Prompt-to-IDE**: Copy prompts into your favorite AI IDE (Cursor, Windsurf, Claude Code)
---
## 🧠 Works Great With Cursor, Windsurf, Claude Code, VS Code
👉 [Install here](https://www.superdesign.dev/)
---
## 🛠️ Getting Started
1. **Install the Extension** from the Cursor/VS Code Marketplace
2. Open the `SuperDesign` sidebar panel
3. Type a prompt (e.g., _"Design a modern login screen"_)
4. View generated mockups, components, and wireframes
5. Fork, tweak, and paste into your project
---
## Can I use my own Claude Code or Cursor subscription?
Yes, after you initialise superdesign extension, some cursor/claude code rules will be added, so you can prompt the agent to do design and preview in superdesign canva (cmd + shift + p -> superdesign: open canva)
If using Cursor - I will highly suggest copy the prompt in 'design.mdc' and create a custom mode in cursor with that same system prompt; This should give you much better performance
Instructions here (Click to play):
[![Instruction video](v0.0.11.png)](https://youtu.be/KChmJMCDOB0?si=pvU0kNRO4GRWjsec&t=122)
## 📂 Where Are My Designs Stored?
Your generated designs are saved locally inside `.superdesign/`.
---
## ❓ FAQ
**Is it free and open source?**
Yes! We are open source — fork it, extend it, remix it.
**Can I customize the design agent?**
Yes — use your own prompt templates, modify behaviors, or add commands.
**Can SuperDesign update existing UI?**
Absolutely — select a component, describe the change, and let the agent do the rest.
<img width="886" height="586" alt="image" src="https://github.com/user-attachments/assets/71b7cfcc-6123-40ea-aae5-05ea6cdcea96" />
**How can I contribute?**
Pull requests are welcome. Star the repo and join us on [Discord](https://discord.gg/XYZ)!
---
## 🔗 Links
- 🌐 Website: [https://superdesign.dev](https://superdesign.dev)
- 📦 GitHub: [https://github.com/superdesigndev/superdesign](https://github.com/superdesigndev/superdesign)
- 💬 Discord: [Join the Community](https://discord.gg/XYZ)
- 🐦 Twitter / X: [@SuperDesignDev](https://x.com/SuperDesignDev)

BIN
cover.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 431 KiB

125
esbuild.js Normal file
View File

@@ -0,0 +1,125 @@
const esbuild = require("esbuild");
const production = process.argv.includes('--production');
const watch = process.argv.includes('--watch');
/**
* @type {import('esbuild').Plugin}
*/
const esbuildProblemMatcherPlugin = {
name: 'esbuild-problem-matcher',
setup(build) {
build.onStart(() => {
console.log('[watch] build started');
});
build.onEnd((result) => {
result.errors.forEach(({ text, location }) => {
console.error(`✘ [ERROR] ${text}`);
console.error(` ${location.file}:${location.line}:${location.column}:`);
});
console.log('[watch] build finished');
});
},
};
async function main() {
const ctx = await esbuild.context({
entryPoints: [
'src/extension.ts'
],
bundle: true,
format: 'cjs',
minify: production,
sourcemap: !production,
sourcesContent: false,
platform: 'node',
outfile: 'dist/extension.js',
external: ['vscode'],
logLevel: 'silent',
plugins: [
/* add to the end of plugins array */
esbuildProblemMatcherPlugin,
]
});
// Webview build context
const webviewCtx = await esbuild.context({
entryPoints: ['src/webview/index.tsx'],
bundle: true,
format: 'esm',
minify: production,
sourcemap: !production,
sourcesContent: false,
platform: 'browser',
outfile: 'dist/webview.js',
logLevel: 'silent',
plugins: [esbuildProblemMatcherPlugin],
loader: {
'.css': 'text',
'.png': 'file',
'.jpg': 'file',
'.svg': 'file',
},
define: {
'process.env.NODE_ENV': production ? '"production"' : '"development"',
},
jsx: 'automatic', // This enables JSX support
});
if (watch) {
await Promise.all([
ctx.watch(),
webviewCtx.watch()
]);
console.log('Watching for changes...');
} else {
await Promise.all([
ctx.rebuild(),
webviewCtx.rebuild()
]);
await ctx.dispose();
await webviewCtx.dispose();
// // Copy Claude Code SDK to dist for runtime access
const fs = require('fs');
const path = require('path');
// const srcPath = path.join(__dirname, 'node_modules', '@anthropic-ai', 'claude-code');
// const destPath = path.join(__dirname, 'dist', 'node_modules', '@anthropic-ai', 'claude-code');
// // Create directory structure
// fs.mkdirSync(path.dirname(destPath), { recursive: true });
// // Copy files
function copyDir(src, dest) {
fs.mkdirSync(dest, { recursive: true });
const entries = fs.readdirSync(src, { withFileTypes: true });
for (let entry of entries) {
const srcPath = path.join(src, entry.name);
const destPath = path.join(dest, entry.name);
entry.isDirectory() ? copyDir(srcPath, destPath) : fs.copyFileSync(srcPath, destPath);
}
}
// copyDir(srcPath, destPath);
// console.log('Claude Code SDK copied to dist/');
// Copy assets to dist folder
const assetsSrcPath = path.join(__dirname, 'src', 'assets');
const assetsDestPath = path.join(__dirname, 'dist', 'src', 'assets');
if (fs.existsSync(assetsSrcPath)) {
copyDir(assetsSrcPath, assetsDestPath);
console.log('Assets copied to dist/src/assets/');
} else {
console.log('Assets directory not found at:', assetsSrcPath);
}
console.log('Build complete!');
}
}
main().catch(e => {
console.error(e);
process.exit(1);
});

125
esbuild.js.backup Normal file
View File

@@ -0,0 +1,125 @@
const esbuild = require("esbuild");
const production = process.argv.includes('--production');
const watch = process.argv.includes('--watch');
/**
* @type {import('esbuild').Plugin}
*/
const esbuildProblemMatcherPlugin = {
name: 'esbuild-problem-matcher',
setup(build) {
build.onStart(() => {
console.log('[watch] build started');
});
build.onEnd((result) => {
result.errors.forEach(({ text, location }) => {
console.error(`✘ [ERROR] ${text}`);
console.error(` ${location.file}:${location.line}:${location.column}:`);
});
console.log('[watch] build finished');
});
},
};
async function main() {
const ctx = await esbuild.context({
entryPoints: [
'src/extension.ts'
],
bundle: true,
format: 'cjs',
minify: production,
sourcemap: !production,
sourcesContent: false,
platform: 'node',
outfile: 'dist/extension.js',
external: ['vscode'],
logLevel: 'silent',
plugins: [
/* add to the end of plugins array */
esbuildProblemMatcherPlugin,
]
});
// Webview build context
const webviewCtx = await esbuild.context({
entryPoints: ['src/webview/index.tsx'],
bundle: true,
format: 'esm',
minify: production,
sourcemap: !production,
sourcesContent: false,
platform: 'browser',
outfile: 'dist/webview.js',
logLevel: 'silent',
plugins: [esbuildProblemMatcherPlugin],
loader: {
'.css': 'text',
'.png': 'file',
'.jpg': 'file',
'.svg': 'file',
},
define: {
'process.env.NODE_ENV': production ? '"production"' : '"development"',
},
jsx: 'automatic', // This enables JSX support
});
if (watch) {
await Promise.all([
ctx.watch(),
webviewCtx.watch()
]);
console.log('Watching for changes...');
} else {
await Promise.all([
ctx.rebuild(),
webviewCtx.rebuild()
]);
await ctx.dispose();
await webviewCtx.dispose();
// Copy Claude Code SDK to dist for runtime access
const fs = require('fs');
const path = require('path');
const srcPath = path.join(__dirname, 'node_modules', '@anthropic-ai', 'claude-code');
const destPath = path.join(__dirname, 'dist', 'node_modules', '@anthropic-ai', 'claude-code');
// Create directory structure
fs.mkdirSync(path.dirname(destPath), { recursive: true });
// Copy files
function copyDir(src, dest) {
fs.mkdirSync(dest, { recursive: true });
const entries = fs.readdirSync(src, { withFileTypes: true });
for (let entry of entries) {
const srcPath = path.join(src, entry.name);
const destPath = path.join(dest, entry.name);
entry.isDirectory() ? copyDir(srcPath, destPath) : fs.copyFileSync(srcPath, destPath);
}
}
copyDir(srcPath, destPath);
console.log('Claude Code SDK copied to dist/');
// Copy assets to dist folder
const assetsSrcPath = path.join(__dirname, 'src', 'assets');
const assetsDestPath = path.join(__dirname, 'dist', 'src', 'assets');
if (fs.existsSync(assetsSrcPath)) {
copyDir(assetsSrcPath, assetsDestPath);
console.log('Assets copied to dist/src/assets/');
} else {
console.log('Assets directory not found at:', assetsSrcPath);
}
console.log('Build complete!');
}
}
main().catch(e => {
console.error(e);
process.exit(1);
});

28
eslint.config.mjs Normal file
View File

@@ -0,0 +1,28 @@
import typescriptEslint from "@typescript-eslint/eslint-plugin";
import tsParser from "@typescript-eslint/parser";
export default [{
files: ["**/*.ts"],
}, {
plugins: {
"@typescript-eslint": typescriptEslint,
},
languageOptions: {
parser: tsParser,
ecmaVersion: 2022,
sourceType: "module",
},
rules: {
"@typescript-eslint/naming-convention": ["warn", {
selector: "import",
format: ["camelCase", "PascalCase"],
}],
curly: "warn",
eqeqeq: "warn",
"no-throw-literal": "warn",
semi: "warn",
},
}];

BIN
icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

1
icon.svg Normal file
View File

@@ -0,0 +1 @@
<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg" class="drop-shadow-lg"><defs><linearGradient id="yellowOrange" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#FFE066"></stop><stop offset="50%" stop-color="#FFB347"></stop><stop offset="100%" stop-color="#FF8C42"></stop></linearGradient><linearGradient id="pinkCoral" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#FFB3E6"></stop><stop offset="50%" stop-color="#FF6B9D"></stop><stop offset="100%" stop-color="#E91E63"></stop></linearGradient><linearGradient id="bluePurple" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#87CEEB"></stop><stop offset="50%" stop-color="#9C27B0"></stop><stop offset="100%" stop-color="#1976D2"></stop></linearGradient></defs><rect x="45" y="13" width="70" height="52" rx="13" ry="13" fill="url(#yellowOrange)"></rect><rect x="26" y="32" width="70" height="52" rx="13" ry="13" fill="url(#pinkCoral)"></rect><rect x="6" y="51" width="70" height="52" rx="13" ry="13" fill="url(#bluePurple)"></rect></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

8387
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

253
package.json Normal file
View File

@@ -0,0 +1,253 @@
{
"name": "superdesign",
"displayName": "superdesign",
"description": "The 1st Design Agent lives inside your IDE",
"version": "0.0.11",
"publisher": "iganbold",
"icon": "icon.png",
"repository": {
"type": "git",
"url": "https://github.com/superdesigndev/superdesign"
},
"engines": {
"vscode": "^1.90.0"
},
"categories": [
"AI",
"Other"
],
"keywords": [
"design",
"ui",
"mockup",
"wireframe",
"component",
"ai",
"agent",
"figma",
"prototype",
"layout",
"frontend",
"react",
"html",
"css",
"claude",
"anthropic",
"generator",
"automation"
],
"activationEvents": [
"onCommand:superdesign.helloWorld",
"onView:superdesign.chatView"
],
"main": "./dist/extension.js",
"contributes": {
"commands": [
{
"command": "superdesign.helloWorld",
"title": "Hello World",
"category": "Superdesign"
},
{
"command": "superdesign.configureApiKey",
"title": "Configure Anthropic API Key",
"category": "Superdesign"
},
{
"command": "superdesign.configureOpenAIApiKey",
"title": "Configure OpenAI API Key",
"category": "Superdesign"
},
{
"command": "superdesign.configureOpenRouterApiKey",
"title": "Configure OpenRouter API Key",
"category": "Superdesign"
},
{
"command": "superdesign.showChatSidebar",
"title": "Show Chat Sidebar",
"category": "Superdesign"
},
{
"command": "superdesign.openCanvas",
"title": "Open Canvas View",
"category": "Superdesign",
"icon": "$(window)"
},
{
"command": "superdesign.clearChat",
"title": "Clear Chat",
"category": "Superdesign",
"icon": "$(clear-all)"
},
{
"command": "superdesign.resetWelcome",
"title": "Reset Welcome Screen",
"category": "Superdesign"
},
{
"command": "superdesign.initializeProject",
"title": "Initialize Superdesign",
"category": "Superdesign"
},
{
"command": "superdesign.openSettings",
"title": "Open Settings",
"category": "Superdesign",
"icon": "$(settings-gear)"
}
],
"menus": {
"view/title": [
{
"command": "superdesign.openCanvas",
"when": "view == superdesign.chatView",
"group": "navigation"
},
{
"command": "superdesign.openSettings",
"when": "view == superdesign.chatView",
"group": "navigation"
},
{
"command": "superdesign.clearChat",
"when": "view == superdesign.chatView",
"group": "navigation"
}
]
},
"views": {
"superdesign-sidebar": [
{
"type": "webview",
"id": "superdesign.chatView",
"name": "SUPER DESIGN",
"when": "true"
}
]
},
"viewsContainers": {
"activitybar": [
{
"id": "superdesign-sidebar",
"title": "Superdesign",
"icon": "icon.svg"
}
]
},
"configuration": {
"title": "Superdesign",
"properties": {
"superdesign.anthropicApiKey": {
"type": "string",
"description": "Anthropic API key for Claude Code integration",
"scope": "application"
},
"superdesign.openaiApiKey": {
"type": "string",
"description": "OpenAI API key for custom agent",
"scope": "application"
},
"superdesign.openrouterApiKey": {
"type": "string",
"description": "OpenRouter API key for custom agent",
"scope": "application"
},
"superdesign.aiModelProvider": {
"type": "string",
"enum": [
"openai",
"anthropic",
"openrouter"
],
"default": "anthropic",
"description": "AI model provider for custom agent (OpenAI, Anthropic, or OpenRouter)",
"scope": "application"
},
"superdesign.aiModel": {
"type": "string",
"description": "Specific AI model to use (e.g., gpt-4o, claude-3-5-sonnet-20241022)",
"scope": "application"
},
"superdesign.aiModelAuto": {
"type": "string",
"description": "Specific AI model to use when 'AUTO' is selected (e.g., gpt-4.1)",
"scope": "application"
},
"superdesign.openaiBaseURL": {
"type": "string",
"description": "Custom base URL for OpenAI API.",
"scope": "application"
},
"superdesign.anthropicBaseURL": {
"type": "string",
"description": "Custom base URL for Anthropic API.",
"scope": "application"
},
"superdesign.openrouterBaseURL": {
"type": "string",
"description": "Custom base URL for OpenRouter API.",
"scope": "application"
}
}
}
},
"scripts": {
"vscode:prepublish": "npm run package",
"compile": "npm run check-types && npm run lint && node esbuild.js",
"watch": "npm-run-all -p watch:*",
"watch:esbuild": "node esbuild.js --watch",
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
"package": "npm run check-types && npm run lint && node esbuild.js --production",
"compile-tests": "tsc -p . --outDir out",
"watch-tests": "tsc -p . -w --outDir out",
"pretest": "npm run compile-tests && npm run compile && npm run lint",
"check-types": "tsc --noEmit",
"lint": "eslint src",
"test": "vscode-test",
"test:llm": "tsc --project tsconfig.test.json && node dist-test/test/llm-service.test.js",
"test:core": "tsc --project tsconfig.test.json && node dist-test/test/core-components.test.js",
"test:read": "tsc --project tsconfig.test.json && node dist-test/test/read-tool.test.js",
"test:write-edit": "tsc --project tsconfig.test.json && node dist-test/test/write-edit-tools.test.js",
"test:ls-grep-glob": "tsc --project tsconfig.test.json && node dist-test/test/ls-grep-glob-tools.test.js",
"test:agent": "tsc --project tsconfig.test.json && node dist-test/test/llm-service.test.js && node dist-test/test/core-components.test.js",
"test:tools": "tsc --project tsconfig.test.json && node dist-test/test/read-tool.test.js && node dist-test/test/write-edit-tools.test.js && node dist-test/test/ls-grep-glob-tools.test.js"
},
"devDependencies": {
"@types/mime-types": "^3.0.1",
"@types/mocha": "^10.0.10",
"@types/node": "^20.19.9",
"@types/vscode": "^1.90.0",
"@typescript-eslint/eslint-plugin": "^8.31.1",
"@typescript-eslint/parser": "^8.31.1",
"@vscode/test-cli": "^0.0.10",
"@vscode/test-electron": "^2.5.2",
"esbuild": "^0.25.3",
"eslint": "^9.25.1",
"npm-run-all": "^4.1.5",
"ts-node": "^10.9.2",
"typescript": "^5.8.3"
},
"dependencies": {
"@ai-sdk/anthropic": "^1.2.12",
"@ai-sdk/google": "^1.2.19",
"@ai-sdk/openai": "^1.3.22",
"@openrouter/ai-sdk-provider": "^0.7.2",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"ai": "^4.3.16",
"execa": "^9.6.0",
"glob": "^11.0.3",
"highlight.js": "^11.11.1",
"lucide-react": "^0.522.0",
"micromatch": "^4.0.8",
"mime-types": "^3.0.1",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-markdown": "^10.1.0",
"react-zoom-pan-pinch": "^3.7.0",
"rehype-highlight": "^7.0.2",
"remark-gfm": "^4.0.1",
"zod": "^3.25.67"
}
}

176
package.json.backup Normal file
View File

@@ -0,0 +1,176 @@
{
"name": "superdesign",
"displayName": "superdesign",
"description": "The 1st Design Agent lives inside your IDE",
"version": "0.0.6",
"publisher": "iganbold",
"icon": "icon.png",
"repository": {
"type": "git",
"url": "https://github.com/superdesigndev/superdesign"
},
"engines": {
"vscode": "^1.90.0"
},
"categories": [
"AI",
"Other"
],
"keywords": [
"design",
"ui",
"mockup",
"wireframe",
"component",
"ai",
"agent",
"figma",
"prototype",
"layout",
"frontend",
"react",
"html",
"css",
"claude",
"anthropic",
"generator",
"automation"
],
"activationEvents": [
"onCommand:superdesign.helloWorld",
"onView:superdesign.chatView"
],
"main": "./dist/extension.js",
"contributes": {
"commands": [
{
"command": "superdesign.helloWorld",
"title": "Hello World",
"category": "Superdesign"
},
{
"command": "superdesign.configureApiKey",
"title": "Configure Anthropic API Key",
"category": "Superdesign"
},
{
"command": "superdesign.showChatSidebar",
"title": "Show Chat Sidebar",
"category": "Superdesign"
},
{
"command": "superdesign.openCanvas",
"title": "Open Canvas View",
"category": "Superdesign",
"icon": "$(window)"
},
{
"command": "superdesign.clearChat",
"title": "Clear Chat",
"category": "Superdesign",
"icon": "$(clear-all)"
},
{
"command": "superdesign.resetWelcome",
"title": "Reset Welcome Screen",
"category": "Superdesign"
},
{
"command": "superdesign.initializeProject",
"title": "Initialize Superdesign",
"category": "Superdesign"
}
],
"menus": {
"view/title": [
{
"command": "superdesign.openCanvas",
"when": "view == superdesign.chatView",
"group": "navigation"
},
{
"command": "superdesign.clearChat",
"when": "view == superdesign.chatView",
"group": "navigation"
}
]
},
"views": {
"superdesign-sidebar": [
{
"type": "webview",
"id": "superdesign.chatView",
"name": "SUPER DESIGN",
"when": "true"
}
]
},
"viewsContainers": {
"activitybar": [
{
"id": "superdesign-sidebar",
"title": "Superdesign",
"icon": "icon.svg"
}
]
},
"configuration": {
"title": "Superdesign",
"properties": {
"superdesign.anthropicApiKey": {
"type": "string",
"description": "Anthropic API key for Claude Code integration",
"scope": "application"
}
}
}
},
"scripts": {
"vscode:prepublish": "npm run package",
"compile": "npm run check-types && npm run lint && node esbuild.js",
"watch": "npm-run-all -p watch:*",
"watch:esbuild": "node esbuild.js --watch",
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
"package": "npm run check-types && npm run lint && node esbuild.js --production",
"compile-tests": "tsc -p . --outDir out",
"watch-tests": "tsc -p . -w --outDir out",
"pretest": "npm run compile-tests && npm run compile && npm run lint",
"check-types": "tsc --noEmit",
"lint": "eslint src",
"test": "vscode-test"
},
"devDependencies": {
"@types/mocha": "^10.0.10",
"@types/node": "20.x",
"@types/vscode": "^1.90.0",
"@typescript-eslint/eslint-plugin": "^8.31.1",
"@typescript-eslint/parser": "^8.31.1",
"@vscode/test-cli": "^0.0.10",
"@vscode/test-electron": "^2.5.2",
"esbuild": "^0.25.3",
"eslint": "^9.25.1",
"npm-run-all": "^4.1.5",
"typescript": "^5.8.3"
},
"dependencies": {
"@ai-sdk/anthropic": "^0.0.50",
"@ai-sdk/google": "^0.0.52",
"@ai-sdk/openai": "^0.0.66",
"@anthropic-ai/claude-code": "^1.0.35",
"@openrouter/ai-sdk-provider": "^0.0.6",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"ai": "^3.4.0",
"execa": "^8.0.0",
"glob": "^10.3.0",
"highlight.js": "^11.11.1",
"lucide-react": "^0.522.0",
"micromatch": "^4.0.5",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-markdown": "^10.1.0",
"react-zoom-pan-pinch": "^3.7.0",
"rehype-highlight": "^7.0.2",
"remark-gfm": "^4.0.1"
}
}

4554
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

BIN
src/assets/bolt_logo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
src/assets/cursor_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
src/assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

1
src/assets/icon.svg Normal file
View File

@@ -0,0 +1 @@
<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg" class="drop-shadow-lg"><defs><linearGradient id="yellowOrange" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#FFE066"></stop><stop offset="50%" stop-color="#FFB347"></stop><stop offset="100%" stop-color="#FF8C42"></stop></linearGradient><linearGradient id="pinkCoral" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#FFB3E6"></stop><stop offset="50%" stop-color="#FF6B9D"></stop><stop offset="100%" stop-color="#E91E63"></stop></linearGradient><linearGradient id="bluePurple" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#87CEEB"></stop><stop offset="50%" stop-color="#9C27B0"></stop><stop offset="100%" stop-color="#1976D2"></stop></linearGradient></defs><rect x="45" y="13" width="70" height="52" rx="13" ry="13" fill="url(#yellowOrange)"></rect><rect x="26" y="32" width="70" height="52" rx="13" ry="13" fill="url(#pinkCoral)"></rect><rect x="6" y="51" width="70" height="52" rx="13" ry="13" fill="url(#bluePurple)"></rect></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
src/assets/lovable_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

1888
src/extension.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,345 @@
import * as vscode from 'vscode';
import { ClaudeCodeService } from '../services/claudeCodeService';
import { ChatMessageService } from '../services/chatMessageService';
import { generateWebviewHtml } from '../templates/webviewTemplate';
import { WebviewContext } from '../types/context';
import { AgentService } from '../types/agent';
export class ChatSidebarProvider implements vscode.WebviewViewProvider {
public static readonly VIEW_TYPE = 'superdesign.chatView';
private _view?: vscode.WebviewView;
private messageHandler: ChatMessageService;
private customMessageHandler?: (message: any) => void;
constructor(
private readonly _extensionUri: vscode.Uri,
private readonly agentService: AgentService,
private readonly outputChannel: vscode.OutputChannel
) {
this.messageHandler = new ChatMessageService(agentService, outputChannel);
}
public setMessageHandler(handler: (message: any) => void) {
this.customMessageHandler = handler;
}
public sendMessage(message: any) {
if (this._view) {
this._view.webview.postMessage(message);
}
}
public resolveWebviewView(
webviewView: vscode.WebviewView,
_context: vscode.WebviewViewResolveContext,
_token: vscode.CancellationToken,
) {
this._view = webviewView;
webviewView.webview.options = {
enableScripts: true,
localResourceRoots: [
vscode.Uri.joinPath(this._extensionUri, 'dist'),
vscode.Uri.joinPath(this._extensionUri, 'src', 'assets')
]
};
const webviewContext: WebviewContext = {
layout: 'sidebar',
extensionUri: this._extensionUri.toString()
};
webviewView.webview.html = generateWebviewHtml(
webviewView.webview,
this._extensionUri,
webviewContext
);
// Handle messages from the webview
webviewView.webview.onDidReceiveMessage(
async (message) => {
// First try custom message handler for auto-canvas functionality
if (this.customMessageHandler) {
this.customMessageHandler(message);
}
// Then handle regular chat messages
switch (message.command) {
case 'chatMessage':
await this.messageHandler.handleChatMessage(message, webviewView.webview);
break;
case 'stopChat':
await this.messageHandler.stopCurrentChat(webviewView.webview);
break;
case 'executeAction':
// Execute command from error action buttons
console.log('Executing action:', message.actionCommand, message.actionArgs);
if (message.actionArgs) {
await vscode.commands.executeCommand(message.actionCommand, message.actionArgs);
} else {
await vscode.commands.executeCommand(message.actionCommand);
}
break;
case 'getBase64Image':
// Forward to extension for image conversion
// This will be handled by extension.ts
break;
case 'getCurrentProvider':
await this.handleGetCurrentProvider(webviewView.webview);
break;
case 'changeProvider':
await this.handleChangeProvider(message.model, webviewView.webview);
break;
}
}
);
}
private async handleGetCurrentProvider(webview: vscode.Webview) {
const config = vscode.workspace.getConfiguration('superdesign');
const currentProvider = config.get<string>('aiModelProvider', 'anthropic');
const currentModel = config.get<string>('aiModel');
// If no specific model is set, use defaults
let defaultModel: string;
switch (currentProvider) {
case 'openai':
defaultModel = 'gpt-4o';
break;
case 'openrouter':
defaultModel = 'anthropic/claude-3-7-sonnet-20250219';
break;
case 'anthropic':
default:
defaultModel = 'claude-3-5-sonnet-20241022';
break;
}
webview.postMessage({
command: 'currentProviderResponse',
provider: currentProvider,
model: currentModel || defaultModel
});
}
private async handleChangeProvider(model: string, webview: vscode.Webview) {
try {
const config = vscode.workspace.getConfiguration('superdesign');
// Determine provider and API key based on model
let provider: string;
let apiKeyKey: string;
let configureCommand: string;
let displayName: string;
if (model === 'openai-auto') {
provider = 'openai';
apiKeyKey = 'openaiApiKey';
configureCommand = 'superdesign.configureOpenAIApiKey';
displayName = 'OpenAI (AUTO)';
} else if (model.includes('/')) {
// OpenRouter model (contains slash like "openai/gpt-4o")
provider = 'openrouter';
apiKeyKey = 'openrouterApiKey';
configureCommand = 'superdesign.configureOpenRouterApiKey';
displayName = `OpenRouter (${this.getModelDisplayName(model)})`;
} else if (model.startsWith('claude-')) {
provider = 'anthropic';
apiKeyKey = 'anthropicApiKey';
configureCommand = 'superdesign.configureApiKey';
displayName = `Anthropic (${this.getModelDisplayName(model)})`;
} else {
provider = 'openai';
apiKeyKey = 'openaiApiKey';
configureCommand = 'superdesign.configureOpenAIApiKey';
displayName = `OpenAI (${this.getModelDisplayName(model)})`;
}
// Update both provider and specific model
await config.update('aiModelProvider', provider, vscode.ConfigurationTarget.Global);
await config.update('aiModel', model, vscode.ConfigurationTarget.Global);
// Check if the API key is configured for the selected provider
const apiKey = config.get<string>(apiKeyKey);
if (!apiKey) {
const result = await vscode.window.showWarningMessage(
`${displayName} selected, but API key is not configured. Would you like to configure it now?`,
'Configure API Key',
'Later'
);
if (result === 'Configure API Key') {
await vscode.commands.executeCommand(configureCommand);
}
}
// Notify webview of successful change
webview.postMessage({
command: 'providerChanged',
provider: provider,
model: model
});
} catch (error) {
vscode.window.showErrorMessage(`Failed to update AI model: ${error}`);
}
}
private getModelDisplayName(model: string): string {
const modelNames: { [key: string]: string } = {
// OpenAI models
'gpt-4.1': 'GPT-4.1',
'gpt-4.1-mini': 'GPT-4.1 Mini',
'gpt-4.1-nano': 'GPT-4.1 Nano',
'gpt-4o': 'GPT-4o',
'gpt-4o-mini': 'GPT-4o Mini',
// Anthropic models
'claude-4-opus-20250514': 'Claude 4 Opus',
'claude-4-sonnet-20250514': 'Claude 4 Sonnet',
'claude-3-7-sonnet-20250219': 'Claude 3.7 Sonnet',
'claude-3-5-sonnet-20241022': 'Claude 3.5 Sonnet',
'claude-3-opus-20240229': 'Claude 3 Opus',
'claude-3-sonnet-20240229': 'Claude 3 Sonnet',
'claude-3-haiku-20240307': 'Claude 3 Haiku',
// OpenRouter - Google models
'google/gemini-2.5-pro': 'Gemini 2.5 Pro',
'google/gemini-2.5-flash': 'Gemini 2.5 Flash',
'google/gemini-2.5-pro-preview-06-05': 'Gemini 2.5 Pro Preview',
'google/gemini-2.5-flash-preview-05-20': 'Gemini 2.5 Flash Preview',
'google/gemini-2.5-pro-preview-03-25': 'Gemini 2.5 Pro Preview (Mar)',
'google/gemini-2.0-flash-001': 'Gemini 2.0 Flash',
'google/gemini-2.0-flash-exp': 'Gemini 2.0 Flash Exp',
'google/gemini-2.0-flash-lite-001': 'Gemini 2.0 Flash Lite',
'google/gemma-3-27b-it': 'Gemma 3 27B',
'google/gemma-3-12b-it': 'Gemma 3 12B',
'google/gemma-3-4b-it': 'Gemma 3 4B',
'google/gemma-2-27b-it': 'Gemma 2 27B',
'google/gemma-2-9b-it': 'Gemma 2 9B',
'google/gemini-flash-1.5': 'Gemini Flash 1.5',
'google/gemini-flash-1.5-8b': 'Gemini Flash 1.5 8B',
'google/gemini-pro-1.5': 'Gemini Pro 1.5',
// OpenRouter - Meta models
'meta-llama/llama-4-maverick-17b-128e-instruct': 'Llama 4 Maverick 17B',
'meta-llama/llama-4-scout-17b-16e-instruct': 'Llama 4 Scout 17B',
'meta-llama/llama-3.3-70b-instruct': 'Llama 3.3 70B',
'meta-llama/llama-3.2-90b-vision-instruct': 'Llama 3.2 90B Vision',
'meta-llama/llama-3.2-11b-vision-instruct': 'Llama 3.2 11B Vision',
'meta-llama/llama-3.2-3b-instruct': 'Llama 3.2 3B',
'meta-llama/llama-3.2-1b-instruct': 'Llama 3.2 1B',
'meta-llama/llama-3.1-405b-instruct': 'Llama 3.1 405B',
'meta-llama/llama-3.1-70b-instruct': 'Llama 3.1 70B',
'meta-llama/llama-3.1-8b-instruct': 'Llama 3.1 8B',
'meta-llama/llama-3-70b-instruct': 'Llama 3 70B',
'meta-llama/llama-3-8b-instruct': 'Llama 3 8B',
'meta-llama/llama-guard-4-12b': 'Llama Guard 4 12B',
'meta-llama/llama-guard-3-8b': 'Llama Guard 3 8B',
'meta-llama/llama-guard-2-8b': 'Llama Guard 2 8B',
// OpenRouter - DeepSeek models
'deepseek/deepseek-r1': 'DeepSeek R1',
'deepseek/deepseek-r1-0528': 'DeepSeek R1 0528',
'deepseek/deepseek-r1-distill-llama-70b': 'DeepSeek R1 Distill Llama 70B',
'deepseek/deepseek-r1-distill-llama-8b': 'DeepSeek R1 Distill Llama 8B',
'deepseek/deepseek-r1-distill-qwen-32b': 'DeepSeek R1 Distill Qwen 32B',
'deepseek/deepseek-r1-distill-qwen-14b': 'DeepSeek R1 Distill Qwen 14B',
'deepseek/deepseek-r1-distill-qwen-7b': 'DeepSeek R1 Distill Qwen 7B',
'deepseek/deepseek-r1-distill-qwen-1.5b': 'DeepSeek R1 Distill Qwen 1.5B',
'deepseek/deepseek-chat-v3': 'DeepSeek Chat V3',
'deepseek/deepseek-v3-base': 'DeepSeek V3 Base',
'deepseek/deepseek-prover-v2': 'DeepSeek Prover V2',
// OpenRouter - Mistral models
'mistralai/mistral-small-3.2-24b-instruct-2506': 'Mistral Small 3.2 24B',
'mistralai/magistral-small-2506': 'Magistral Small',
'mistralai/magistral-medium-2506': 'Magistral Medium',
'mistralai/devstral-small-2505': 'Devstral Small',
'mistralai/mistral-medium-3': 'Mistral Medium 3',
'mistralai/mistral-small-3.1-24b-instruct-2503': 'Mistral Small 3.1 24B',
'mistralai/mistral-saba-2502': 'Mistral Saba',
'mistralai/mistral-small-24b-instruct-2501': 'Mistral Small 24B',
'mistralai/codestral-2501': 'Codestral',
'mistralai/mistral-large-2411': 'Mistral Large 2411',
'mistralai/mistral-large-2407': 'Mistral Large 2407',
'mistralai/pixtral-large-2411': 'Pixtral Large',
'mistralai/pixtral-12b': 'Pixtral 12B',
'mistralai/ministral-8b': 'Ministral 8B',
'mistralai/ministral-3b': 'Ministral 3B',
'mistralai/mistral-nemo': 'Mistral Nemo',
'mistralai/mistral-large': 'Mistral Large',
'mistralai/mixtral-8x22b-instruct': 'Mixtral 8x22B',
'mistralai/mixtral-8x7b-instruct': 'Mixtral 8x7B',
'mistralai/mistral-7b-instruct': 'Mistral 7B',
// OpenRouter - xAI models
'x-ai/grok-3': 'Grok 3',
'x-ai/grok-3-mini': 'Grok 3 Mini',
'x-ai/grok-3-beta': 'Grok 3 Beta',
'x-ai/grok-3-mini-beta': 'Grok 3 Mini Beta',
'x-ai/grok-2-vision-1212': 'Grok 2 Vision',
'x-ai/grok-2-1212': 'Grok 2',
'x-ai/grok-vision-beta': 'Grok Vision Beta',
// OpenRouter - Qwen models
'qwen/qwen3-235b-a22b-04-28': 'Qwen3 235B',
'qwen/qwen3-32b-04-28': 'Qwen3 32B',
'qwen/qwen3-30b-a3b-04-28': 'Qwen3 30B',
'qwen/qwen3-14b-04-28': 'Qwen3 14B',
'qwen/qwen3-8b-04-28': 'Qwen3 8B',
'qwen/qwen2.5-vl-72b-instruct': 'Qwen2.5 VL 72B',
'qwen/qwen2.5-vl-32b-instruct': 'Qwen2.5 VL 32B',
'qwen/qwen-2.5-coder-32b-instruct': 'Qwen 2.5 Coder 32B',
'qwen/qwen-2.5-72b-instruct': 'Qwen 2.5 72B',
'qwen/qwen-2.5-7b-instruct': 'Qwen 2.5 7B',
'qwen/qwen-2-72b-instruct': 'Qwen 2 72B',
'qwen/qwen-2-vl-7b-instruct': 'Qwen 2 VL 7B',
'qwen/qwq-32b': 'QwQ 32B',
'qwen/qwq-32b-preview': 'QwQ 32B Preview',
'qwen/qwen-vl-max-2025-01-25': 'Qwen VL Max',
'qwen/qwen-vl-plus': 'Qwen VL Plus',
'qwen/qwen-max-2025-01-25': 'Qwen Max',
'qwen/qwen-plus-2025-01-25': 'Qwen Plus',
'qwen/qwen-turbo-2024-11-01': 'Qwen Turbo',
// OpenRouter - Perplexity models
'perplexity/sonar-reasoning-pro': 'Sonar Reasoning Pro',
'perplexity/sonar-pro': 'Sonar Pro',
'perplexity/sonar-deep-research': 'Sonar Deep Research',
'perplexity/sonar-reasoning': 'Sonar Reasoning',
'perplexity/sonar': 'Sonar',
'perplexity/r1-1776': 'R1-1776',
'perplexity/llama-3.1-sonar-large-128k-online': 'Llama 3.1 Sonar Large Online',
'perplexity/llama-3.1-sonar-small-128k-online': 'Llama 3.1 Sonar Small Online',
// OpenRouter - Microsoft models
'microsoft/phi-4-reasoning-plus-04-30': 'Phi-4 Reasoning Plus',
'microsoft/mai-ds-r1': 'MAI-DS-R1',
'microsoft/phi-4-multimodal-instruct': 'Phi-4 Multimodal',
'microsoft/phi-4': 'Phi-4',
'microsoft/phi-3.5-mini-128k-instruct': 'Phi-3.5 Mini',
'microsoft/phi-3-medium-128k-instruct': 'Phi-3 Medium',
'microsoft/phi-3-mini-128k-instruct': 'Phi-3 Mini',
'microsoft/wizardlm-2-8x22b': 'WizardLM-2 8x22B',
// OpenRouter - NVIDIA models
'nvidia/llama-3.3-nemotron-super-49b-v1': 'Llama 3.3 Nemotron Super 49B',
'nvidia/llama-3.1-nemotron-ultra-253b-v1': 'Llama 3.1 Nemotron Ultra 253B',
'nvidia/llama-3.1-nemotron-70b-instruct': 'Llama 3.1 Nemotron 70B',
// OpenRouter - Other models
'minimax/minimax-01': 'MiniMax-01',
'minimax/minimax-m1': 'MiniMax-M1',
'liquid/lfm-40b': 'LFM 40B',
'liquid/lfm-7b': 'LFM 7B',
'liquid/lfm-3b': 'LFM 3B',
'cohere/command-a-03-2025': 'Command A',
'cohere/command-r7b-12-2024': 'Command R7B',
'cohere/command-r-plus': 'Command R Plus',
'cohere/command-r': 'Command R',
'amazon/nova-pro-v1': 'Nova Pro',
'amazon/nova-lite-v1': 'Nova Lite',
'amazon/nova-micro-v1': 'Nova Micro',
'ai21/jamba-1.6-large': 'Jamba 1.6 Large',
'ai21/jamba-1.6-mini': 'Jamba 1.6 Mini',
'01-ai/yi-large': 'Yi Large',
'inflection/inflection-3-productivity': 'Inflection 3 Productivity',
'inflection/inflection-3-pi': 'Inflection 3 Pi',
'rekaai/reka-flash-3': 'Reka Flash 3',
'openrouter/auto': 'Auto (Best Available)'
};
return modelNames[model] || model;
}
}

View File

@@ -0,0 +1,457 @@
import * as vscode from 'vscode';
import { ClaudeCodeService } from './claudeCodeService';
import { AgentService } from '../types/agent';
import { CoreMessage } from 'ai';
import { Logger } from './logger';
export class ChatMessageService {
private currentRequestController?: AbortController;
constructor(
private agentService: AgentService,
private outputChannel: vscode.OutputChannel
) {}
async handleChatMessage(message: any, webview: vscode.Webview): Promise<void> {
try {
const chatHistory: CoreMessage[] = message.chatHistory || [];
const latestMessage = message.message || '';
const messageContent = message.messageContent || latestMessage;
console.log('========chatHistory', chatHistory);
Logger.info(`Chat message received with ${chatHistory.length} history messages`);
Logger.info(`Latest message: ${latestMessage}`);
// Debug structured content
if (typeof messageContent !== 'string' && Array.isArray(messageContent)) {
Logger.info(`Structured content: ${messageContent.length} parts`);
messageContent.forEach((part, index) => {
if (part.type === 'text') {
Logger.info(` [${index}] text: "${part.text?.substring(0, 100)}..."`);
} else if (part.type === 'image') {
Logger.info(` [${index}] image: ${part.mimeType || 'unknown type'} (${part.image?.length || 0} chars)`);
}
});
} else {
Logger.info(`Simple text content: ${String(messageContent).substring(0, 100)}...`);
}
// Create new AbortController for this request
this.currentRequestController = new AbortController();
// Send initial streaming start message
webview.postMessage({
command: 'chatStreamStart'
});
// Debug log chat history with VS Code output channel
this.outputChannel.appendLine('=== CHAT HISTORY DEBUG ===');
this.outputChannel.appendLine(`📥 Input: ${chatHistory.length} CoreMessage messages`);
// Log each message
this.outputChannel.appendLine('📋 Chat history:');
chatHistory.forEach((msg, index) => {
const content = typeof msg.content === 'string' ? msg.content :
Array.isArray(msg.content) ?
msg.content.map(part =>
part.type === 'text' ? part.text?.substring(0, 50) + '...' :
part.type === 'tool-call' ? `[tool-call: ${part.toolName}]` :
part.type === 'tool-result' ? `[tool-result: ${part.toolName}]` :
`[${part.type}]`
).join(', ') :
'[complex content]';
this.outputChannel.appendLine(` [${index}] ${msg.role}: "${content.substring(0, 100)}..."`);
});
this.outputChannel.appendLine('=== END CHAT HISTORY DEBUG ===');
// Use conversation history or single prompt
let response: any[];
if (chatHistory.length > 0) {
// Use conversation history - CoreMessage format is already compatible
this.outputChannel.appendLine(`Using conversation history with ${chatHistory.length} messages`);
response = await this.agentService.query(
undefined, // no prompt
chatHistory, // use CoreMessage array directly
undefined,
this.currentRequestController,
(streamMessage: any) => {
// Process and send each message as it arrives
this.handleStreamMessage(streamMessage, webview);
}
);
} else {
// Fallback to single prompt for first message
this.outputChannel.appendLine('No conversation history, using single prompt');
response = await this.agentService.query(
latestMessage, // use latest message as prompt
undefined, // no messages array
undefined,
this.currentRequestController,
(streamMessage: any) => {
// Process and send each message as it arrives
this.handleStreamMessage(streamMessage, webview);
}
);
}
// Check if request was aborted
if (this.currentRequestController.signal.aborted) {
Logger.warn('Request was aborted');
return;
}
Logger.info(`Agent response completed with ${response.length} total messages`);
// Send stream end message
webview.postMessage({
command: 'chatStreamEnd'
});
} catch (error) {
// Check if the error is due to abort
if (this.currentRequestController?.signal.aborted) {
Logger.info('Request was stopped by user');
webview.postMessage({
command: 'chatStopped'
});
return;
}
Logger.error(`Chat message failed: ${error}`);
Logger.error(`Error type: ${typeof error}, constructor: ${error?.constructor?.name}`);
// Check if this is an API key authentication error or process failure
const errorMessage = error instanceof Error ? error.message : String(error);
Logger.error(`Processing error message: "${errorMessage}"`);
if (this.agentService.isApiKeyAuthError(errorMessage) || !this.agentService.hasApiKey()) {
// Determine which provider is currently selected to show specific error
const config = vscode.workspace.getConfiguration('superdesign');
const specificModel = config.get<string>('aiModel');
const provider = config.get<string>('aiModelProvider', 'anthropic');
// Determine provider from model name if specific model is set
let effectiveProvider = provider;
let providerName = 'AI';
let configureCommand = 'superdesign.configureApiKey';
if (specificModel) {
if (specificModel.includes('/')) {
effectiveProvider = 'openrouter';
} else if (specificModel.startsWith('claude-')) {
effectiveProvider = 'anthropic';
} else {
effectiveProvider = 'openai';
}
}
switch (effectiveProvider) {
case 'openrouter':
providerName = 'OpenRouter';
configureCommand = 'superdesign.configureOpenRouterApiKey';
break;
case 'anthropic':
providerName = 'Anthropic';
configureCommand = 'superdesign.configureApiKey';
break;
case 'openai':
providerName = 'OpenAI';
configureCommand = 'superdesign.configureOpenAIApiKey';
break;
}
const hasApiKey = this.agentService.hasApiKey();
const displayMessage = hasApiKey ?
`Invalid ${providerName} API key. Please check your configuration.` :
`${providerName} API key not configured. Please set up your API key to use this AI model.`;
webview.postMessage({
command: 'chatErrorWithActions',
error: displayMessage,
actions: [
{ text: `Configure ${providerName} API Key`, command: configureCommand },
{ text: 'Open Settings', command: 'workbench.action.openSettings', args: '@ext:iganbold.superdesign' }
]
});
} else {
// Regular error - show standard error message
vscode.window.showErrorMessage(`Chat failed: ${error}`);
webview.postMessage({
command: 'chatError',
error: errorMessage
});
}
} finally {
// Clear the controller when done
this.currentRequestController = undefined;
}
}
private handleStreamMessage(message: CoreMessage, webview: vscode.Webview): void {
Logger.debug(`Handling CoreMessage: ${JSON.stringify(message, null, 2)}`);
// Check if this is an update to existing message
const isUpdate = (message as any)._isUpdate;
const updateToolId = (message as any)._updateToolId;
// Handle assistant messages
if (message.role === 'assistant') {
if (typeof message.content === 'string') {
// Simple text content
if (message.content.trim()) {
webview.postMessage({
command: 'chatResponseChunk',
messageType: 'assistant',
content: message.content,
metadata: {}
});
}
} else if (Array.isArray(message.content)) {
// Handle assistant content array (text parts, tool calls, etc.)
for (const part of message.content) {
if (part.type === 'text' && (part as any).text) {
// Send text content
webview.postMessage({
command: 'chatResponseChunk',
messageType: 'assistant',
content: (part as any).text,
metadata: {}
});
} else if (part.type === 'tool-call') {
// Send tool call or update
const toolPart = part as any;
const command = isUpdate ? 'chatToolUpdate' : 'chatResponseChunk';
const messageType = isUpdate ? undefined : 'tool-call';
if (isUpdate) {
// Send tool parameter update
webview.postMessage({
command: 'chatToolUpdate',
tool_use_id: toolPart.toolCallId,
tool_input: toolPart.args
});
} else {
// Send new tool call message
webview.postMessage({
command: 'chatResponseChunk',
messageType: 'tool-call',
content: '',
metadata: {
tool_name: toolPart.toolName,
tool_id: toolPart.toolCallId,
tool_input: toolPart.args
}
});
}
}
}
}
}
// Handle tool messages (CoreToolMessage)
if (message.role === 'tool' && Array.isArray(message.content)) {
for (const toolResultPart of message.content) {
if (toolResultPart.type === 'tool-result') {
const part = toolResultPart as any;
const content = typeof part.result === 'string' ?
part.result :
JSON.stringify(part.result, null, 2);
Logger.debug(`Tool result for ${part.toolCallId}: "${content.substring(0, 200)}..."`);
// Send tool result to frontend
webview.postMessage({
command: 'chatResponseChunk',
messageType: 'tool-result',
content: content,
metadata: {
tool_id: part.toolCallId,
tool_name: part.toolName,
is_error: part.isError || false
}
});
// Also send completion signal
webview.postMessage({
command: 'chatToolResult',
tool_use_id: part.toolCallId,
content: content,
is_error: part.isError || false
});
}
}
}
// Handle user messages
if (message.role === 'user') {
if (typeof message.content === 'string' && message.content.trim()) {
webview.postMessage({
command: 'chatResponseChunk',
messageType: 'user',
content: message.content,
metadata: {}
});
}
}
// Skip other message types (system, etc.)
}
// Legacy handler for backward compatibility
private handleLegacyResultMessage(message: any, webview: vscode.Webview): void {
if (message.type === 'result') {
Logger.debug(`Result message structure: ${JSON.stringify(message, null, 2)}`);
// Skip error result messages that contain raw API key errors - these are handled by our custom error handler
if (message.is_error) {
// Check if this is an API key related error in any field
const messageStr = JSON.stringify(message).toLowerCase();
if (messageStr.includes('api key') || messageStr.includes('authentication') ||
messageStr.includes('unauthorized') || messageStr.includes('anthropic') ||
messageStr.includes('process exited') || messageStr.includes('exit code')) {
Logger.debug('Skipping raw API key error result message - handled by custom error handler');
return;
}
}
// Skip final success result messages that are just summaries
if (message.subtype === 'success' && message.result && typeof message.result === 'string') {
const resultText = message.result.toLowerCase();
// Skip if it looks like a final summary (contains phrases like "successfully created", "perfect", etc.)
if (resultText.includes('successfully') || resultText.includes('perfect') ||
resultText.includes('created') || resultText.includes('variations')) {
Logger.debug('Skipping final summary result message');
return;
}
}
let content = '';
let resultType = 'result';
let isError = false;
if (typeof message.message === 'string') {
content = message.message;
} else if (message.content) {
content = typeof message.content === 'string' ? message.content : JSON.stringify(message.content);
} else if (message.text) {
content = message.text;
} else if (message.result && typeof message.result === 'string') {
content = message.result;
} else {
// Skip messages that would result in raw JSON dump
Logger.debug('Skipping result message with no readable content');
return;
}
// Determine result type and error status
if (message.subtype) {
if (message.subtype.includes('error')) {
isError = true;
resultType = 'error';
} else if (message.subtype === 'success') {
resultType = 'success';
}
}
Logger.debug(`Extracted result content: "${content.substring(0, 200)}..."`);
if (content.trim()) {
webview.postMessage({
command: 'chatResponseChunk',
messageType: 'tool-result',
content: content,
metadata: {
session_id: message.session_id,
parent_tool_use_id: message.parent_tool_use_id,
result_type: resultType,
is_error: isError,
duration_ms: message.duration_ms,
total_cost_usd: message.total_cost_usd
}
});
}
}
// Log tool activity
if ((message.type === 'assistant' || message.type === 'user') && ('subtype' in message) && (message.subtype === 'tool_use' || message.subtype === 'tool_result')) {
Logger.debug(`Tool activity detected: ${message.subtype}`);
}
}
async stopCurrentChat(webview: vscode.Webview): Promise<void> {
if (this.currentRequestController) {
Logger.info('Stopping current chat request');
this.currentRequestController.abort();
// Send stopped message back to webview
webview.postMessage({
command: 'chatStopped'
});
} else {
Logger.info('No active chat request to stop');
}
}
private processClaudeResponse(response: any[]): string {
let fullResponse = '';
let assistantMessages: string[] = [];
let toolResults: string[] = [];
for (const msg of response) {
const subtype = 'subtype' in msg ? msg.subtype : undefined;
Logger.debug(`Processing message type: ${msg.type}${subtype ? `, subtype: ${subtype}` : ''}`);
// Collect assistant messages
if (msg.type === 'assistant' && msg.message) {
let content = '';
if (typeof msg.message === 'string') {
content = msg.message;
} else if (msg.message.content && Array.isArray(msg.message.content)) {
content = msg.message.content
.filter((item: any) => item.type === 'text')
.map((item: any) => item.text)
.join('\n');
} else if (msg.message.content && typeof msg.message.content === 'string') {
content = msg.message.content;
}
if (content.trim()) {
assistantMessages.push(content);
}
}
// Collect tool results
if (msg.type === 'result' && msg.subtype === 'success' && msg.result) {
const result = typeof msg.result === 'string' ? msg.result : JSON.stringify(msg.result, null, 2);
toolResults.push(result);
}
// Handle tool usage messages
if ((msg.type === 'assistant' || msg.type === 'user') && ('subtype' in msg) && (msg.subtype === 'tool_use' || msg.subtype === 'tool_result')) {
Logger.debug(`Tool activity detected: ${msg.subtype}`);
}
}
// Combine all responses
if (assistantMessages.length > 0) {
fullResponse = assistantMessages.join('\n\n');
}
if (toolResults.length > 0 && !fullResponse.includes(toolResults[0])) {
if (fullResponse) {
fullResponse += '\n\n--- Tool Results ---\n' + toolResults.join('\n\n');
} else {
fullResponse = toolResults.join('\n\n');
}
}
if (!fullResponse) {
fullResponse = 'I processed your request but didn\'t generate a visible response. Check the console for details.';
}
return fullResponse;
}
}

View File

@@ -0,0 +1,458 @@
// This is deprecated, use customAgentService instead
import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs';
import * as os from 'os';
import { Logger } from './logger';
// Dynamic import types for Claude Code
type SDKMessage = any; // Will be properly typed when imported
type ClaudeCodeOptions = any; // Will be properly typed when imported
type QueryFunction = (params: {
prompt: string;
abortController?: AbortController;
options?: any;
}) => AsyncGenerator<SDKMessage>;
export class ClaudeCodeService {
private isInitialized = false;
private initializationPromise: Promise<void> | null = null;
private workingDirectory: string = '';
private outputChannel: vscode.OutputChannel;
private currentSessionId: string | null = null;
private claudeCodeQuery: QueryFunction | null = null;
constructor(outputChannel: vscode.OutputChannel) {
this.outputChannel = outputChannel;
// Initialize on construction
this.initializationPromise = this.initialize();
}
private async initialize(): Promise<void> {
if (this.isInitialized) {
return;
}
try {
Logger.info('Starting Claude Code initialization...');
if (os.platform() === 'win32') {
throw new Error('Claude Code is not supported on Windows.');
}
// Setup working directory first
await this.setupWorkingDirectory();
// Check if API key is configured
const config = vscode.workspace.getConfiguration('superdesign');
const apiKey = config.get<string>('anthropicApiKey');
if (!apiKey) {
Logger.warn('No API key found');
throw new Error('Missing API key');
}
// Set the environment variable for Claude Code SDK
process.env.ANTHROPIC_API_KEY = apiKey;
// Dynamically import Claude Code SDK
Logger.info('Importing Claude Code SDK...');
// try {
// // Try importing from the copied module location first
// let claudeCodeModule;
// try {
// // Try multiple possible paths for the extension location
// const possiblePaths = [
// path.resolve(__dirname, '..', 'node_modules', '@anthropic-ai', 'claude-code', 'sdk.mjs'),
// path.resolve(__dirname, 'node_modules', '@anthropic-ai', 'claude-code', 'sdk.mjs'),
// path.join(__dirname, '..', 'node_modules', '@anthropic-ai', 'claude-code', 'sdk.mjs')
// ];
// let importSucceeded = false;
// for (const modulePath of possiblePaths) {
// try {
// if (fs.existsSync(modulePath)) {
// claudeCodeModule = await import(`file://${modulePath}`);
// importSucceeded = true;
// break;
// }
// } catch (pathError) {
// continue;
// }
// }
// if (!importSucceeded) {
// throw new Error('All local import paths failed');
// }
// } catch (localImportError) {
// // Fallback to standard import
// try {
// // claudeCodeModule = await import('@anthropic-ai/claude-code');
// } catch (standardImportError) {
// Logger.error(`Claude Code SDK import failed: ${standardImportError}`);
// throw standardImportError;
// }
// }
// this.claudeCodeQuery = claudeCodeModule.query;
// if (!this.claudeCodeQuery) {
// throw new Error('Query function not found in Claude Code module');
// }
// Logger.info('Claude Code SDK imported successfully');
// } catch (importError) {
// Logger.error(`Failed to import Claude Code SDK: ${importError}`);
// throw new Error(`Claude Code SDK import failed: ${importError}`);
// }
this.isInitialized = true;
Logger.info('Claude Code SDK initialized successfully');
} catch (error) {
Logger.error(`Failed to initialize Claude Code SDK: ${error}`);
// Check if this is an API key related error (no UI popup needed here as error will be handled in chat)
const errorMessage = error instanceof Error ? error.message : String(error);
if (!this.isApiKeyAuthError(errorMessage)) {
vscode.window.showErrorMessage(`Failed to initialize Claude Code: ${error}`);
}
// Reset initialization promise so it can be retried
this.initializationPromise = null;
this.isInitialized = false;
throw error;
}
}
private async setupWorkingDirectory(): Promise<void> {
try {
// Try to get workspace root first
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
if (workspaceRoot) {
// Create .superdesign folder in workspace root
const superdesignDir = path.join(workspaceRoot, '.superdesign');
// Create directory if it doesn't exist
if (!fs.existsSync(superdesignDir)) {
fs.mkdirSync(superdesignDir, { recursive: true });
Logger.info(`Created .superdesign directory: ${superdesignDir}`);
}
this.workingDirectory = superdesignDir;
} else {
Logger.warn('No workspace root found, using temporary directory');
// Fallback to OS temp directory if no workspace
const tempDir = path.join(os.tmpdir(), 'superdesign-claude');
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true });
Logger.info(`Created temporary directory: ${tempDir}`);
}
this.workingDirectory = tempDir;
vscode.window.showWarningMessage(
'No workspace folder found. Using temporary directory for Claude Code operations.'
);
}
} catch (error) {
Logger.error(`Failed to setup working directory: ${error}`);
// Final fallback to current working directory
this.workingDirectory = process.cwd();
Logger.warn(`Using current working directory as fallback: ${this.workingDirectory}`);
}
}
private async ensureInitialized(): Promise<void> {
if (this.initializationPromise) {
await this.initializationPromise;
}
if (!this.isInitialized || !this.claudeCodeQuery) {
// Try to initialize if not already done
if (!this.initializationPromise) {
this.initializationPromise = this.initialize();
await this.initializationPromise;
} else {
throw new Error('Claude Code SDK not initialized');
}
}
}
async query(prompt?: string, conversationMessages?: any, options?: Partial<ClaudeCodeOptions>, abortController?: AbortController, onMessage?: (message: SDKMessage) => void): Promise<SDKMessage[]> {
// ClaudeCodeService handles conversation via internal session management
// so we ignore conversationMessages and just use the prompt
if (!prompt) {
throw new Error('ClaudeCodeService requires a prompt parameter');
}
Logger.info('=== QUERY FUNCTION CALLED ===');
Logger.info(`Query prompt: ${prompt.substring(0, 200)}...`);
Logger.info(`Query options: ${JSON.stringify(options, null, 2)}`);
Logger.info(`Streaming enabled: ${!!onMessage}`);
if (conversationMessages) {
Logger.info('Note: ClaudeCodeService ignores conversationMessages (uses internal session management)');
}
await this.ensureInitialized();
const messages: SDKMessage[] = [];
const systemPrompt = `# Role
You are a **senior front-end designer**.
You pay close attention to every pixel, spacing, font, color;
Whenever there are UI implementation task, think deeply of the design style first, and then implement UI bit by bit
# When asked to create design:
1. You ALWAYS spin up 3 parallel sub agents concurrently to implemeht one design with variations, so it's faster for user to iterate (Unless specifically asked to create only one version)
<task_for_each_sub_agent>
1. Build one single html page of just one screen to build a design based on users' feedback/task
2. You ALWAYS output design files in '.superdesign/design_iterations' folder as {design_name}_{n}.html (Where n needs to be unique like table_1.html, table_2.html, etc.) or svg file
3. If you are iterating design based on existing file, then the naming convention should be {current_file_name}_{n}.html, e.g. if we are iterating ui_1.html, then each version should be ui_1_1.html, ui_1_2.html, etc.
</task_for_each_sub_agent>
## When asked to design UI:
1. Similar process as normal design task, but refer to 'UI design & implementation guidelines' for guidelines
## When asked to update or iterate design:
1. Don't edit the existing design, just create a new html file with the same name but with _n.html appended to the end, e.g. if we are iterating ui_1.html, then each version should be ui_1_1.html, ui_1_2.html, etc.
2. At default you should spin up 3 parallel sub agents concurrently to try implement the design, so it's faster for user to iterate
## When asked to design logo or icon:
1. Copy/duplicate existing svg file but name it based on our naming convention in design_ierations folder, and then make edits to the copied svg file (So we can avoid lots of mistakes), like 'original_filename.svg .superdesign/design-iterations/new_filename.svg'
2. Very important sub agent copy first, and Each agent just copy & edit a single svg file with svg code
3. you should focus on the the correctness of the svg code
## When asked to design a component:
1. Similar process as normal design task, and each agent just create a single html page with component inside;
2. Focus just on just one component itself, and don't add any other elements or text
3. Each HTML just have one component with mock data inside
## When asked to design wireframes:
1. Focus on minimal line style black and white wireframes, no colors, and never include any images, just try to use css to make some placeholder images. (Don't use service like placehold.co too, we can't render it)
2. Don't add any annotation of styles, just basic wireframes like Balsamiq style
3. Focus on building out the flow of the wireframes
# When asked to extract design system from images:
Your goal is to extract a generalized and reusable design system from the screenshots provided, **without including specific image content**, so that frontend developers or AI agents can reference the JSON as a style foundation for building consistent UIs.
1. Analyze the screenshots provided:
* Color palette
* Typography rules
* Spacing guidelines
* Layout structure (grids, cards, containers, etc.)
* UI components (buttons, inputs, tables, etc.)
* Border radius, shadows, and other visual styling patterns
2. Create a design-system.json file in 'design_system' folder that clearly defines these rules and can be used to replicate the visual language in a consistent way.
3. if design-system.json already exist, then create a new file with the name design-system_{n}.json (Where n needs to be unique like design-system_1.json, design-system_2.json, etc.)
**Constraints**
* Do **not** extract specific content from the screenshots (no text, logos, icons).
* Focus purely on *design principles*, *structure*, and *styles*.
--------
# UI design & implementation guidelines:
## Design Style
- A **perfect balance** between **elegant minimalism** and **functional design**.
- **Soft, refreshing gradient colors** that seamlessly integrate with the brand palette.
- **Well-proportioned white space** for a clean layout.
- **Light and immersive** user experience.
- **Clear information hierarchy** using **subtle shadows and modular card layouts**.
- **Natural focus on core functionalities**.
- **Refined rounded corners**.
- **Delicate micro-interactions**.
- **Comfortable visual proportions**.
- **Responsive design** You only output responsive design, it needs to look perfect on both mobile, tablet and desktop.
- If its a mobile app, also make sure you have responsive design OR make the center the mobile UI
## Technical Specifications
1. **Images**: do NEVER include any images, we can't render images in webview,just try to use css to make some placeholder images. (Don't use service like placehold.co too, we can't render it)
2. **Styles**: Use **Tailwind CSS** via **CDN** for styling. (Use !important declarations for critical design tokens that must not be overridden, Load order management - ensure custom styles load after framework CSS, CSS-in-JS or scoped styles to avoid global conflicts, Use utility-first approach - define styles using Tailwind classes instead of custom CSS when possible)
3. **Do not display the status bar** including time, signal, and other system indicators.
4. **All text should be only black or white**.
5. Choose a **4 pt or 8 pt spacing system**—all margins, padding, line-heights, and element sizes must be exact multiples.
6. Use **consistent spacing tokens** (e.g., 4, 8, 16, 24, 32px) — never arbitrary values like 5 px or 13 px.
7. Apply **visual grouping** ("spacing friendship"): tighter gaps (48px) for related items, larger gaps (1624px) for distinct groups.
8. Ensure **typographic rhythm**: fontsizes, lineheights, and spacing aligned to the grid (e.g., 16 px text with 24 px line-height).
9. Maintain **touch-area accessibility**: buttons and controls should meet or exceed 48×48 px, padded using grid units.
## 🎨 Color Style
* Use a **minimal palette**: default to **black, white, and neutrals**—no flashy gradients or mismatched hues .
* Follow a **603010 ratio**: \~60% background (white/light gray), \~30% surface (white/medium gray), \~10% accents (charcoal/black) .
* Accent colors limited to **one subtle tint** (e.g., charcoal black or very soft beige). Interactive elements like links or buttons use this tone sparingly.
* Always check **contrast** for text vs background via WCAG (≥4.5:1)
## ✍️ Typography & Hierarchy
### 1. 🎯 Hierarchy Levels & Structure
* Always define at least **three typographic levels**: **Heading (H1)**, **Subheading (H2)**, and **Body**.
* Use **size, weight, color**, and **spacing** to create clear differences between them ([toptal.com][1], [skyryedesign.com][2]).
* H1 should stand out clearly (largest & boldest), H2 should be distinctly smaller/medium-weight, and body remains readable and lighter.
### 2. 📏 Size & Scale
* Follow a modular scale: e.g., **H1: 36px**, **H2: 28px**, **Body: 16px** (min). Adjust for mobile if needed .
* Maintain strong contrast—don't use size differences of only 2px; aim for at least **68px difference** between levels .
### 3. 🧠 Weight, Style & Color
* Use **bold or medium weight** for headings, **regular** for body.
* Utilize **color contrast** (e.g., darker headings, neutral body) to support hierarchy ([mews.design][3], [toptal.com][1]).
* Avoid excessive styles like italics or uppercase—unless used sparingly for emphasis or subheadings.
### 4. ✂️ Spacing & Rhythm
* Add **0.8×1.5× line-height** for body and headings to improve legibility ([skyryedesign.com][2]).
* Use consistent **margin spacing above/below headings** (e.g., margin-top: 1.2× line-height) .
`;
try {
const finalOptions: Partial<ClaudeCodeOptions> = {
maxTurns: 10,
allowedTools: [
'Read', 'Write', 'Edit', 'MultiEdit', 'Bash', 'LS', 'Grep', 'Glob'
],
permissionMode: 'acceptEdits' as const,
cwd: this.workingDirectory,
customSystemPrompt: systemPrompt,
...options
};
if (this.currentSessionId) {
finalOptions.resume = this.currentSessionId;
}
const queryParams = {
prompt: prompt!, // Non-null assertion since we checked above
abortController: abortController || new AbortController(),
options: finalOptions
};
if (!this.claudeCodeQuery) {
throw new Error('Claude Code SDK not properly initialized - query function not available');
}
for await (const message of this.claudeCodeQuery(queryParams)) {
messages.push(message as SDKMessage);
// Call the streaming callback if provided
if (onMessage) {
try {
onMessage(message as SDKMessage);
} catch (callbackError) {
Logger.error(`Streaming callback error: ${callbackError}`);
// Don't break the loop if callback fails
}
}
}
const lastMessageWithSessionId = [...messages].reverse().find(m => 'session_id' in m && m.session_id);
if (lastMessageWithSessionId && 'session_id' in lastMessageWithSessionId && lastMessageWithSessionId.session_id) {
this.currentSessionId = lastMessageWithSessionId.session_id;
}
Logger.info(`Query completed successfully. Received ${messages.length} messages`);
return messages;
} catch (error) {
Logger.error(`Claude Code query failed: ${error}`);
// Check if this is an API key authentication error (handled in chat interface)
const errorMessage = error instanceof Error ? error.message : String(error);
if (!this.isApiKeyAuthError(errorMessage)) {
vscode.window.showErrorMessage(`Claude Code query failed: ${error}`);
}
throw error;
}
}
get isReady(): boolean {
return this.isInitialized;
}
async waitForInitialization(): Promise<boolean> {
try {
await this.ensureInitialized();
return true;
} catch (error) {
Logger.error(`Initialization failed: ${error}`);
return false;
}
}
getWorkingDirectory(): string {
return this.workingDirectory;
}
// Method to refresh API key from settings and reinitialize if needed
async refreshApiKey(): Promise<boolean> {
try {
const config = vscode.workspace.getConfiguration('superdesign');
const apiKey = config.get<string>('anthropicApiKey');
if (!apiKey) {
Logger.warn('No API key found during refresh');
return false;
}
// Update environment variable
process.env.ANTHROPIC_API_KEY = apiKey;
Logger.info('API key refreshed from settings');
// If not initialized yet, try to initialize
if (!this.isInitialized) {
try {
await this.initialize();
return true;
} catch (error) {
Logger.error(`Failed to initialize after API key refresh: ${error}`);
return false;
}
}
return true;
} catch (error) {
Logger.error(`Failed to refresh API key: ${error}`);
return false;
}
}
// Method to check if API key is configured
hasApiKey(): boolean {
const config = vscode.workspace.getConfiguration('superdesign');
const apiKey = config.get<string>('anthropicApiKey');
return !!apiKey && apiKey.trim().length > 0;
}
// Method to detect if an error is related to API key authentication
public isApiKeyAuthError(errorMessage: string): boolean {
const authErrorPatterns = [
'authentication failed',
'invalid api key',
'unauthorized',
'api key',
'authentication error',
'invalid token',
'access denied',
'401',
'ANTHROPIC_API_KEY',
'process exited with code 1',
'claude code process exited',
'exit code 1'
];
const lowercaseMessage = errorMessage.toLowerCase();
const isAuthError = authErrorPatterns.some(pattern => lowercaseMessage.includes(pattern));
Logger.info(`Checking if error is auth-related: "${errorMessage}" -> ${isAuthError}`);
if (isAuthError) {
const matchedPattern = authErrorPatterns.find(pattern => lowercaseMessage.includes(pattern));
Logger.info(`Matched pattern: "${matchedPattern}"`);
}
return isAuthError;
}
}

View File

@@ -0,0 +1,931 @@
import { streamText, CoreMessage } from 'ai';
import { createOpenAI } from '@ai-sdk/openai';
import { createAnthropic } from '@ai-sdk/anthropic';
import { createOpenRouter } from '@openrouter/ai-sdk-provider';
import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs';
import * as os from 'os';
import { AgentService, ExecutionContext } from '../types/agent';
import { createReadTool } from '../tools/read-tool';
import { createWriteTool } from '../tools/write-tool';
import { createBashTool } from '../tools/bash-tool';
import { createEditTool } from '../tools/edit-tool';
import { createGlobTool } from '../tools/glob-tool';
import { createGrepTool } from '../tools/grep-tool';
import { createThemeTool } from '../tools/theme-tool';
import { createLsTool } from '../tools/ls-tool';
import { createMultieditTool } from '../tools/multiedit-tool';
export class CustomAgentService implements AgentService {
private workingDirectory: string = '';
private outputChannel: vscode.OutputChannel;
private isInitialized = false;
constructor(outputChannel: vscode.OutputChannel) {
this.outputChannel = outputChannel;
this.outputChannel.appendLine('CustomAgentService constructor called');
this.setupWorkingDirectory();
}
private async setupWorkingDirectory(): Promise<void> {
try {
// Try to get workspace root first
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
this.outputChannel.appendLine(`Workspace root detected: ${workspaceRoot}`);
if (workspaceRoot) {
// Create .superdesign folder in workspace root
const superdesignDir = path.join(workspaceRoot, '.superdesign');
this.outputChannel.appendLine(`Setting up .superdesign directory at: ${superdesignDir}`);
// Create directory if it doesn't exist
if (!fs.existsSync(superdesignDir)) {
fs.mkdirSync(superdesignDir, { recursive: true });
this.outputChannel.appendLine(`Created .superdesign directory: ${superdesignDir}`);
} else {
this.outputChannel.appendLine(`.superdesign directory already exists: ${superdesignDir}`);
}
this.workingDirectory = superdesignDir;
this.outputChannel.appendLine(`Working directory set to: ${this.workingDirectory}`);
} else {
this.outputChannel.appendLine('No workspace root found, using fallback');
// Fallback to OS temp directory if no workspace
const tempDir = path.join(os.tmpdir(), 'superdesign-custom');
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true });
this.outputChannel.appendLine(`Created temporary superdesign directory: ${tempDir}`);
}
this.workingDirectory = tempDir;
this.outputChannel.appendLine(`Working directory set to (fallback): ${this.workingDirectory}`);
vscode.window.showWarningMessage(
'No workspace folder found. Using temporary directory for Custom Agent operations.'
);
}
this.isInitialized = true;
} catch (error) {
this.outputChannel.appendLine(`Failed to setup working directory: ${error}`);
// Final fallback to current working directory
this.workingDirectory = process.cwd();
this.outputChannel.appendLine(`Working directory set to (final fallback): ${this.workingDirectory}`);
this.isInitialized = true;
}
}
private getModel() {
const config = vscode.workspace.getConfiguration('superdesign');
const specificModel = config.get<string>('aiModel');
const provider = config.get<string>('aiModelProvider', 'anthropic');
this.outputChannel.appendLine(`Using AI provider: ${provider}`);
if (specificModel) {
this.outputChannel.appendLine(`Using specific AI model: ${specificModel}`);
}
// Determine provider from model name if specific model is set
let effectiveProvider = provider;
if (specificModel) {
if (specificModel.includes('/')) {
effectiveProvider = 'openrouter';
} else if (specificModel.startsWith('claude-')) {
effectiveProvider = 'anthropic';
} else {
effectiveProvider = 'openai';
}
}
switch (effectiveProvider) {
case 'openrouter':
const openrouterKey = config.get<string>('openrouterApiKey');
if (!openrouterKey) {
throw new Error('OpenRouter API key not configured. Please run "Configure OpenRouter API Key" command.');
}
this.outputChannel.appendLine(`OpenRouter API key found: ${openrouterKey.substring(0, 12)}...`);
const openrouterBaseURL = config.get<string>('openrouterBaseURL');
const openrouter = createOpenRouter({
apiKey: openrouterKey,
baseURL: openrouterBaseURL
});
// Use specific model if available, otherwise default to Claude 3.7 Sonnet via OpenRouter
const openrouterModel = specificModel || 'anthropic/claude-3-7-sonnet-20250219';
this.outputChannel.appendLine(`Using OpenRouter model: ${openrouterModel}`);
return openrouter.chat(openrouterModel);
case 'anthropic':
const anthropicKey = config.get<string>('anthropicApiKey');
if (!anthropicKey) {
throw new Error('Anthropic API key not configured. Please run "Configure Anthropic API Key" command.');
}
this.outputChannel.appendLine(`Anthropic API key found: ${anthropicKey.substring(0, 12)}...`);
const anthropicBaseURL = config.get<string>('anthropicBaseURL');
const anthropic = createAnthropic({
apiKey: anthropicKey,
baseURL: anthropicBaseURL,
});
// Use specific model if available, otherwise default to claude-3-5-sonnet
const anthropicModel = specificModel || 'claude-3-5-sonnet-20241022';
this.outputChannel.appendLine(`Using Anthropic model: ${anthropicModel}`);
return anthropic(anthropicModel);
case 'openai':
default:
const openaiKey = config.get<string>('openaiApiKey');
if (!openaiKey) {
throw new Error('OpenAI API key not configured. Please run "Configure OpenAI API Key" command.');
}
this.outputChannel.appendLine(`OpenAI API key found: ${openaiKey.substring(0, 7)}...`);
const openaiBaseURL = config.get<string>('openaiBaseURL');
const openai = createOpenAI({
apiKey: openaiKey,
baseURL: openaiBaseURL,
});
// Use specific model if available, otherwise default to gpt-4o
let openaiModel = specificModel || 'gpt-4o';
if (openaiModel === 'openai-auto') {
openaiModel = config.get<string>('aiModelAuto') || 'gpt-4o';
this.outputChannel.appendLine(`AUTO model detected, using configured model: ${openaiModel}`);
}
this.outputChannel.appendLine(`Using OpenAI model: ${openaiModel}`);
return openai(openaiModel);
}
}
private getSystemPrompt(): string {
const config = vscode.workspace.getConfiguration('superdesign');
const specificModel = config.get<string>('aiModel');
const provider = config.get<string>('aiModelProvider', 'anthropic');
// Determine the actual model name being used
let modelName: string;
if (specificModel) {
modelName = specificModel;
} else {
// Use defaults based on provider
switch (provider) {
case 'openai':
modelName = 'gpt-4o';
break;
case 'openrouter':
modelName = 'anthropic/claude-3-7-sonnet-20250219';
break;
case 'anthropic':
default:
modelName = 'claude-3-5-sonnet-20241022';
break;
}
}
return `# Role
You are superdesign, a senior frontend designer integrated into VS Code as part of the Super Design extension.
Your goal is to help user generate amazing design using code
# Current Context
- Extension: Super Design (Design Agent for VS Code)
- AI Model: ${modelName}
- Working directory: ${this.workingDirectory}
# Instructions
- Use the available tools when needed to help with file operations and code analysis
- When creating design file:
- Build one single html page of just one screen to build a design based on users' feedback/task
- You ALWAYS output design files in 'design_iterations' folder as {design_name}_{n}.html (Where n needs to be unique like table_1.html, table_2.html, etc.) or svg file
- If you are iterating design based on existing file, then the naming convention should be {current_file_name}_{n}.html, e.g. if we are iterating ui_1.html, then each version should be ui_1_1.html, ui_1_2.html, etc.
- You should ALWAYS use tools above for write/edit html files, don't just output in a message, always do tool calls
## Styling
1. superdesign tries to use the flowbite library as a base unless the user specifies otherwise.
2. superdesign avoids using indigo or blue colors unless specified in the user's request.
3. superdesign MUST generate responsive designs.
4. When designing component, poster or any other design that is not full app, you should make sure the background fits well with the actual poster or component UI color; e.g. if component is light then background should be dark, vice versa.
5. Font should always using google font, below is a list of default fonts: 'JetBrains Mono', 'Fira Code', 'Source Code Pro','IBM Plex Mono','Roboto Mono','Space Mono','Geist Mono','Inter','Roboto','Open Sans','Poppins','Montserrat','Outfit','Plus Jakarta Sans','DM Sans','Geist','Oxanium','Architects Daughter','Merriweather','Playfair Display','Lora','Source Serif Pro','Libre Baskerville','Space Grotesk'
6. When creating CSS, make sure you include !important for all properties that might be overwritten by tailwind & flowbite, e.g. h1, body, etc.
7. Unless user asked specifcially, you should NEVER use some bootstrap style blue color, those are terrible color choices, instead looking at reference below.
8. Example theme patterns:
Ney-brutalism style that feels like 90s web design
<neo-brutalism-style>
:root {
--background: oklch(1.0000 0 0);
--foreground: oklch(0 0 0);
--card: oklch(1.0000 0 0);
--card-foreground: oklch(0 0 0);
--popover: oklch(1.0000 0 0);
--popover-foreground: oklch(0 0 0);
--primary: oklch(0.6489 0.2370 26.9728);
--primary-foreground: oklch(1.0000 0 0);
--secondary: oklch(0.9680 0.2110 109.7692);
--secondary-foreground: oklch(0 0 0);
--muted: oklch(0.9551 0 0);
--muted-foreground: oklch(0.3211 0 0);
--accent: oklch(0.5635 0.2408 260.8178);
--accent-foreground: oklch(1.0000 0 0);
--destructive: oklch(0 0 0);
--destructive-foreground: oklch(1.0000 0 0);
--border: oklch(0 0 0);
--input: oklch(0 0 0);
--ring: oklch(0.6489 0.2370 26.9728);
--chart-1: oklch(0.6489 0.2370 26.9728);
--chart-2: oklch(0.9680 0.2110 109.7692);
--chart-3: oklch(0.5635 0.2408 260.8178);
--chart-4: oklch(0.7323 0.2492 142.4953);
--chart-5: oklch(0.5931 0.2726 328.3634);
--sidebar: oklch(0.9551 0 0);
--sidebar-foreground: oklch(0 0 0);
--sidebar-primary: oklch(0.6489 0.2370 26.9728);
--sidebar-primary-foreground: oklch(1.0000 0 0);
--sidebar-accent: oklch(0.5635 0.2408 260.8178);
--sidebar-accent-foreground: oklch(1.0000 0 0);
--sidebar-border: oklch(0 0 0);
--sidebar-ring: oklch(0.6489 0.2370 26.9728);
--font-sans: DM Sans, sans-serif;
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
--font-mono: Space Mono, monospace;
--radius: 0px;
--shadow-2xs: 4px 4px 0px 0px hsl(0 0% 0% / 0.50);
--shadow-xs: 4px 4px 0px 0px hsl(0 0% 0% / 0.50);
--shadow-sm: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 1px 2px -1px hsl(0 0% 0% / 1.00);
--shadow: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 1px 2px -1px hsl(0 0% 0% / 1.00);
--shadow-md: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 2px 4px -1px hsl(0 0% 0% / 1.00);
--shadow-lg: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 4px 6px -1px hsl(0 0% 0% / 1.00);
--shadow-xl: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 8px 10px -1px hsl(0 0% 0% / 1.00);
--shadow-2xl: 4px 4px 0px 0px hsl(0 0% 0% / 2.50);
--tracking-normal: 0em;
--spacing: 0.25rem;
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}
</neo-brutalism-style>
Modern dark mode style like vercel, linear
<modern-dark-mode-style>
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.1450 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.1450 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.1450 0 0);
--primary: oklch(0.2050 0 0);
--primary-foreground: oklch(0.9850 0 0);
--secondary: oklch(0.9700 0 0);
--secondary-foreground: oklch(0.2050 0 0);
--muted: oklch(0.9700 0 0);
--muted-foreground: oklch(0.5560 0 0);
--accent: oklch(0.9700 0 0);
--accent-foreground: oklch(0.2050 0 0);
--destructive: oklch(0.5770 0.2450 27.3250);
--destructive-foreground: oklch(1 0 0);
--border: oklch(0.9220 0 0);
--input: oklch(0.9220 0 0);
--ring: oklch(0.7080 0 0);
--chart-1: oklch(0.8100 0.1000 252);
--chart-2: oklch(0.6200 0.1900 260);
--chart-3: oklch(0.5500 0.2200 263);
--chart-4: oklch(0.4900 0.2200 264);
--chart-5: oklch(0.4200 0.1800 266);
--sidebar: oklch(0.9850 0 0);
--sidebar-foreground: oklch(0.1450 0 0);
--sidebar-primary: oklch(0.2050 0 0);
--sidebar-primary-foreground: oklch(0.9850 0 0);
--sidebar-accent: oklch(0.9700 0 0);
--sidebar-accent-foreground: oklch(0.2050 0 0);
--sidebar-border: oklch(0.9220 0 0);
--sidebar-ring: oklch(0.7080 0 0);
--font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--radius: 0.625rem;
--shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
--shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
--shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
--shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
--shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10);
--shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10);
--shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10);
--shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
--tracking-normal: 0em;
--spacing: 0.25rem;
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}
</modern-dark-mode-style>
## Images & icons
1. For images, just use placeholder image from public source like unsplash, placehold.co or others that you already know exact image url; Don't make up urls
2. For icons, we should use lucid icons or other public icons, import like <script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
## Script
1. When importing tailwind css, just use <script src="https://cdn.tailwindcss.com"></script>, don't load CSS directly as a stylesheet resource like <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
2. When using flowbite, import like <script src="https://cdn.jsdelivr.net/npm/flowbite@2.0.0/dist/flowbite.min.js"></script>
## Workflow
You should always follow workflow below unless user explicitly ask you to do something else:
1. Layout design
2. Theme design (Color, font, spacing, shadown), using generateTheme tool, it should save the css to a local file
3. Core Animation design
4. Generate a singlehtml file for the UI
5. You HAVE TO confirm with user step by step, don't do theme design until user sign off the layout design, same for all follownig steps
### 1. Layout design
Think through how should the layout of interface look like, what are different UI components
And present the layout in ASCII wireframe format, here are the guidelines of good ASCII wireframe, you can do ASCII art too for more custom layout or graphic design
### 2. Theme design
Think through what are the colors, fonts, spacing, etc.
### 3. Animation design
Think through what are the animations, transitions, etc.
### 4. Generate html file for each UI component and then combine them together to form a single html file
Generate html file for each UI component and then combine them together to form a single html file
Make sure to reference the theme css file you created in step 2, and add custom ones that doesn't exist yet in html file
<example>
<user>design an AI chat UI</user>
<assistant>
Let's think through the layout design for an AI chat UI. Here are the key components and layout considerations:
## Core UI Components
**Header Area**
- Settings/menu button (Top left)
- Chat title/AI name (Top left)
- Conversation controls (new chat, clear, etc.) (Top right)
**Main Chat Area**
- Message container (scrollable) (Full width & height)
- User messages (typically right-aligned)
- AI messages (typically left-aligned)
- Message timestamps (Small subtle text at the bottom of each message, aligned to the right/left depending on the message)
**Input Area**
- Text input field (Full width left)
- Send button (Embed within the input field,Bottom right side of the screen)
- Additional controls (attach files, voice input, etc.) (Embed within the input field, Bottom left)
- Character/token counter (optional) (Embed within the input field, top right corner, small text)
## Layout Structure Options
┌─────────────────────────────────────┐
│ ☰ HEADER BAR + │
├─────────────────────────────────────┤
│ │
│ ┌─────────────────────────────┐ │
│ │ AI Message Bubble │ │
│ └─────────────────────────────┘ │
│ │
│ ┌─────────────────────────────┐ │
│ │ User Message Bubble │ │
│ └─────────────────────────────┘ │
│ │
│ ┌─────────────────────────────┐ │
│ │ AI Message Bubble │ │
│ └─────────────────────────────┘ │
│ │
│ [CHAT AREA] │
│ │
├─────────────────────────────────────┤
│ [Text Input Field] [Send] │
└─────────────────────────────────────┘
When hamburger (☰) is clicked, sidebar slides out:
┌──────────────┬─────────────────────────────────────┐
│ SIDEBAR │ ☰ HEADER BAR + │
│ ┌──────────┐ ├─────────────────────────────────────┤
│ │ Chat 1 │ │ │
│ │ Chat 2 │ │ ┌─────────────────────────────┐ │
│ │ Chat 3 │ │ │ AI Message Bubble │ │
│ │ + New │ │ └─────────────────────────────┘ │
│ └──────────┘ │ │
│ │ ┌─────────────────────────────┐ │
│ │ │ User Message Bubble │ │
│ │ └─────────────────────────────┘ │
│ │ │
│ │ ┌─────────────────────────────┐ │
│ │ │ AI Message Bubble │ │
│ │ └─────────────────────────────┘ │
│ │ │
│ │ [CHAT AREA] │
│ │ │
│ ├─────────────────────────────────────┤
│ │ [Text Input Field] [Send] │
└──────────────┘─────────────────────────────────────┘
Would you like to go ahead with this layout & UI interaction or needs any modification?
</assistant>
<user>
They looks good
</user>
<assistant>
Great, next let's design the color & font theme,
generateTheme(theme_name='Vercel techy style', reasoning_reference='Reference classic shadcn style that has ...', cssFilePath='design_iterations/theme_1.css', cssSheet=':root {
--background: oklch(1.0000 0 0);
--foreground: oklch(0.1448 0 0);
--card: oklch(1.0000 0 0);
--card-foreground: oklch(0.1448 0 0);
--popover: oklch(1.0000 0 0);
--popover-foreground: oklch(0.1448 0 0);
--primary: oklch(0.5555 0 0);
--primary-foreground: oklch(0.9851 0 0);
--secondary: oklch(0.9702 0 0);
--secondary-foreground: oklch(0.2046 0 0);
--muted: oklch(0.9702 0 0);
--muted-foreground: oklch(0.5486 0 0);
--accent: oklch(0.9702 0 0);
--accent-foreground: oklch(0.2046 0 0);
--destructive: oklch(0.5830 0.2387 28.4765);
--destructive-foreground: oklch(0.9702 0 0);
--border: oklch(0.9219 0 0);
--input: oklch(0.9219 0 0);
--ring: oklch(0.7090 0 0);
--chart-1: oklch(0.5555 0 0);
--chart-2: oklch(0.5555 0 0);
--chart-3: oklch(0.5555 0 0);
--chart-4: oklch(0.5555 0 0);
--chart-5: oklch(0.5555 0 0);
--sidebar: oklch(0.9851 0 0);
--sidebar-foreground: oklch(0.1448 0 0);
--sidebar-primary: oklch(0.2046 0 0);
--sidebar-primary-foreground: oklch(0.9851 0 0);
--sidebar-accent: oklch(0.9702 0 0);
--sidebar-accent-foreground: oklch(0.2046 0 0);
--sidebar-border: oklch(0.9219 0 0);
--sidebar-ring: oklch(0.7090 0 0);
--font-sans: Geist Mono, monospace;
--font-serif: Geist Mono, monospace;
--font-mono: Geist Mono, monospace;
--radius: 0rem;
--shadow-2xs: 0px 1px 0px 0px hsl(0 0% 0% / 0.00);
--shadow-xs: 0px 1px 0px 0px hsl(0 0% 0% / 0.00);
--shadow-sm: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 1px 2px -1px hsl(0 0% 0% / 0.00);
--shadow: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 1px 2px -1px hsl(0 0% 0% / 0.00);
--shadow-md: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 2px 4px -1px hsl(0 0% 0% / 0.00);
--shadow-lg: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 4px 6px -1px hsl(0 0% 0% / 0.00);
--shadow-xl: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 8px 10px -1px hsl(0 0% 0% / 0.00);
--shadow-2xl: 0px 1px 0px 0px hsl(0 0% 0% / 0.00);
--tracking-normal: 0em;
--spacing: 0.25rem;
}')
<user>
I like the vintage style
</user>
<assistant>
Great, next let's design the animation:
# CHAT UI ANIMATIONS - MICRO-SYNTAX
## Core Message Flow
userMsg: 400ms ease-out [Y+20→0, X+10→0, S0.9→1]
aiMsg: 600ms bounce [Y+15→0, S0.95→1] +200ms
typing: 1400ms ∞ [Y±8, α0.4→1] stagger+200ms
status: 300ms ease-out [α0.6→1, S1→1.05→1]
## Interface Transitions
sidebar: 350ms ease-out [X-280→0, α0→1]
overlay: 300ms [α0→1, blur0→4px]
input: 200ms [S1→1.01, shadow+ring] focus
input: 150ms [S1.01→1, shadow-ring] blur
## Button Interactions
sendBtn: 150ms [S1→0.95→1, R±2°] press
sendBtn: 200ms [S1→1.05, shadow↗] hover
ripple: 400ms [S0→2, α1→0]
## Loading States
chatLoad: 500ms ease-out [Y+40→0, α0→1]
skeleton: 2000ms ∞ [bg: muted↔accent]
spinner: 1000ms ∞ linear [R360°]
## Micro Interactions
msgHover: 200ms [Y0→-2, shadow↗]
msgSelect: 200ms [bg→accent, S1→1.02]
error: 400ms [X±5] shake
success: 600ms bounce [S0→1.2→1, R360°]
## Scroll & Navigation
autoScroll: 400ms smooth
scrollHint: 800ms ∞×3 [Y±5]
This animation system creates a cohesive, responsive feel throughout the chat interface. Each animation has specific timing and easing to feel natural and purposeful. Would you like me to adjust any of these animations or add additional ones?
</assistant>
<user>
This looks great, lets do it
</user>
<assistant>
Great, I will generate the html file for each UI component and then combine them together to form a single html file
write(file_path='design_iterations/chat_ui.css', content='...')
write(file_path='design_iterations/chat_ui.html', content='...')
I've created the html design, please reveiw and let me know if you need any changes
</example>
# Available Tools
- **read**: Read file contents within the workspace (supports text files, images, with line range options)
- **write**: Write content to files in the workspace (creates parent directories automatically)
- **edit**: Replace text within files using exact string matching (requires precise text matching including whitespace and indentation)
- **multiedit**: Perform multiple find-and-replace operations on a single file in sequence (each edit applied to result of previous edit)
- **glob**: Find files and directories matching glob patterns (e.g., "*.js", "src/**/*.ts") - efficient for locating files by name or path structure
- **grep**: Search for text patterns within file contents using regular expressions (can filter by file types and paths)
- **ls**: List directory contents with optional filtering, sorting, and detailed information (shows files and subdirectories)
- **bash**: Execute shell/bash commands within the workspace (secure execution with timeouts and output capture)
- **generateTheme**: Generate a theme for the design
`;}
async query(
prompt?: string,
conversationHistory?: CoreMessage[],
options?: any,
abortController?: AbortController,
onMessage?: (message: any) => void
): Promise<any[]> {
this.outputChannel.appendLine('=== CUSTOM AGENT QUERY CALLED ===');
// Determine which input format we're using
const usingConversationHistory = !!conversationHistory && conversationHistory.length > 0;
if (usingConversationHistory) {
this.outputChannel.appendLine(`Query using conversation history: ${conversationHistory!.length} messages`);
} else if (prompt) {
this.outputChannel.appendLine(`Query prompt: ${prompt.substring(0, 200)}...`);
} else {
throw new Error('Either prompt or conversationHistory must be provided');
}
this.outputChannel.appendLine(`Query options: ${JSON.stringify(options, null, 2)}`);
this.outputChannel.appendLine(`Streaming enabled: ${!!onMessage}`);
if (!this.isInitialized) {
await this.setupWorkingDirectory();
}
const responseMessages: any[] = [];
const sessionId = `session_${Date.now()}`;
let messageBuffer = '';
// Tool call streaming state
let currentToolCall: any = null;
let toolCallBuffer = '';
try {
this.outputChannel.appendLine('Starting AI SDK streamText...');
// Create execution context for tools
const executionContext: ExecutionContext = {
workingDirectory: this.workingDirectory,
sessionId: sessionId,
outputChannel: this.outputChannel,
abortController: abortController,
};
// Create tools with context
const tools = {
read: createReadTool(executionContext),
write: createWriteTool(executionContext),
edit: createEditTool(executionContext),
multiedit: createMultieditTool(executionContext),
glob: createGlobTool(executionContext),
grep: createGrepTool(executionContext),
ls: createLsTool(executionContext),
bash: createBashTool(executionContext),
generateTheme: createThemeTool(executionContext)
};
// Prepare AI SDK input based on available data
const streamTextConfig: any = {
model: this.getModel(),
system: this.getSystemPrompt(),
tools: tools,
toolCallStreaming: true,
maxSteps: 10, // Enable multi-step reasoning with tools
maxTokens: 8192 // Increase token limit to prevent truncation
};
if (usingConversationHistory) {
// Use conversation messages
streamTextConfig.messages = conversationHistory;
this.outputChannel.appendLine(`Using conversation history with ${conversationHistory!.length} messages`);
// Debug: Log the actual messages being sent to AI SDK
this.outputChannel.appendLine('=== AI SDK MESSAGES DEBUG ===');
conversationHistory!.forEach((msg, index) => {
const content = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content);
this.outputChannel.appendLine(` [${index}] ${msg.role}: "${content.substring(0, 150)}..."`);
});
this.outputChannel.appendLine('=== END AI SDK MESSAGES DEBUG ===');
} else {
// Use single prompt
streamTextConfig.prompt = prompt;
this.outputChannel.appendLine(`Using single prompt: ${prompt!.substring(0, 100)}...`);
}
console.log('========streamTextConfig', streamTextConfig);
const result = streamText(streamTextConfig);
this.outputChannel.appendLine('AI SDK streamText created, starting to process chunks...');
for await (const chunk of result.fullStream) {
// Check for abort signal
if (abortController?.signal.aborted) {
this.outputChannel.appendLine('Operation aborted by user');
throw new Error('Operation cancelled');
}
this.outputChannel.appendLine(`Received chunk type: ${chunk.type}`);
switch (chunk.type) {
case 'text-delta':
// Handle streaming text (assistant message chunks) - CoreMessage format
messageBuffer += chunk.textDelta;
const textMessage: CoreMessage = {
role: 'assistant',
content: chunk.textDelta
};
onMessage?.(textMessage);
responseMessages.push(textMessage);
break;
case 'finish':
// Final result message - CoreMessage format
this.outputChannel.appendLine(`===Stream finished with reason: ${chunk.finishReason}`);
this.outputChannel.appendLine(`${JSON.stringify(chunk)}`);
this.outputChannel.appendLine(`========================================`);
const resultMessage: CoreMessage = {
role: 'assistant',
content: chunk.finishReason === 'stop' ? 'Response completed successfully' : 'Response completed'
};
onMessage?.(resultMessage);
responseMessages.push(resultMessage);
break;
case 'error':
// Error handling - CoreMessage format
const errorMsg = (chunk as any).error?.message || 'Unknown error occurred';
this.outputChannel.appendLine(`Stream error: ${errorMsg}`);
const errorMessage: CoreMessage = {
role: 'assistant',
content: `Error: ${errorMsg}`
};
onMessage?.(errorMessage);
responseMessages.push(errorMessage);
break;
case 'tool-call-streaming-start':
// Tool call streaming started - CoreAssistantMessage format
const streamStart = chunk as any;
currentToolCall = {
toolCallId: streamStart.toolCallId,
toolName: streamStart.toolName,
args: {}
};
toolCallBuffer = '';
this.outputChannel.appendLine(`Tool call streaming started: ${streamStart.toolName} (ID: ${streamStart.toolCallId})`);
// Send initial tool call message in CoreAssistantMessage format
const toolCallStartMessage: CoreMessage = {
role: 'assistant',
content: [{
type: 'tool-call',
toolCallId: streamStart.toolCallId,
toolName: streamStart.toolName,
args: {} // Empty initially, will be updated with deltas
}]
};
onMessage?.(toolCallStartMessage);
responseMessages.push(toolCallStartMessage);
break;
case 'tool-call-delta':
// Streaming tool call parameters - update existing message
const delta = chunk as any;
if (currentToolCall && delta.argsTextDelta) {
toolCallBuffer += delta.argsTextDelta;
// Try to parse current buffer as JSON and send update
try {
const parsedArgs = JSON.parse(toolCallBuffer);
// Send UPDATE signal (not new message) with special marker
const updateMessage: CoreMessage & { _isUpdate?: boolean, _updateToolId?: string } = {
role: 'assistant',
content: [{
type: 'tool-call',
toolCallId: currentToolCall.toolCallId,
toolName: currentToolCall.toolName,
args: parsedArgs
}],
_isUpdate: true,
_updateToolId: currentToolCall.toolCallId
};
onMessage?.(updateMessage);
} catch (parseError) {
// JSON not complete yet, continue buffering
if (toolCallBuffer.length % 100 === 0) {
this.outputChannel.appendLine(`Tool call progress: ${toolCallBuffer.length} characters received (parsing...)`);
}
}
}
break;
case 'tool-call':
// Handle final complete tool call - CoreAssistantMessage format
const toolCall = chunk as any;
this.outputChannel.appendLine(`=====Tool call complete: ${JSON.stringify(toolCall)}`);
this.outputChannel.appendLine(`========================================`);
// Skip sending duplicate tool call message if we already sent streaming start
if (!currentToolCall) {
// Only send if we didn't already send a streaming start message
const toolCallMessage: CoreMessage = {
role: 'assistant',
content: [{
type: 'tool-call',
toolCallId: toolCall.toolCallId,
toolName: toolCall.toolName,
args: toolCall.args
}]
};
onMessage?.(toolCallMessage);
responseMessages.push(toolCallMessage);
} else {
this.outputChannel.appendLine(`Skipping duplicate tool call message - already sent streaming start for ID: ${toolCall.toolCallId}`);
}
// Reset tool call streaming state
currentToolCall = null;
toolCallBuffer = '';
break;
case 'step-start':
// Log step start with details
const stepStart = chunk as any;
this.outputChannel.appendLine(`====Step ${stepStart.step || 'unknown'} started: ${stepStart.stepType || 'reasoning'}`);
this.outputChannel.appendLine(`${JSON.stringify(chunk)}`);
this.outputChannel.appendLine(`========================================`);
break;
case 'step-finish':
// Log step completion with details
const stepFinish = chunk as any;
this.outputChannel.appendLine(`====Step ${stepFinish.step || 'unknown'} finished: ${stepFinish.stepType || 'reasoning'} (${stepFinish.finishReason || 'completed'})`);
this.outputChannel.appendLine(`${JSON.stringify(chunk)}`);
this.outputChannel.appendLine(`========================================`);
break;
default:
// Handle tool results and other unknown chunk types
if ((chunk as any).type === 'tool-result') {
const toolResult = chunk as any;
this.outputChannel.appendLine(`Tool result received for ID: ${toolResult.toolCallId}: ${JSON.stringify(toolResult.result).substring(0, 200)}...`);
// Send tool result in CoreToolMessage format
const toolResultMessage: CoreMessage = {
role: 'tool',
content: [{
type: 'tool-result',
toolCallId: toolResult.toolCallId,
toolName: toolResult.toolName,
result: toolResult.result,
isError: toolResult.isError || false
}]
};
onMessage?.(toolResultMessage);
responseMessages.push(toolResultMessage);
} else {
this.outputChannel.appendLine(`Unknown chunk type: ${chunk.type}`);
}
break;
}
}
this.outputChannel.appendLine(`Query completed successfully. Total messages: ${responseMessages.length}`);
this.outputChannel.appendLine(`Complete response: "${messageBuffer}"`);
return responseMessages;
} catch (error) {
this.outputChannel.appendLine(`Custom Agent query failed: ${error}`);
this.outputChannel.appendLine(`Error stack: ${error instanceof Error ? error.stack : 'No stack trace'}`);
// Send error message if streaming callback is available
if (onMessage) {
const errorMessage = {
type: 'result',
subtype: 'error',
result: error instanceof Error ? error.message : String(error),
session_id: sessionId,
is_error: true
};
onMessage(errorMessage);
}
throw error;
}
}
get isReady(): boolean {
return this.isInitialized;
}
async waitForInitialization(): Promise<boolean> {
if (!this.isInitialized) {
await this.setupWorkingDirectory();
}
return this.isInitialized;
}
getWorkingDirectory(): string {
return this.workingDirectory;
}
hasApiKey(): boolean {
const config = vscode.workspace.getConfiguration('superdesign');
const specificModel = config.get<string>('aiModel');
const provider = config.get<string>('aiModelProvider', 'anthropic');
// Determine provider from model name if specific model is set
let effectiveProvider = provider;
if (specificModel) {
if (specificModel.includes('/')) {
effectiveProvider = 'openrouter';
} else if (specificModel.startsWith('claude-')) {
effectiveProvider = 'anthropic';
} else {
effectiveProvider = 'openai';
}
}
switch (effectiveProvider) {
case 'openrouter':
return !!config.get<string>('openrouterApiKey');
case 'anthropic':
return !!config.get<string>('anthropicApiKey');
case 'openai':
default:
return !!config.get<string>('openaiApiKey');
}
}
isApiKeyAuthError(errorMessage: string): boolean {
if (!errorMessage) {
return false;
}
const lowerError = errorMessage.toLowerCase();
return lowerError.includes('api key') ||
lowerError.includes('authentication') ||
lowerError.includes('unauthorized') ||
lowerError.includes('invalid_api_key') ||
lowerError.includes('permission_denied') ||
lowerError.includes('api_key_invalid') ||
lowerError.includes('unauthenticated');
}
}

80
src/services/logger.ts Normal file
View File

@@ -0,0 +1,80 @@
import * as vscode from 'vscode';
export enum LogLevel {
DEBUG,
INFO,
WARN,
ERROR,
}
export class Logger {
private static outputChannel: vscode.OutputChannel;
private static currentLevel: LogLevel = LogLevel.INFO;
public static initialize() {
if (!this.outputChannel) {
this.outputChannel = vscode.window.createOutputChannel('Superdesign');
}
}
public static setLevel(level: LogLevel) {
this.currentLevel = level;
}
private static log(level: LogLevel, label: string, message: string, showNotification: boolean = false) {
if (level < this.currentLevel) {
return;
}
this.initialize();
const timestamp = new Date().toISOString();
this.outputChannel.appendLine(`[${timestamp}] [${label}] ${message}`);
if (showNotification) {
switch (level) {
case LogLevel.ERROR:
vscode.window.showErrorMessage(message);
break;
case LogLevel.WARN:
vscode.window.showWarningMessage(message);
break;
case LogLevel.INFO:
vscode.window.showInformationMessage(message);
break;
default:
// No notification for debug
break;
}
}
}
public static debug(message: string, showNotification: boolean = false) {
this.log(LogLevel.DEBUG, 'DEBUG', message, showNotification);
}
public static info(message: string, showNotification: boolean = false) {
this.log(LogLevel.INFO, 'INFO', message, showNotification);
}
public static warn(message: string, showNotification: boolean = false) {
this.log(LogLevel.WARN, 'WARN', message, showNotification);
}
public static error(message: string, showNotification: boolean = false) {
this.log(LogLevel.ERROR, 'ERROR', message, showNotification);
}
public static dispose() {
if (this.outputChannel) {
this.outputChannel.dispose();
}
}
/**
* Get the output channel for direct access if needed
*/
public static getOutputChannel(): vscode.OutputChannel {
this.initialize();
return this.outputChannel;
}
}

View File

@@ -0,0 +1,81 @@
import * as vscode from 'vscode';
import { WebviewContext } from '../types/context';
export function generateWebviewHtml(
webview: vscode.Webview,
extensionUri: vscode.Uri,
context: WebviewContext
): string {
const scriptUri = webview.asWebviewUri(
vscode.Uri.joinPath(extensionUri, 'dist', 'webview.js')
);
// Generate webview URIs for logo images
const logoUris = {
cursor: webview.asWebviewUri(vscode.Uri.joinPath(extensionUri, 'src', 'assets', 'cursor_logo.png')).toString(),
windsurf: webview.asWebviewUri(vscode.Uri.joinPath(extensionUri, 'src', 'assets', 'windsurf_logo.png')).toString(),
claudeCode: webview.asWebviewUri(vscode.Uri.joinPath(extensionUri, 'src', 'assets', 'claude_code_logo.png')).toString(),
lovable: webview.asWebviewUri(vscode.Uri.joinPath(extensionUri, 'src', 'assets', 'lovable_logo.png')).toString(),
bolt: webview.asWebviewUri(vscode.Uri.joinPath(extensionUri, 'src', 'assets', 'bolt_logo.jpg')).toString(),
};
// Debug logging
console.log('Extension URI:', extensionUri.toString());
console.log('Generated logo URIs:', logoUris);
// Check if files exist
const fs = require('fs');
const path = require('path');
Object.entries(logoUris).forEach(([name, uri]) => {
const filePath = path.join(extensionUri.fsPath, 'src', 'assets', name === 'bolt' ? 'bolt_logo.jpg' : `${name === 'claudeCode' ? 'claude_code' : name}_logo.png`);
const exists = fs.existsSync(filePath);
console.log(`${name} logo exists at ${filePath}:`, exists);
});
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource} 'unsafe-inline'; img-src ${webview.cspSource} data: https: vscode-webview:; script-src ${webview.cspSource} 'unsafe-inline'; font-src ${webview.cspSource};">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Superdesign Chat</title>
<style>
body {
font-family: var(--vscode-font-family);
font-size: var(--vscode-font-size);
font-weight: var(--vscode-font-weight);
color: ${context.layout === 'sidebar' ? 'var(--vscode-sideBar-foreground)' : 'var(--vscode-panel-foreground)'};
background-color: ${context.layout === 'sidebar' ? 'var(--vscode-sideBar-background)' : 'var(--vscode-panel-background)'};
border-right: ${context.layout === 'sidebar' ? '1px solid var(--vscode-sideBar-border)' : '1px solid var(--vscode-panel-border)'};
margin: 0;
padding: ${context.layout === 'sidebar' ? '8px' : '16px'};
height: 100vh;
overflow: hidden;
box-sizing: border-box;
}
</style>
</head>
<body>
<div id="root"></div>
<script>
// Debug: Check if context data is being generated
console.log('About to set webview context. Context object:', ${JSON.stringify({ ...context, logoUris })});
// Initialize context for React app
window.__WEBVIEW_CONTEXT__ = ${JSON.stringify({ ...context, logoUris })};
// Debug logging in webview
console.log('Webview context set:', window.__WEBVIEW_CONTEXT__);
console.log('Logo URIs received in webview:', window.__WEBVIEW_CONTEXT__?.logoUris);
// Additional debug - check if context persists
setTimeout(() => {
console.log('Context check after 1 second:', window.__WEBVIEW_CONTEXT__);
}, 1000);
</script>
<script src="${scriptUri}"></script>
</body>
</html>`;
}

263
src/tools/bash-tool.ts Normal file
View File

@@ -0,0 +1,263 @@
import { z } from 'zod';
import { tool } from 'ai';
import { spawn, ChildProcess } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { ExecutionContext } from '../types/agent';
import {
handleToolError,
validateWorkspacePath,
resolveWorkspacePath,
createSuccessResponse,
validateDirectoryExists,
ToolResponse
} from './tool-utils';
const bashParametersSchema = z.object({
command: z.string().describe('Shell command to execute (e.g., "npm install", "ls -la", "git status")'),
description: z.string().optional().describe('Brief description of what the command does for logging purposes'),
directory: z.string().optional().describe('Directory to run command in (relative to workspace root). Defaults to workspace root.'),
timeout: z.number().optional().describe('Timeout in milliseconds (default: 30000ms = 30 seconds)'),
capture_output: z.boolean().optional().describe('Whether to capture and return command output (default: true)'),
env: z.record(z.string()).optional().describe('Environment variables to set for the command execution')
});
interface CommandResult {
command: string;
directory: string;
stdout: string;
stderr: string;
exitCode: number | null;
signal: string | null;
duration: number;
timedOut: boolean;
processId?: number;
}
/**
* Check for potentially unsafe commands
*/
function hasUnsafeCommand(command: string): boolean {
const unsafePatterns = [
// System modification
/\brm\s+(-[rf]*\s+)?\/\s*$/i,
/\brm\s+-[rf]*\s+\/$/i,
/\b(format|fdisk|mkfs)\b/i,
// Network operations that could be dangerous
/\b(curl|wget)\s+.*\|\s*(bash|sh|python|ruby|perl)/i,
// Process manipulation
/\b(kill|killall|pkill)\s+(-9\s+)?1\b/i,
// System shutdown/reboot
/\b(shutdown|reboot|halt|init\s+0)\b/i,
// Privilege escalation
/\b(sudo\s+su|sudo.*passwd|chmod\s+777)/i,
// Directory traversal attempts
/\.\.(\/|\\)/,
// Dangerous redirections
/>\s*(\/dev\/|\/proc\/|\/sys\/)/i,
];
return unsafePatterns.some(pattern => pattern.test(command));
}
// Path validation is now handled by validateWorkspacePath in tool-utils
/**
* Execute command with proper process management
*/
async function executeCommand(
command: string,
options: {
cwd: string;
env: NodeJS.ProcessEnv;
timeout: number;
captureOutput: boolean;
}
): Promise<CommandResult> {
const startTime = Date.now();
const isWindows = os.platform() === 'win32';
// Choose shell based on platform
const shell = isWindows ? 'cmd.exe' : 'bash';
const shellArgs = isWindows ? ['/c', command] : ['-c', command];
const child: ChildProcess = spawn(shell, shellArgs, {
cwd: options.cwd,
env: options.env,
stdio: options.captureOutput ? ['ignore', 'pipe', 'pipe'] : 'inherit',
detached: !isWindows, // Create process group on Unix systems
});
let stdout = '';
let stderr = '';
let timedOut = false;
// Capture output if requested
if (options.captureOutput && child.stdout && child.stderr) {
child.stdout.on('data', (data: Buffer) => {
stdout += data.toString();
});
child.stderr.on('data', (data: Buffer) => {
stderr += data.toString();
});
}
// Set up timeout
const timeoutHandle = setTimeout(() => {
timedOut = true;
if (child.pid) {
try {
if (isWindows) {
// On Windows, use taskkill to terminate process tree
spawn('taskkill', ['/pid', child.pid.toString(), '/f', '/t']);
} else {
// On Unix, kill the process group
process.kill(-child.pid, 'SIGTERM');
// Force kill after 1 second if still running
setTimeout(() => {
if (child.pid && !child.killed) {
try {
process.kill(-child.pid, 'SIGKILL');
} catch (e) {
// Process might already be dead
}
}
}, 1000);
}
} catch (error) {
// Process might already be dead
}
}
}, options.timeout);
// Wait for process to complete
const exitPromise = new Promise<{ code: number | null; signal: string | null }>((resolve) => {
child.on('exit', (code, signal) => {
clearTimeout(timeoutHandle);
resolve({ code, signal });
});
});
const { code, signal } = await exitPromise;
const duration = Date.now() - startTime;
return {
command,
directory: path.relative(options.cwd, options.cwd) || '.',
stdout: stdout.trim(),
stderr: stderr.trim(),
exitCode: code,
signal: signal,
duration,
timedOut,
processId: child.pid
};
}
export function createBashTool(context: ExecutionContext) {
return tool({
description: 'Execute shell/bash commands within the SuperDesign workspace. Supports timeouts, output capture, and secure execution.',
parameters: bashParametersSchema,
execute: async (params): Promise<ToolResponse> => {
try {
const { command, description, directory, timeout = 30000, capture_output = true, env } = params;
// Security checks
if (hasUnsafeCommand(command)) {
return handleToolError('Command contains potentially unsafe operations', 'Security check', 'security');
}
// Resolve execution directory
const workingDir = directory || '.';
// Security check for workspace boundary
const pathError = validateWorkspacePath(workingDir, context);
if (pathError) {
return pathError;
}
const absolutePath = resolveWorkspacePath(workingDir, context);
// Check if directory exists
const dirError = validateDirectoryExists(absolutePath, workingDir);
if (dirError) {
return dirError;
}
console.log(`Executing command: ${command}${description ? ` (${description})` : ''}`);
console.log(`Working directory: ${workingDir}`);
// Prepare environment
const processEnv = {
...process.env,
...env
};
// Execute the command
const result = await executeCommand(command, {
cwd: absolutePath,
env: processEnv,
timeout,
captureOutput: capture_output
});
// Log results
if (result.timedOut) {
console.log(`Command timed out after ${timeout}ms`);
} else if (result.exitCode === 0) {
console.log(`Command completed successfully in ${result.duration}ms`);
} else {
console.log(`Command failed with exit code ${result.exitCode} in ${result.duration}ms`);
}
// Create summary for display
let summary = `Command: ${command}\n`;
summary += `Directory: ${result.directory}\n`;
summary += `Exit Code: ${result.exitCode}\n`;
summary += `Duration: ${result.duration}ms\n`;
if (result.timedOut) {
summary += `Status: TIMED OUT (${timeout}ms)\n`;
} else if (result.signal) {
summary += `Signal: ${result.signal}\n`;
}
if (capture_output) {
if (result.stdout) {
summary += `\nStdout:\n${result.stdout}\n`;
}
if (result.stderr) {
summary += `\nStderr:\n${result.stderr}\n`;
}
}
if (result.timedOut) {
return handleToolError(`Command timed out after ${timeout}ms`, 'Command execution', 'execution');
}
if (result.exitCode !== 0) {
return handleToolError(
`Command failed with exit code ${result.exitCode}${result.stderr ? `\nStderr: ${result.stderr}` : ''}`,
'Command execution',
'execution'
);
}
return createSuccessResponse({
command,
directory: workingDir,
exitCode: result.exitCode,
duration: result.duration,
stdout: result.stdout,
stderr: result.stderr,
summary
});
} catch (error) {
return handleToolError(error, 'Bash tool execution', 'execution');
}
}
});
}

197
src/tools/edit-tool.ts Normal file
View File

@@ -0,0 +1,197 @@
import { z } from 'zod';
import { tool } from 'ai';
import * as fs from 'fs';
import * as path from 'path';
import { ExecutionContext } from '../types/agent';
import {
handleToolError,
validateWorkspacePath,
resolveWorkspacePath,
createSuccessResponse,
validateFileExists,
ToolResponse
} from './tool-utils';
const editParametersSchema = z.object({
file_path: z.string().describe('Path to the file to edit (relative to workspace root, or absolute path within workspace)'),
old_string: z.string().describe('The exact text to find and replace. Must match exactly including whitespace, indentation, and context. For single replacements, include 3+ lines of context before and after the target text.'),
new_string: z.string().describe('The text to replace old_string with. Should maintain proper indentation and formatting.'),
expected_replacements: z.number().min(1).optional().describe('Number of replacements expected (default: 1). Use when replacing multiple occurrences.')
});
interface CalculatedEdit {
currentContent: string;
newContent: string;
occurrences: number;
isNewFile: boolean;
error?: string;
}
// Path validation is now handled by validateWorkspacePath in tool-utils
/**
* Escape special regex characters
*/
function escapeRegExp(string: string): string {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
/**
* Calculate the edit operation without executing it
*/
function calculateEdit(
file_path: string,
old_string: string,
new_string: string,
expected_replacements: number,
context: ExecutionContext
): CalculatedEdit {
// Use the utility function to resolve paths
const absolutePath = resolveWorkspacePath(file_path, context);
// Check if file exists
if (!fs.existsSync(absolutePath)) {
if (old_string === '') {
// Creating a new file
return {
currentContent: '',
newContent: new_string,
occurrences: 1,
isNewFile: true
};
} else {
return {
currentContent: '',
newContent: '',
occurrences: 0,
isNewFile: false,
error: `File not found: ${file_path}. Cannot apply edit. Use empty old_string to create a new file.`
};
}
}
// Read current content
let currentContent: string;
try {
currentContent = fs.readFileSync(absolutePath, 'utf8');
// Normalize line endings to LF
currentContent = currentContent.replace(/\r\n/g, '\n');
} catch (error) {
return {
currentContent: '',
newContent: '',
occurrences: 0,
isNewFile: false,
error: `Failed to read file: ${error instanceof Error ? error.message : String(error)}`
};
}
// Handle creating file that already exists
if (old_string === '') {
return {
currentContent,
newContent: '',
occurrences: 0,
isNewFile: false,
error: `File already exists, cannot create: ${file_path}`
};
}
// Count occurrences
const occurrences = (currentContent.match(new RegExp(escapeRegExp(old_string), 'g')) || []).length;
// Validate occurrence count
if (occurrences === 0) {
return {
currentContent,
newContent: currentContent,
occurrences: 0,
isNewFile: false,
error: `Text not found in file. 0 occurrences of old_string found. Ensure exact text match including whitespace and indentation.`
};
}
if (occurrences !== expected_replacements) {
return {
currentContent,
newContent: currentContent,
occurrences,
isNewFile: false,
error: `Expected ${expected_replacements} replacement(s) but found ${occurrences} occurrence(s).`
};
}
// Apply replacement
const newContent = currentContent.split(old_string).join(new_string);
return {
currentContent,
newContent,
occurrences,
isNewFile: false
};
}
export function createEditTool(context: ExecutionContext) {
return tool({
description: 'Replace text within a file using exact string matching. Accepts both relative and absolute file paths within the workspace.',
parameters: editParametersSchema,
execute: async (params): Promise<ToolResponse> => {
try {
const { file_path, old_string, new_string, expected_replacements = 1 } = params;
// Validate workspace path (handles both absolute and relative paths)
const pathError = validateWorkspacePath(file_path, context);
if (pathError) {
return pathError;
}
console.log(`Editing file: ${file_path}`);
// Calculate the edit
const editResult = calculateEdit(file_path, old_string, new_string, expected_replacements, context);
if (editResult.error) {
return handleToolError(editResult.error, 'Edit operation', 'execution');
}
const absolutePath = resolveWorkspacePath(file_path, context);
// Create parent directories if needed (for new files)
if (editResult.isNewFile) {
const dirName = path.dirname(absolutePath);
if (!fs.existsSync(dirName)) {
fs.mkdirSync(dirName, { recursive: true });
console.log(`Created parent directories for: ${file_path}`);
}
}
// Write the updated content
fs.writeFileSync(absolutePath, editResult.newContent, 'utf8');
const newLines = editResult.newContent.split('\n').length;
const newSize = Buffer.byteLength(editResult.newContent, 'utf8');
if (editResult.isNewFile) {
console.log(`Created new file: ${file_path} (${newLines} lines)`);
} else {
console.log(`Applied ${editResult.occurrences} replacement(s) to: ${file_path} (${newLines} lines)`);
}
return createSuccessResponse({
file_path,
absolute_path: absolutePath,
is_new_file: editResult.isNewFile,
replacements_made: editResult.occurrences,
lines_total: newLines,
bytes_total: newSize,
old_string_length: old_string.length,
new_string_length: new_string.length
});
} catch (error) {
return handleToolError(error, 'Edit tool execution', 'execution');
}
}
});
}

294
src/tools/glob-tool.ts Normal file
View File

@@ -0,0 +1,294 @@
import { z } from 'zod';
import { tool } from 'ai';
import * as fs from 'fs';
import * as path from 'path';
import { ExecutionContext } from '../types/agent';
import {
handleToolError,
validateWorkspacePath,
resolveWorkspacePath,
createSuccessResponse,
validateDirectoryExists,
ToolResponse
} from './tool-utils';
const globParametersSchema = z.object({
pattern: z.string().describe('Glob pattern to match (e.g., "*.js", "src/**/*.ts", "**/*.{js,ts}")'),
path: z.string().optional().describe('Directory to search in (relative to workspace root, or absolute path within workspace). Defaults to workspace root.'),
case_sensitive: z.boolean().optional().describe('Whether the search should be case-sensitive (default: false)'),
include_dirs: z.boolean().optional().describe('Whether to include directories in results (default: false)'),
show_hidden: z.boolean().optional().describe('Whether to include hidden files/directories (starting with .)'),
max_results: z.number().min(1).optional().describe('Maximum number of results to return (default: 500)'),
sort_by_time: z.boolean().optional().describe('Whether to sort results by modification time, newest first (default: false)')
});
interface GlobFileEntry {
path: string;
absolutePath: string;
isDirectory: boolean;
size: number;
modifiedTime: Date;
extension?: string;
}
// Path validation is now handled by validateWorkspacePath in tool-utils
/**
* Convert glob pattern to regex pattern
*/
function globToRegex(pattern: string, caseSensitive: boolean = false): RegExp {
// Handle special cases for braces {js,ts}
let regexPattern = pattern;
// Handle brace expansion like {js,ts,jsx}
const braceRegex = /\{([^}]+)\}/g;
regexPattern = regexPattern.replace(braceRegex, (match, content) => {
const options = content.split(',').map((s: string) => s.trim());
return `(${options.join('|')})`;
});
// Escape regex special characters except glob chars
regexPattern = regexPattern
.replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape special regex chars except *, ?, and already handled {}
.replace(/\\\{/g, '{') // Restore { that we want to keep
.replace(/\\\}/g, '}') // Restore } that we want to keep
.replace(/\\\|/g, '|') // Restore | that we want to keep
.replace(/\\\(/g, '(') // Restore ( that we want to keep
.replace(/\\\)/g, ')'); // Restore ) that we want to keep
// Handle glob patterns
regexPattern = regexPattern
.replace(/\*\*/g, '###DOUBLESTAR###') // Temporarily replace **
.replace(/\*/g, '[^/]*') // * becomes [^/]* (match any chars except path separator)
.replace(/###DOUBLESTAR###/g, '.*') // ** becomes .* (match any chars including path separator)
.replace(/\?/g, '[^/]'); // ? becomes [^/] (match single char except path separator)
const flags = caseSensitive ? '' : 'i';
return new RegExp(`^${regexPattern}$`, flags);
}
/**
* Check if a file should be skipped based on common patterns
*/
function shouldSkipPath(relativePath: string, showHidden: boolean): boolean {
// Skip hidden files unless requested
if (!showHidden && relativePath.split('/').some(part => part.startsWith('.'))) {
return true;
}
// Skip common directories that should never be searched
const skipPatterns = [
/node_modules/,
/\.git$/,
/\.svn$/,
/\.hg$/,
/\.vscode$/,
/dist$/,
/build$/,
/coverage$/,
/\.nyc_output$/,
/\.next$/,
/\.cache$/
];
return skipPatterns.some(pattern => pattern.test(relativePath));
}
/**
* Recursively find files matching the pattern
*/
async function findMatches(
searchDir: string,
pattern: RegExp,
options: {
includeDirs: boolean;
showHidden: boolean;
maxResults: number;
}
): Promise<GlobFileEntry[]> {
const results: GlobFileEntry[] = [];
const scanDirectory = async (currentDir: string): Promise<void> => {
if (results.length >= options.maxResults) {
return;
}
try {
const entries = await fs.promises.readdir(currentDir, { withFileTypes: true });
for (const entry of entries) {
if (results.length >= options.maxResults) {
break;
}
const fullPath = path.join(currentDir, entry.name);
const relativePath = path.relative(searchDir, fullPath);
// Skip paths that should be ignored
if (shouldSkipPath(relativePath, options.showHidden)) {
continue;
}
const isDirectory = entry.isDirectory();
// Check if this path matches the pattern
const matches = pattern.test(relativePath);
if (matches && (options.includeDirs || !isDirectory)) {
try {
const stats = await fs.promises.stat(fullPath);
results.push({
path: relativePath,
absolutePath: fullPath,
isDirectory,
size: isDirectory ? 0 : stats.size,
modifiedTime: stats.mtime,
extension: isDirectory ? undefined : path.extname(entry.name).slice(1)
});
} catch (error) {
// Ignore stat errors and continue
}
}
// Recursively scan subdirectories
if (isDirectory) {
await scanDirectory(fullPath);
}
}
} catch (error) {
// Ignore permission errors and continue
}
};
await scanDirectory(searchDir);
return results;
}
/**
* Sort results by modification time (newest first) or alphabetically
*/
function sortResults(results: GlobFileEntry[], sortByTime: boolean): GlobFileEntry[] {
if (!sortByTime) {
// Sort alphabetically with directories first
return results.sort((a, b) => {
if (a.isDirectory && !b.isDirectory) {
return -1;
}
if (!a.isDirectory && b.isDirectory) {
return 1;
}
return a.path.localeCompare(b.path);
});
}
// Sort by modification time (newest first) with recent files prioritized
const oneDayAgo = new Date().getTime() - (24 * 60 * 60 * 1000);
return results.sort((a, b) => {
const aTime = a.modifiedTime.getTime();
const bTime = b.modifiedTime.getTime();
const aIsRecent = aTime > oneDayAgo;
const bIsRecent = bTime > oneDayAgo;
// Both recent: newest first
if (aIsRecent && bIsRecent) {
return bTime - aTime;
}
// One recent: recent first
if (aIsRecent) {
return -1;
}
if (bIsRecent) {
return 1;
}
// Both old: alphabetical
return a.path.localeCompare(b.path);
});
}
export function createGlobTool(context: ExecutionContext) {
return tool({
description: 'Find files and directories matching glob patterns (e.g., "*.js", "src/**/*.ts"). Efficient for locating files by name or path structure.',
parameters: globParametersSchema,
execute: async (params): Promise<ToolResponse> => {
try {
const {
pattern,
path: searchPath = '.',
case_sensitive = false,
include_dirs = false,
show_hidden = false,
max_results = 500,
sort_by_time = false
} = params;
// Validate workspace path (handles both absolute and relative paths)
const pathError = validateWorkspacePath(searchPath, context);
if (pathError) {
return pathError;
}
// Resolve search directory
const absolutePath = resolveWorkspacePath(searchPath, context);
// Check if path exists and is a directory
const dirError = validateDirectoryExists(absolutePath, searchPath);
if (dirError) {
return dirError;
}
console.log(`Finding files matching pattern "${pattern}" in ${searchPath}`);
// Convert glob pattern to regex
const regex = globToRegex(pattern, case_sensitive);
// Find matching files
const matches = await findMatches(absolutePath, regex, {
includeDirs: include_dirs,
showHidden: show_hidden,
maxResults: max_results
});
// Sort results
const sortedMatches = sortResults(matches, sort_by_time);
// Create summary
const fileCount = sortedMatches.filter(m => !m.isDirectory).length;
const dirCount = sortedMatches.filter(m => m.isDirectory).length;
let summary = `Found ${sortedMatches.length} match(es) for pattern "${pattern}"`;
if (fileCount > 0 && dirCount > 0) {
summary += ` (${fileCount} files, ${dirCount} directories)`;
} else if (fileCount > 0) {
summary += ` (${fileCount} files)`;
} else if (dirCount > 0) {
summary += ` (${dirCount} directories)`;
}
if (sortedMatches.length >= max_results) {
summary += ` - results truncated at ${max_results}`;
}
console.log(summary);
return createSuccessResponse({
pattern,
search_path: searchPath,
matches: sortedMatches,
total_matches: sortedMatches.length,
file_count: fileCount,
directory_count: dirCount,
summary,
truncated: sortedMatches.length >= max_results,
sorted_by_time: sort_by_time
});
} catch (error) {
return handleToolError(error, 'Glob tool execution', 'execution');
}
}
});
}

315
src/tools/grep-tool.ts Normal file
View File

@@ -0,0 +1,315 @@
import { z } from 'zod';
import { tool } from 'ai';
import * as fs from 'fs';
import * as path from 'path';
import { ExecutionContext } from '../types/agent';
import {
handleToolError,
validateWorkspacePath,
resolveWorkspacePath,
createSuccessResponse,
validateDirectoryExists,
ToolResponse
} from './tool-utils';
const grepParametersSchema = z.object({
pattern: z.string().describe('Regular expression pattern to search for (e.g., "function\\s+\\w+", "import.*from")'),
path: z.string().optional().describe('Directory to search in (relative to workspace root, or absolute path within workspace). Defaults to workspace root.'),
include: z.string().optional().describe('File pattern to include (e.g., "*.js", "*.{ts,tsx}", "src/**/*.ts")'),
case_sensitive: z.boolean().optional().describe('Whether the search should be case-sensitive (default: false)'),
max_files: z.number().min(1).optional().describe('Maximum number of files to search (default: 1000)'),
max_matches: z.number().min(1).optional().describe('Maximum number of matches to return (default: 100)')
});
interface GrepMatch {
filePath: string;
lineNumber: number;
line: string;
matchStart: number;
matchEnd: number;
}
// Path validation is now handled by validateWorkspacePath in tool-utils
/**
* Check if a file path matches the include pattern
*/
function matchesIncludePattern(filePath: string, includePattern?: string): boolean {
if (!includePattern) {
return true;
}
// Convert glob pattern to regex (simplified)
const regexPattern = includePattern
.replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape special regex chars
.replace(/\*\*/g, '###DOUBLESTAR###') // Temporarily replace **
.replace(/\*/g, '[^/]*') // * becomes [^/]* (no directory separators)
.replace(/###DOUBLESTAR###/g, '.*') // ** becomes .* (any characters)
.replace(/\?/g, '[^/]'); // ? becomes [^/] (single char, no dir sep)
const regex = new RegExp(`^${regexPattern}$`);
return regex.test(filePath);
}
/**
* Check if a file should be skipped based on common patterns
*/
function shouldSkipFile(filePath: string): boolean {
const skipPatterns = [
/node_modules/,
/\.git/,
/\.vscode/,
/dist/,
/build/,
/coverage/,
/\.nyc_output/,
/\.next/,
/\.cache/,
/\.DS_Store/,
/Thumbs\.db/,
/\.log$/,
/\.tmp$/,
/\.temp$/
];
return skipPatterns.some(pattern => pattern.test(filePath));
}
/**
* Simple check if file is likely a text file
*/
function isTextFile(filePath: string): boolean {
const textExtensions = [
'.js', '.ts', '.jsx', '.tsx', '.json', '.html', '.htm', '.css', '.scss', '.sass',
'.py', '.java', '.cpp', '.c', '.h', '.hpp', '.cs', '.php', '.rb', '.go',
'.rs', '.swift', '.kt', '.scala', '.clj', '.hs', '.elm', '.ml', '.f',
'.txt', '.md', '.rst', '.asciidoc', '.xml', '.yaml', '.yml', '.toml',
'.ini', '.cfg', '.conf', '.properties', '.env', '.gitignore', '.gitattributes',
'.dockerfile', '.makefile', '.sh', '.bat', '.ps1', '.sql', '.graphql',
'.vue', '.svelte', '.astro', '.prisma', '.proto'
];
const ext = path.extname(filePath).toLowerCase();
return textExtensions.includes(ext) || !ext; // Include extensionless files
}
/**
* Recursively find files to search
*/
async function findFilesToSearch(
dirPath: string,
includePattern?: string,
maxFiles: number = 1000
): Promise<string[]> {
const files: string[] = [];
const scanDirectory = async (currentPath: string): Promise<void> => {
if (files.length >= maxFiles) {
return;
}
try {
const entries = await fs.promises.readdir(currentPath, { withFileTypes: true });
for (const entry of entries) {
if (files.length >= maxFiles) {
break;
}
const fullPath = path.join(currentPath, entry.name);
const relativePath = path.relative(dirPath, fullPath);
// Skip common directories and files
if (shouldSkipFile(relativePath)) {
continue;
}
if (entry.isDirectory()) {
await scanDirectory(fullPath);
} else if (entry.isFile()) {
// Check if file matches include pattern
if (matchesIncludePattern(relativePath, includePattern)) {
// Only include text files (basic check)
if (isTextFile(fullPath)) {
files.push(fullPath);
}
}
}
}
} catch (error) {
// Ignore permission errors and continue
}
};
await scanDirectory(dirPath);
return files;
}
/**
* Search for pattern in a single file
*/
async function searchInFile(filePath: string, regex: RegExp, maxMatches: number): Promise<GrepMatch[]> {
const matches: GrepMatch[] = [];
try {
const content = await fs.promises.readFile(filePath, 'utf8');
const lines = content.split(/\r?\n/);
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
if (matches.length >= maxMatches) {
break;
}
const line = lines[lineIndex];
let match;
regex.lastIndex = 0; // Reset regex state
while ((match = regex.exec(line)) !== null) {
matches.push({
filePath,
lineNumber: lineIndex + 1,
line: line,
matchStart: match.index,
matchEnd: match.index + match[0].length
});
if (matches.length >= maxMatches) {
break;
}
// Prevent infinite loop on zero-length matches
if (match.index === regex.lastIndex) {
regex.lastIndex++;
}
}
}
} catch (error) {
// Ignore files that can't be read (binary files, permission issues, etc.)
}
return matches;
}
export function createGrepTool(context: ExecutionContext) {
return tool({
description: 'Search for text patterns within file contents using regular expressions. Can filter by file types and paths.',
parameters: grepParametersSchema,
execute: async (params): Promise<ToolResponse> => {
try {
const {
pattern,
path: searchPath = '.',
include,
case_sensitive = false,
max_files = 1000,
max_matches = 100
} = params;
// Pattern validation (test if it's a valid regex)
try {
new RegExp(pattern);
} catch (error) {
return handleToolError(
`Invalid regular expression pattern: ${error instanceof Error ? error.message : String(error)}`,
'Pattern validation',
'validation'
);
}
// Validate workspace path (handles both absolute and relative paths)
const pathError = validateWorkspacePath(searchPath, context);
if (pathError) {
return pathError;
}
// Resolve search directory
const absolutePath = resolveWorkspacePath(searchPath, context);
// Check if path exists and is a directory
const dirError = validateDirectoryExists(absolutePath, searchPath);
if (dirError) {
return dirError;
}
console.log(`Searching for pattern "${pattern}" in ${searchPath}`);
// Create regex pattern
const regexFlags = case_sensitive ? 'g' : 'gi';
const regex = new RegExp(pattern, regexFlags);
// Find files to search
const filesToSearch = await findFilesToSearch(absolutePath, include, max_files);
if (filesToSearch.length === 0) {
const message = `No files found to search in ${searchPath}${include ? ` matching ${include}` : ''}`;
return createSuccessResponse({
pattern,
search_path: searchPath,
include_pattern: include,
files_searched: 0,
matches: [],
total_matches: 0,
message
});
}
// Search in files
const allMatches: GrepMatch[] = [];
let filesSearched = 0;
let filesWithMatches = 0;
for (const file of filesToSearch) {
if (allMatches.length >= max_matches) {
break;
}
const fileMatches = await searchInFile(file, regex, max_matches - allMatches.length);
if (fileMatches.length > 0) {
// Convert absolute paths to relative paths for output
const relativePath = path.relative(absolutePath, file);
fileMatches.forEach(match => {
match.filePath = relativePath;
});
allMatches.push(...fileMatches);
filesWithMatches++;
}
filesSearched++;
}
// Format results
let summary = `Found ${allMatches.length} match(es) for "${pattern}" in ${filesWithMatches} file(s)`;
if (filesSearched < filesToSearch.length) {
summary += ` (searched ${filesSearched}/${filesToSearch.length} files)`;
}
// Group matches by file for better readability
const matchesByFile: Record<string, GrepMatch[]> = {};
allMatches.forEach(match => {
if (!matchesByFile[match.filePath]) {
matchesByFile[match.filePath] = [];
}
matchesByFile[match.filePath].push(match);
});
console.log(summary);
return createSuccessResponse({
pattern,
search_path: searchPath,
include_pattern: include,
files_searched: filesSearched,
files_with_matches: filesWithMatches,
matches: allMatches,
matches_by_file: matchesByFile,
total_matches: allMatches.length,
summary,
truncated: allMatches.length >= max_matches
});
} catch (error) {
return handleToolError(error, 'Grep tool execution', 'execution');
}
}
});
}

227
src/tools/ls-tool.ts Normal file
View File

@@ -0,0 +1,227 @@
import { z } from 'zod';
import { tool } from 'ai';
import * as fs from 'fs';
import * as path from 'path';
import { ExecutionContext } from '../types/agent';
import {
handleToolError,
validateWorkspacePath,
resolveWorkspacePath,
createSuccessResponse,
validateDirectoryExists,
ToolResponse
} from './tool-utils';
const lsParametersSchema = z.object({
path: z.string().optional().describe('Path to the directory to list (relative to workspace root, or absolute path within workspace). Defaults to workspace root.'),
show_hidden: z.boolean().optional().describe('Whether to show hidden files and directories (starting with .)'),
ignore: z.array(z.string()).optional().describe('Array of glob patterns to ignore (e.g., ["*.log", "temp*"])'),
detailed: z.boolean().optional().describe('Whether to show detailed file information (size, modified time)')
});
interface FileEntry {
name: string;
isDirectory: boolean;
size: number;
modifiedTime: Date;
extension?: string;
}
// Path validation is now handled by validateWorkspacePath in tool-utils
/**
* Check if a filename should be ignored based on patterns
*/
function shouldIgnore(filename: string, patterns?: string[]): boolean {
if (!patterns || patterns.length === 0) {
return false;
}
for (const pattern of patterns) {
// Convert glob pattern to RegExp (simplified version)
const regexPattern = pattern
.replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape special regex chars
.replace(/\*/g, '.*') // * becomes .*
.replace(/\?/g, '.'); // ? becomes .
const regex = new RegExp(`^${regexPattern}$`);
if (regex.test(filename)) {
return true;
}
}
return false;
}
/**
* Format file size in human-readable format
*/
function formatFileSize(bytes: number): string {
if (bytes === 0) {
return '0 B';
}
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
const k = 1024;
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${units[i]}`;
}
/**
* Format modified time in relative format
*/
function formatModifiedTime(date: Date): string {
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffMinutes = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMs / 3600000);
const diffDays = Math.floor(diffMs / 86400000);
if (diffMinutes < 1) {
return 'just now';
}
if (diffMinutes < 60) {
return `${diffMinutes}m ago`;
}
if (diffHours < 24) {
return `${diffHours}h ago`;
}
if (diffDays < 7) {
return `${diffDays}d ago`;
}
return date.toLocaleDateString();
}
export function createLsTool(context: ExecutionContext) {
return tool({
description: 'List the contents of a directory in the SuperDesign workspace. Shows files and subdirectories with optional filtering.',
parameters: lsParametersSchema,
execute: async (params): Promise<ToolResponse> => {
try {
const { path: targetPath = '.', show_hidden = false, ignore, detailed = false } = params;
// Validate workspace path (handles both absolute and relative paths)
const pathError = validateWorkspacePath(targetPath, context);
if (pathError) {
return pathError;
}
// Resolve target directory
const absolutePath = resolveWorkspacePath(targetPath, context);
console.log(`Listing directory: ${targetPath}`);
// Check if path exists and is a directory
const dirError = validateDirectoryExists(absolutePath, targetPath);
if (dirError) {
return dirError;
}
// Read directory contents
const files = fs.readdirSync(absolutePath);
if (files.length === 0) {
console.log(`Directory is empty: ${targetPath}`);
return createSuccessResponse({
path: targetPath,
absolute_path: absolutePath,
entries: [],
total_count: 0
});
}
const entries: FileEntry[] = [];
let hiddenCount = 0;
let ignoredCount = 0;
// Process each file/directory
for (const file of files) {
// Skip hidden files unless requested
if (!show_hidden && file.startsWith('.')) {
hiddenCount++;
continue;
}
// Check ignore patterns
if (shouldIgnore(file, ignore)) {
ignoredCount++;
continue;
}
const fullPath = path.join(absolutePath, file);
try {
const fileStats = fs.statSync(fullPath);
const isDir = fileStats.isDirectory();
const entry: FileEntry = {
name: file,
isDirectory: isDir,
size: isDir ? 0 : fileStats.size,
modifiedTime: fileStats.mtime,
extension: isDir ? undefined : path.extname(file).slice(1)
};
entries.push(entry);
} catch (error) {
// Log error but continue with other files
console.log(`Error accessing ${file}: ${error instanceof Error ? error.message : String(error)}`);
}
}
// Sort entries (directories first, then alphabetically)
entries.sort((a, b) => {
if (a.isDirectory && !b.isDirectory) {
return -1;
}
if (!a.isDirectory && b.isDirectory) {
return 1;
}
return a.name.localeCompare(b.name);
});
// Create formatted output
let summary = `Listed ${entries.length} item(s) in ${targetPath}`;
if (hiddenCount > 0) {
summary += ` (${hiddenCount} hidden)`;
}
if (ignoredCount > 0) {
summary += ` (${ignoredCount} ignored)`;
}
// Create detailed listing if requested
let detailedListing = '';
if (detailed && entries.length > 0) {
detailedListing = '\n\nDetailed listing:\n';
detailedListing += entries.map(entry => {
const type = entry.isDirectory ? '[DIR]' : '[FILE]';
const size = entry.isDirectory ? '' : ` ${formatFileSize(entry.size)}`;
const modified = ` ${formatModifiedTime(entry.modifiedTime)}`;
const ext = entry.extension ? ` .${entry.extension}` : '';
return `${type} ${entry.name}${size}${modified}${ext}`;
}).join('\n');
}
console.log(`${summary}${detailedListing}`);
return createSuccessResponse({
path: targetPath,
absolute_path: absolutePath,
entries,
total_count: entries.length,
hidden_count: hiddenCount,
ignored_count: ignoredCount,
directories: entries.filter(e => e.isDirectory).length,
files: entries.filter(e => !e.isDirectory).length,
summary,
detailed_listing: detailed ? detailedListing : undefined
});
} catch (error) {
return handleToolError(error, 'Ls tool execution', 'execution');
}
}
});
}

183
src/tools/multiedit-tool.ts Normal file
View File

@@ -0,0 +1,183 @@
import { z } from 'zod';
import { tool } from 'ai';
import * as fs from 'fs';
import * as path from 'path';
import { ExecutionContext } from '../types/agent';
import {
handleToolError,
validateWorkspacePath,
resolveWorkspacePath,
createSuccessResponse,
validateFileExists,
ToolResponse
} from './tool-utils';
const singleEditSchema = z.object({
old_string: z.string().describe('The exact text to find and replace. Must match exactly including whitespace.'),
new_string: z.string().describe('The text to replace old_string with.'),
expected_replacements: z.number().min(1).optional().describe('Number of replacements expected (default: 1)')
});
const multieditParametersSchema = z.object({
file_path: z.string().describe('Path to the file to edit (relative to workspace root, or absolute path within workspace)'),
edits: z.array(singleEditSchema).min(1).describe('Array of edit operations to perform in sequence'),
fail_fast: z.boolean().optional().describe('Whether to stop on first error (true) or continue with remaining edits (false, default)')
});
interface SingleEdit {
old_string: string;
new_string: string;
expected_replacements?: number;
}
interface EditResult {
edit: SingleEdit;
success: boolean;
occurrences: number;
error?: string;
}
// Path validation is now handled by validateWorkspacePath in tool-utils
/**
* Escape special regex characters
*/
function escapeRegExp(string: string): string {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
/**
* Apply a single edit to content
*/
function applySingleEdit(content: string, edit: SingleEdit): EditResult {
const expectedReplacements = edit.expected_replacements ?? 1;
// Count occurrences
const regex = new RegExp(escapeRegExp(edit.old_string), 'g');
const matches = content.match(regex) || [];
const occurrences = matches.length;
// Validate occurrence count
if (occurrences === 0) {
return {
edit,
success: false,
occurrences: 0,
error: `Text not found: "${edit.old_string.substring(0, 50)}${edit.old_string.length > 50 ? '...' : ''}"`
};
}
if (occurrences !== expectedReplacements) {
return {
edit,
success: false,
occurrences,
error: `Expected ${expectedReplacements} replacement(s) but found ${occurrences} occurrence(s)`
};
}
return {
edit,
success: true,
occurrences
};
}
export function createMultieditTool(context: ExecutionContext) {
return tool({
description: 'Perform multiple find-and-replace operations on a single file in sequence. Each edit is applied to the result of the previous edit. Accepts both relative and absolute file paths within the workspace.',
parameters: multieditParametersSchema,
execute: async (params): Promise<ToolResponse> => {
try {
const { file_path, edits, fail_fast = true } = params;
// Validate workspace path (handles both absolute and relative paths)
const pathError = validateWorkspacePath(file_path, context);
if (pathError) {
return pathError;
}
// Resolve path
const absolutePath = resolveWorkspacePath(file_path, context);
// Check if file exists
const fileError = validateFileExists(absolutePath, file_path);
if (fileError) {
return fileError;
}
console.log(`Performing ${edits.length} edit(s) on: ${file_path}`);
// Read current content
let currentContent: string;
try {
currentContent = fs.readFileSync(absolutePath, 'utf8');
// Normalize line endings to LF
currentContent = currentContent.replace(/\r\n/g, '\n');
} catch (error) {
return handleToolError(error, 'Failed to read file', 'permission');
}
const originalContent = currentContent;
const editResults: EditResult[] = [];
let successCount = 0;
let totalReplacements = 0;
// Apply edits sequentially
for (let i = 0; i < edits.length; i++) {
const edit = edits[i];
console.log(`Applying edit ${i + 1}/${edits.length}: "${edit.old_string.substring(0, 30)}..." => "${edit.new_string.substring(0, 30)}..."`);
const editResult = applySingleEdit(currentContent, edit);
editResults.push(editResult);
if (editResult.success) {
// Apply the edit
currentContent = currentContent.split(edit.old_string).join(edit.new_string);
successCount++;
totalReplacements += editResult.occurrences;
console.log(`✓ Edit ${i + 1} successful: ${editResult.occurrences} replacement(s)`);
} else {
console.log(`✗ Edit ${i + 1} failed: ${editResult.error}`);
if (fail_fast) {
return handleToolError(
`Edit operation failed at step ${i + 1}: ${editResult.error}`,
'Edit sequence',
'execution'
);
}
}
}
// Write the updated content if any edits were successful
if (successCount > 0) {
fs.writeFileSync(absolutePath, currentContent, 'utf8');
}
const newLines = currentContent.split('\n').length;
const newSize = Buffer.byteLength(currentContent, 'utf8');
const hasErrors = editResults.some(r => !r.success);
console.log(`Multi-edit completed: ${successCount}/${edits.length} edits successful, ${totalReplacements} total replacements`);
return createSuccessResponse({
file_path,
absolute_path: absolutePath,
edits_total: edits.length,
edits_successful: successCount,
edits_failed: edits.length - successCount,
total_replacements: totalReplacements,
lines_total: newLines,
bytes_total: newSize,
content_changed: currentContent !== originalContent,
edit_results: editResults
});
} catch (error) {
return handleToolError(error, 'Multiedit tool execution', 'execution');
}
}
});
}

301
src/tools/read-tool.ts Normal file
View File

@@ -0,0 +1,301 @@
import * as fs from 'fs';
import * as path from 'path';
import * as mime from 'mime-types';
import { tool } from 'ai';
import { z } from 'zod';
import { ExecutionContext } from '../types/agent';
import {
handleToolError,
validateWorkspacePath,
resolveWorkspacePath,
createSuccessResponse,
validateFileExists,
ToolResponse
} from './tool-utils';
/**
* File read result with metadata
*/
export interface FileReadResult {
content: string;
filePath: string;
fileType: 'text' | 'image' | 'pdf' | 'binary';
mimeType?: string;
lineCount?: number;
isTruncated?: boolean;
linesShown?: [number, number]; // [startLine, endLine]
size: number;
}
// Constants for file processing
const DEFAULT_MAX_LINES = 1000;
const MAX_LINE_LENGTH = 2000;
const MAX_FILE_SIZE_BYTES = 10 * 1024 * 1024; // 10MB
// Path validation is now handled by validateWorkspacePath in tool-utils
/**
* Check if a file is likely binary by sampling content
*/
function isBinaryFile(filePath: string): boolean {
try {
const fd = fs.openSync(filePath, 'r');
const fileSize = fs.fstatSync(fd).size;
if (fileSize === 0) {
fs.closeSync(fd);
return false;
}
const bufferSize = Math.min(4096, fileSize);
const buffer = Buffer.alloc(bufferSize);
const bytesRead = fs.readSync(fd, buffer, 0, buffer.length, 0);
fs.closeSync(fd);
if (bytesRead === 0) {return false;}
// Check for null bytes (strong binary indicator)
for (let i = 0; i < bytesRead; i++) {
if (buffer[i] === 0) {
return true;
}
}
// Count non-printable characters
let nonPrintableCount = 0;
for (let i = 0; i < bytesRead; i++) {
if (buffer[i] < 9 || (buffer[i] > 13 && buffer[i] < 32)) {
nonPrintableCount++;
}
}
// If >30% non-printable characters, consider binary
return nonPrintableCount / bytesRead > 0.3;
} catch {
return false;
}
}
/**
* Detect file type based on extension and content
*/
function detectFileType(filePath: string): 'text' | 'image' | 'pdf' | 'binary' {
const ext = path.extname(filePath).toLowerCase();
const mimeType = mime.lookup(filePath);
// Check for images
if (mimeType && mimeType.startsWith('image/')) {
return 'image';
}
// Check for PDF
if (mimeType === 'application/pdf') {
return 'pdf';
}
// Known binary extensions
const binaryExtensions = [
'.exe', '.dll', '.so', '.dylib', '.zip', '.tar', '.gz', '.7z',
'.bin', '.dat', '.class', '.jar', '.war', '.pyc', '.pyo',
'.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
'.odt', '.ods', '.odp', '.wasm', '.obj', '.o', '.a', '.lib'
];
if (binaryExtensions.includes(ext)) {
return 'binary';
}
// Content-based binary detection
if (isBinaryFile(filePath)) {
return 'binary';
}
return 'text';
}
/**
* Process text file content with line range support
*/
async function processTextFile(
filePath: string,
startLine?: number,
lineCount?: number,
encoding: string = 'utf-8'
): Promise<{ content: string; metadata: Partial<FileReadResult> }> {
const content = await fs.promises.readFile(filePath, encoding as BufferEncoding);
const lines = content.split('\n');
const originalLineCount = lines.length;
// Handle line range
const actualStartLine = Math.max((startLine || 1) - 1, 0); // Convert to 0-based
const actualLineCount = lineCount || Math.min(DEFAULT_MAX_LINES, originalLineCount);
const endLine = Math.min(actualStartLine + actualLineCount, originalLineCount);
const selectedLines = lines.slice(actualStartLine, endLine);
// Truncate long lines
let linesWereTruncated = false;
const processedLines = selectedLines.map(line => {
if (line.length > MAX_LINE_LENGTH) {
linesWereTruncated = true;
return line.substring(0, MAX_LINE_LENGTH) + '... [line truncated]';
}
return line;
});
const contentWasTruncated = endLine < originalLineCount;
const isTruncated = contentWasTruncated || linesWereTruncated;
let processedContent = processedLines.join('\n');
// Add truncation notice
if (contentWasTruncated) {
processedContent = `[Content truncated: showing lines ${actualStartLine + 1}-${endLine} of ${originalLineCount} total lines]\n\n` + processedContent;
} else if (linesWereTruncated) {
processedContent = `[Some lines truncated due to length (max ${MAX_LINE_LENGTH} chars)]\n\n` + processedContent;
}
return {
content: processedContent,
metadata: {
lineCount: originalLineCount,
isTruncated,
linesShown: [actualStartLine + 1, endLine]
}
};
}
/**
* Process image or PDF file
*/
async function processMediaFile(
filePath: string,
fileType: 'image' | 'pdf'
): Promise<{ content: string; metadata: Partial<FileReadResult> }> {
const buffer = await fs.promises.readFile(filePath);
const base64Data = buffer.toString('base64');
const mimeType = mime.lookup(filePath) || 'application/octet-stream';
// For SuperDesign, we'll return a descriptive message rather than raw base64
// The actual file handling would be done by the VS Code webview
const fileName = path.basename(filePath);
const fileSize = (buffer.length / 1024).toFixed(1);
return {
content: `[${fileType.toUpperCase()} FILE: ${fileName}]\nFile size: ${fileSize} KB\nMIME type: ${mimeType}\nBase64 data available for webview display.`,
metadata: {
mimeType
}
};
}
/**
* Create SuperDesign read tool with execution context
*/
export function createReadTool(context: ExecutionContext) {
return tool({
description: 'Read the contents of a file within the SuperDesign workspace. Supports text files, images (PNG, JPG, SVG, etc.), and handles large files with line-range reading.',
parameters: z.object({
filePath: z.string().describe('Path to the file to read, relative to the workspace root or absolute path within workspace'),
startLine: z.number().optional().describe('Optional: Starting line number to read from (1-based). Use with lineCount for large files.'),
lineCount: z.number().optional().describe('Optional: Number of lines to read. Use with startLine to read specific sections.'),
encoding: z.string().optional().describe('Optional: File encoding (utf-8, ascii, etc.). Defaults to utf-8.')
}),
execute: async ({ filePath, startLine, lineCount, encoding }): Promise<ToolResponse> => {
const startTime = Date.now();
try {
// Validate workspace path (handles both absolute and relative paths)
const pathError = validateWorkspacePath(filePath, context);
if (pathError) {
return pathError;
}
// Resolve file path
const absolutePath = resolveWorkspacePath(filePath, context);
// Check file existence
const fileError = validateFileExists(absolutePath, filePath);
if (fileError) {
return fileError;
}
// Check if it's a directory
const stats = fs.statSync(absolutePath);
if (stats.isDirectory()) {
return handleToolError(`Path is a directory, not a file: ${filePath}`, 'Path validation', 'validation');
}
// Check file size
if (stats.size > MAX_FILE_SIZE_BYTES) {
const sizeMB = (stats.size / 1024 / 1024).toFixed(1);
return handleToolError(
`File too large (${sizeMB}MB). Maximum size: ${MAX_FILE_SIZE_BYTES / 1024 / 1024}MB`,
'File size check',
'validation'
);
}
// Detect file type
const fileType = detectFileType(absolutePath);
context.outputChannel.appendLine(`[read] Reading ${fileType} file: ${filePath} (${(stats.size / 1024).toFixed(1)} KB)`);
let content: string;
let metadata: Partial<FileReadResult> = {};
// Process based on file type
switch (fileType) {
case 'text': {
const result = await processTextFile(
absolutePath,
startLine,
lineCount,
encoding
);
content = result.content;
metadata = result.metadata;
break;
}
case 'image':
case 'pdf': {
const result = await processMediaFile(absolutePath, fileType);
content = result.content;
metadata = result.metadata;
break;
}
case 'binary': {
const fileName = path.basename(absolutePath);
const fileSize = (stats.size / 1024).toFixed(1);
content = `[BINARY FILE: ${fileName}]\nFile size: ${fileSize} KB\nCannot display binary content as text.`;
break;
}
default:
return handleToolError(`Unsupported file type: ${fileType}`, 'File type detection', 'validation');
}
// Create result
const fileReadResult: FileReadResult = {
content,
filePath,
fileType,
mimeType: mime.lookup(absolutePath) || undefined,
size: stats.size,
...metadata
};
const duration = Date.now() - startTime;
context.outputChannel.appendLine(`[read] File read completed in ${duration}ms`);
return createSuccessResponse(fileReadResult);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
context.outputChannel.appendLine(`[read] Read failed: ${errorMessage}`);
return handleToolError(error, 'Read tool execution', 'execution');
}
}
});
}

90
src/tools/theme-tool.ts Normal file
View File

@@ -0,0 +1,90 @@
import * as fs from 'fs';
import * as path from 'path';
import { tool } from 'ai';
import { z } from 'zod';
import { ExecutionContext } from '../types/agent';
import {
handleToolError,
validateWorkspacePath,
resolveWorkspacePath,
createSuccessResponse,
validateFileExists,
ToolResponse
} from './tool-utils';
const themePrompt = `Design a perfect theme that including color, font, spacing, shadown, etc.
`;
const cssSheetDescription = `The full css sheet content, has to include below classes:
:root selector - Must contain CSS custom properties
CSS custom properties format - --variable-name: value;
Semicolon-terminated - Each property must end with ;
--background, --foreground (basic colors)
--primary, --primary-foreground (brand colors)
--secondary, --muted, --accent (semantic colors)
--destructive, --border, --input, --ring (UI elements)
--card, --popover + their foreground variants
--chart-1 through --chart-5 (data visualization)
--sidebar-* variables for navigation
--font-sans, --font-serif, --font-mono
--radius, --spacing
--shadow-* variables (xs, sm, md, lg, xl, etc.)
You can add more relevant ones based on use cases, but make sure to include all the above classes.`;
/**
* Create SuperDesign read tool with execution context
*/
export function createThemeTool(context: ExecutionContext) {
return tool({
description: themePrompt,
parameters: z.object({
theme_name: z.string().describe('The name of the theme'),
reasoning_reference: z.string().describe('Think through the theme design to make it coherent and what reference you used'),
cssSheet: z.string().describe(cssSheetDescription),
cssFilePath: z.string().describe('Path to the css file to write to (relative to workspace root, or absolute path within workspace)'),
create_dirs: z.boolean().optional().default(true).describe('Whether to create parent directories if they don\'t exist (default: true)')
}),
execute: async ({ cssSheet, theme_name, reasoning_reference, cssFilePath, create_dirs = true}): Promise<ToolResponse> => {
try {
// Validate and resolve the CSS file path
const validationError = validateWorkspacePath(cssFilePath, context);
if (validationError) {
return validationError;
}
const resolvedPath = resolveWorkspacePath(cssFilePath, context);
// Create parent directories if needed
if (create_dirs) {
const dirPath = path.dirname(resolvedPath);
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
context.outputChannel.appendLine(`[theme] Created directory: ${dirPath}`);
}
}
// Write the CSS content to file
fs.writeFileSync(resolvedPath, cssSheet, 'utf8');
context.outputChannel.appendLine(`[theme] Successfully created theme "${theme_name}" at: ${resolvedPath}`);
return createSuccessResponse({
success: true,
message: `Theme "${theme_name}" saved successfully`,
filePath: resolvedPath,
theme_name,
reasoning_reference,
cssSheet
});
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
context.outputChannel.appendLine(`[theme] Theme creation failed: ${errorMessage}`);
return handleToolError(error, 'Theme tool execution', 'execution');
}
}
});
}

181
src/tools/tool-utils.ts Normal file
View File

@@ -0,0 +1,181 @@
import * as path from 'path';
import { ExecutionContext } from '../types/agent';
/**
* Standard error response structure for all tools
*/
export interface ToolErrorResponse {
success: false;
error: string;
error_type?: 'validation' | 'security' | 'file_not_found' | 'permission' | 'execution' | 'unknown';
details?: any;
}
/**
* Standard success response structure for all tools
*/
export interface ToolSuccessResponse {
success: true;
[key: string]: any;
}
export type ToolResponse = ToolSuccessResponse | ToolErrorResponse;
/**
* Generic error handler that converts exceptions/errors to standardized error responses
*/
export function handleToolError(
error: unknown,
context?: string,
errorType: ToolErrorResponse['error_type'] = 'unknown'
): ToolErrorResponse {
let errorMessage: string;
let details: any;
if (error instanceof Error) {
errorMessage = error.message;
details = {
name: error.name,
stack: error.stack?.split('\n').slice(0, 3) // Truncated stack trace
};
} else if (typeof error === 'string') {
errorMessage = error;
} else {
errorMessage = 'An unknown error occurred';
details = { originalError: error };
}
// Add context if provided
if (context) {
errorMessage = `${context}: ${errorMessage}`;
}
console.error(`Tool error (${errorType}): ${errorMessage}`);
return {
success: false,
error: errorMessage,
error_type: errorType,
details
};
}
/**
* Validate if a path is within the workspace directory (supports both absolute and relative paths)
*/
export function validateWorkspacePath(filePath: string, context: ExecutionContext): ToolErrorResponse | null {
try {
// Prevent directory traversal attacks
if (filePath.includes('..')) {
return handleToolError('Path cannot contain ".." for security reasons', 'Path validation', 'security');
}
const normalizedWorkspace = path.normalize(context.workingDirectory);
// Handle both absolute and relative paths
let resolvedPath: string;
if (path.isAbsolute(filePath)) {
resolvedPath = path.normalize(filePath);
} else {
resolvedPath = path.resolve(context.workingDirectory, filePath);
}
// Check if path is within workspace boundary
if (!resolvedPath.startsWith(normalizedWorkspace)) {
return handleToolError(
`Path must be within workspace directory: ${filePath}`,
'Security check',
'security'
);
}
return null; // No error
} catch (error) {
return handleToolError(error, 'Path validation', 'validation');
}
}
/**
* Safely resolve a file path (supports both absolute and relative paths)
*/
export function resolveWorkspacePath(filePath: string, context: ExecutionContext): string {
if (path.isAbsolute(filePath)) {
return path.normalize(filePath);
} else {
return path.resolve(context.workingDirectory, filePath);
}
}
/**
* Create a success response
*/
export function createSuccessResponse(data: Record<string, any>): ToolSuccessResponse {
return {
success: true,
...data
};
}
/**
* Validation helper for required string parameters
*/
export function validateRequiredString(value: any, paramName: string): ToolErrorResponse | null {
if (!value || typeof value !== 'string' || value.trim() === '') {
return handleToolError(
`${paramName} is required and must be a non-empty string`,
'Parameter validation',
'validation'
);
}
return null;
}
/**
* Validation helper for file existence
*/
export function validateFileExists(absolutePath: string, filePath: string): ToolErrorResponse | null {
const fs = require('fs');
try {
if (!fs.existsSync(absolutePath)) {
return handleToolError(
`File not found: ${filePath}`,
'File existence check',
'file_not_found'
);
}
return null;
} catch (error) {
return handleToolError(error, 'File existence check', 'permission');
}
}
/**
* Validation helper for directory existence
*/
export function validateDirectoryExists(absolutePath: string, dirPath: string): ToolErrorResponse | null {
const fs = require('fs');
try {
if (!fs.existsSync(absolutePath)) {
return handleToolError(
`Directory not found: ${dirPath}`,
'Directory existence check',
'file_not_found'
);
}
const stats = fs.statSync(absolutePath);
if (!stats.isDirectory()) {
return handleToolError(
`Path is not a directory: ${dirPath}`,
'Directory validation',
'validation'
);
}
return null;
} catch (error) {
return handleToolError(error, 'Directory validation', 'permission');
}
}

101
src/tools/write-tool.ts Normal file
View File

@@ -0,0 +1,101 @@
import * as fs from 'fs';
import * as path from 'path';
import { tool } from 'ai';
import { z } from 'zod';
import { ExecutionContext } from '../types/agent';
import {
handleToolError,
validateWorkspacePath,
resolveWorkspacePath,
createSuccessResponse,
ToolResponse
} from './tool-utils';
/**
* Write tool result with metadata
*/
export interface WriteToolResult {
file_path: string;
absolute_path: string;
is_new_file: boolean;
lines_written: number;
bytes_written: number;
}
// Path validation is now handled by validateWorkspacePath in tool-utils
/**
* Create SuperDesign write tool with execution context
*/
export function createWriteTool(context: ExecutionContext) {
return tool({
description: 'Write content to a file in the SuperDesign workspace. Creates parent directories if needed.',
parameters: z.object({
file_path: z.string().describe('Path to the file to write to (relative to workspace root, or absolute path within workspace)'),
content: z.string().describe('Content to write to the file'),
create_dirs: z.boolean().optional().default(true).describe('Whether to create parent directories if they don\'t exist (default: true)')
}),
execute: async ({ file_path, content, create_dirs = true }): Promise<ToolResponse> => {
const startTime = Date.now();
try {
// Validate workspace path (handles both absolute and relative paths)
const pathError = validateWorkspacePath(file_path, context);
if (pathError) {
return pathError;
}
// Resolve absolute path within workspace
const absolutePath = resolveWorkspacePath(file_path, context);
context.outputChannel.appendLine(`[write] Writing to file: ${file_path}`);
// Check if target is a directory
if (fs.existsSync(absolutePath)) {
const stats = fs.lstatSync(absolutePath);
if (stats.isDirectory()) {
return handleToolError(`Target path is a directory, not a file: ${file_path}`, 'Path validation', 'validation');
}
}
// Create parent directories if needed and requested
if (create_dirs) {
const dirName = path.dirname(absolutePath);
if (!fs.existsSync(dirName)) {
fs.mkdirSync(dirName, { recursive: true });
context.outputChannel.appendLine(`[write] Created parent directories for: ${file_path}`);
}
}
// Determine if this is a new file or overwrite
const isNewFile = !fs.existsSync(absolutePath);
// Write the file
fs.writeFileSync(absolutePath, content, 'utf8');
const duration = Date.now() - startTime;
const lines = content.split('\n').length;
const size = Buffer.byteLength(content, 'utf8');
context.outputChannel.appendLine(`[write] ${isNewFile ? 'Created' : 'Updated'} file: ${file_path} (${lines} lines, ${size} bytes) in ${duration}ms`);
const result: WriteToolResult = {
file_path,
absolute_path: absolutePath,
is_new_file: isNewFile,
lines_written: lines,
bytes_written: size
};
return createSuccessResponse(result);
} catch (error) {
const duration = Date.now() - startTime;
const errorMessage = error instanceof Error ? error.message : String(error);
context.outputChannel.appendLine(`[write] Error writing file: ${errorMessage} (${duration}ms)`);
return handleToolError(error, 'Write tool execution', 'execution');
}
}
});
}

22
src/types/agent.ts Normal file
View File

@@ -0,0 +1,22 @@
import * as vscode from 'vscode';
import { CoreMessage } from 'ai';
export interface AgentService {
query(
prompt?: string,
messages?: CoreMessage[],
options?: any,
abortController?: AbortController,
onMessage?: (message: any) => void
): Promise<any[]>;
hasApiKey(): boolean;
isApiKeyAuthError(errorMessage: string): boolean;
}
export interface ExecutionContext {
workingDirectory: string;
sessionId: string;
outputChannel: vscode.OutputChannel;
abortController?: AbortController;
}

46
src/types/context.ts Normal file
View File

@@ -0,0 +1,46 @@
export type WebviewLayout = 'sidebar' | 'panel';
export interface WebviewContext {
layout: WebviewLayout;
extensionUri: string;
logoUris?: {
cursor: string;
windsurf: string;
claudeCode: string;
lovable: string;
bolt: string;
};
}
export interface WebviewMessage {
command: string;
[key: string]: any;
}
export interface ChatCommand extends WebviewMessage {
command: 'chatMessage';
message: string;
}
export interface ChatResponse extends WebviewMessage {
command: 'chatResponse';
response: string;
}
export interface ChatError extends WebviewMessage {
command: 'chatError';
error: string;
}
export interface StopChatCommand extends WebviewMessage {
command: 'stopChat';
}
export interface ChatStopped extends WebviewMessage {
command: 'chatStopped';
}
export interface InitContext extends WebviewMessage {
command: 'initContext';
context: WebviewContext;
}

1586
src/webview/App.css Normal file

File diff suppressed because it is too large Load Diff

113
src/webview/App.tsx Normal file
View File

@@ -0,0 +1,113 @@
import React, { useState, useEffect } from 'react';
import ChatInterface from './components/Chat/ChatInterface';
import CanvasView from './components/CanvasView';
import { WebviewContext } from '../types/context';
// Import CSS as string for esbuild
import styles from './App.css';
const App: React.FC = () => {
console.log('🚀 App component starting...');
const [vscode] = useState(() => {
console.log('📞 Acquiring vscode API...');
return acquireVsCodeApi();
});
const [context, setContext] = useState<WebviewContext | null>(null);
const [currentView, setCurrentView] = useState<'chat' | 'canvas'>('chat');
const [nonce, setNonce] = useState<string | null>(null);
useEffect(() => {
console.log('🔄 App useEffect running...');
// Detect which view to render based on data-view attribute
const rootElement = document.getElementById('root');
console.log('📍 Root element:', rootElement);
const viewType = rootElement?.getAttribute('data-view');
const nonceValue = rootElement?.getAttribute('data-nonce');
console.log('🎯 View type detected:', viewType);
console.log('🔐 Nonce value:', nonceValue);
if (nonceValue) {
setNonce(nonceValue);
console.log('✅ Nonce set:', nonceValue);
}
if (viewType === 'canvas') {
setCurrentView('canvas');
console.log('🎨 Switching to canvas view');
} else {
setCurrentView('chat');
console.log('💬 Switching to chat view');
}
// Inject CSS styles
const styleElement = document.createElement('style');
styleElement.textContent = styles;
document.head.appendChild(styleElement);
console.log('🎨 CSS styles injected');
// Get context from window (only needed for chat interface)
const webviewContext = (window as any).__WEBVIEW_CONTEXT__;
console.log('🌐 Webview context from window:', webviewContext);
if (webviewContext) {
setContext(webviewContext);
console.log('✅ Context set:', webviewContext);
} else {
console.log('⚠️ No webview context found in window');
}
return () => {
document.head.removeChild(styleElement);
};
}, []);
const renderView = () => {
console.log('🖼️ Rendering view, currentView:', currentView);
switch (currentView) {
case 'canvas':
console.log('🎨 Rendering CanvasView with vscode:', !!vscode, 'nonce:', nonce);
try {
// Canvas view doesn't need context - it gets data from extension directly
return <CanvasView vscode={vscode} nonce={nonce} />;
} catch (error) {
console.error('❌ Error rendering CanvasView:', error);
return <div>Error rendering canvas: {String(error)}</div>;
}
case 'chat':
default:
console.log('💬 Rendering ChatInterface, context:', !!context);
// Chat interface needs context
if (!context) {
console.log('⏳ Context not ready, showing loading...');
return <div>Loading...</div>;
}
try {
return (
<ChatInterface
layout={context.layout}
vscode={vscode}
/>
);
} catch (error) {
console.error('❌ Error rendering ChatInterface:', error);
return <div>Error rendering chat: {String(error)}</div>;
}
}
};
console.log('🔄 App rendering, currentView:', currentView);
return (
<div className={`superdesign-app ${currentView}-view ${context?.layout ? `${context.layout}-layout` : ''}`}>
{renderView()}
</div>
);
};
export default App;

View File

@@ -0,0 +1,860 @@
import React, { useState, useEffect, useRef } from 'react';
import { TransformWrapper, TransformComponent, ReactZoomPanPinchRef } from 'react-zoom-pan-pinch';
import DesignFrame from './DesignFrame';
import { calculateGridPosition, calculateFitToView, getGridMetrics, generateResponsiveConfig, buildHierarchyTree, calculateHierarchyPositions, getHierarchicalPosition, detectDesignRelationships } from '../utils/gridLayout';
import {
DesignFile,
CanvasState,
WebviewMessage,
ExtensionToWebviewMessage,
CanvasConfig,
ViewportMode,
FrameViewportState,
FramePositionState,
DragState,
GridPosition,
LayoutMode,
HierarchyTree,
ConnectionLine
} from '../types/canvas.types';
import ConnectionLines from './ConnectionLines';
import {
ZoomInIcon,
ZoomOutIcon,
HomeIcon,
ScaleIcon,
RefreshIcon,
GlobeIcon,
MobileIcon,
TabletIcon,
DesktopIcon,
TreeIcon,
LinkIcon
} from './Icons';
interface CanvasViewProps {
vscode: any;
nonce: string | null;
}
const CANVAS_CONFIG: CanvasConfig = {
frameSize: { width: 320, height: 400 }, // Smaller default frame size for better density
gridSpacing: 50, // Much tighter spacing between frames
framesPerRow: 4, // Fit 4 frames per row by default
minZoom: 0.1,
maxZoom: 5,
responsive: {
enableScaling: true,
minFrameSize: { width: 160, height: 200 }, // Reduced minimum size
maxFrameSize: { width: 400, height: 500 }, // Reduced maximum size
scaleWithZoom: false
},
viewports: {
desktop: { width: 1000, height: 600 }, // More compact desktop view
tablet: { width: 640, height: 800 }, // Smaller tablet view
mobile: { width: 320, height: 550 } // More compact mobile view
},
hierarchy: {
horizontalSpacing: 180, // Reduced horizontal spacing for hierarchy
verticalSpacing: 120, // Reduced vertical spacing for hierarchy
connectionLineWidth: 2,
connectionLineColor: 'var(--vscode-textLink-foreground)',
showConnections: true
}
};
const CanvasView: React.FC<CanvasViewProps> = ({ vscode, nonce }) => {
console.log('🎨 CanvasView component starting...');
console.log('📞 CanvasView props - vscode:', !!vscode, 'nonce:', nonce);
const [designFiles, setDesignFiles] = useState<DesignFile[]>([]);
const [selectedFrames, setSelectedFrames] = useState<string[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [currentZoom, setCurrentZoom] = useState(1);
const [currentConfig, setCurrentConfig] = useState<CanvasConfig>(CANVAS_CONFIG);
const [globalViewportMode, setGlobalViewportMode] = useState<ViewportMode>('tablet');
const [frameViewports, setFrameViewports] = useState<FrameViewportState>({});
const [useGlobalViewport, setUseGlobalViewport] = useState(true);
const [customPositions, setCustomPositions] = useState<FramePositionState>({});
const [dragState, setDragState] = useState<DragState>({
isDragging: false,
draggedFrame: null,
startPosition: { x: 0, y: 0 },
currentPosition: { x: 0, y: 0 },
offset: { x: 0, y: 0 }
});
const [layoutMode, setLayoutMode] = useState<LayoutMode>('grid');
const [hierarchyTree, setHierarchyTree] = useState<HierarchyTree | null>(null);
const [showConnections, setShowConnections] = useState(true);
const transformRef = useRef<ReactZoomPanPinchRef>(null);
console.log('✅ CanvasView state initialized successfully');
// Performance optimization: Switch render modes based on zoom level
const getOptimalRenderMode = (_zoom: number): 'placeholder' | 'iframe' => {
// Always render iframe as requested by the user
return 'iframe';
};
// Helper function to transform mouse coordinates to canvas space
const transformMouseToCanvasSpace = (clientX: number, clientY: number, canvasRect: DOMRect): GridPosition => {
// Get current transform state from the TransformWrapper
const transformState = transformRef.current?.instance?.transformState;
const currentScale = transformState?.scale || 1;
const currentTranslateX = transformState?.positionX || 0;
const currentTranslateY = transformState?.positionY || 0;
// Calculate mouse position relative to canvas, then adjust for zoom and pan
const rawMouseX = clientX - canvasRect.left;
const rawMouseY = clientY - canvasRect.top;
// Transform mouse coordinates to canvas space (inverse of current transform)
return {
x: (rawMouseX - currentTranslateX) / currentScale,
y: (rawMouseY - currentTranslateY) / currentScale
};
};
// Viewport management functions
const getFrameViewport = (fileName: string): ViewportMode => {
if (useGlobalViewport) {
return globalViewportMode;
}
return frameViewports[fileName] || 'desktop';
};
const handleFrameViewportChange = (fileName: string, viewport: ViewportMode) => {
setFrameViewports(prev => ({
...prev,
[fileName]: viewport
}));
};
const handleGlobalViewportChange = (viewport: ViewportMode) => {
setGlobalViewportMode(viewport);
if (useGlobalViewport) {
// Update all frames to the new global viewport
const newFrameViewports: FrameViewportState = {};
designFiles.forEach(file => {
newFrameViewports[file.name] = viewport;
});
setFrameViewports(newFrameViewports);
// Update hierarchy positioning when viewport changes to adjust connection spacing
if (hierarchyTree && designFiles.length > 0) {
// Recalculate frame dimensions for new viewport
let totalWidth = 0;
let totalHeight = 0;
let frameCount = 0;
designFiles.forEach(file => {
const viewportDimensions = currentConfig.viewports[viewport];
totalWidth += viewportDimensions.width;
totalHeight += viewportDimensions.height + 50; // Add header space
frameCount++;
});
const avgFrameDimensions = frameCount > 0 ? {
width: Math.round(totalWidth / frameCount),
height: Math.round(totalHeight / frameCount)
} : { width: 400, height: 550 };
const updatedTree = calculateHierarchyPositions(hierarchyTree, currentConfig, avgFrameDimensions);
setHierarchyTree(updatedTree);
}
}
};
const toggleGlobalViewport = () => {
const newUseGlobal = !useGlobalViewport;
setUseGlobalViewport(newUseGlobal);
if (newUseGlobal) {
// Set all frames to current global viewport
const newFrameViewports: FrameViewportState = {};
designFiles.forEach(file => {
newFrameViewports[file.name] = globalViewportMode;
});
setFrameViewports(newFrameViewports);
}
};
// Responsive config update
useEffect(() => {
const updateConfig = () => {
const responsive = generateResponsiveConfig(CANVAS_CONFIG, window.innerWidth);
setCurrentConfig(responsive);
};
updateConfig();
window.addEventListener('resize', updateConfig);
return () => window.removeEventListener('resize', updateConfig);
}, []);
useEffect(() => {
// Request design files from extension
const loadMessage: WebviewMessage = {
command: 'loadDesignFiles'
};
vscode.postMessage(loadMessage);
// Listen for messages from extension
const messageHandler = (event: MessageEvent) => {
const message: ExtensionToWebviewMessage = event.data;
switch (message.command) {
case 'designFilesLoaded':
// Convert date strings back to Date objects
const filesWithDates = message.data.files.map(file => ({
...file,
modified: new Date(file.modified)
}));
// Detect design relationships and build hierarchy
const filesWithRelationships = detectDesignRelationships(filesWithDates);
setDesignFiles(filesWithRelationships);
// Build hierarchy tree
const tree = buildHierarchyTree(filesWithRelationships);
// Calculate average frame dimensions based on viewport usage
let totalWidth = 0;
let totalHeight = 0;
let frameCount = 0;
filesWithRelationships.forEach(file => {
const frameViewport = getFrameViewport(file.name);
const viewportDimensions = currentConfig.viewports[frameViewport];
totalWidth += viewportDimensions.width;
totalHeight += viewportDimensions.height + 50; // Add header space
frameCount++;
});
const avgFrameDimensions = frameCount > 0 ? {
width: Math.round(totalWidth / frameCount),
height: Math.round(totalHeight / frameCount)
} : { width: 400, height: 550 };
const positionedTree = calculateHierarchyPositions(tree, currentConfig, avgFrameDimensions);
setHierarchyTree(positionedTree);
setIsLoading(false);
// Auto-center view after files are loaded
setTimeout(() => {
if (transformRef.current) {
transformRef.current.resetTransform();
}
}, 100);
break;
case 'error':
setError(message.data.error);
setIsLoading(false);
break;
case 'fileChanged':
// Handle file system changes (will implement in Task 2.3)
console.log('File changed:', message.data);
// Re-request files when changes occur
vscode.postMessage({ command: 'loadDesignFiles' });
break;
}
};
window.addEventListener('message', messageHandler);
return () => window.removeEventListener('message', messageHandler);
}, [vscode]); // Removed currentConfig dependency to prevent constant re-renders
const handleFrameSelect = (fileName: string) => {
setSelectedFrames([fileName]); // Single selection for now
// Find the selected file to get its full path
const selectedFile = designFiles.find(file => file.name === fileName);
const filePath = selectedFile ? selectedFile.path : fileName;
const selectMessage: WebviewMessage = {
command: 'selectFrame',
data: { fileName }
};
vscode.postMessage(selectMessage);
// Also send context to chat interface with full path
const contextMessage: WebviewMessage = {
command: 'setContextFromCanvas',
data: { fileName: filePath, type: 'frame' }
};
vscode.postMessage(contextMessage);
};
const handleSendToChat = (fileName: string, prompt: string) => {
// Find the selected file to get its full path
const selectedFile = designFiles.find(file => file.name === fileName);
const filePath = selectedFile ? selectedFile.path : fileName;
// Set context first
const contextMessage: WebviewMessage = {
command: 'setContextFromCanvas',
data: { fileName: filePath, type: 'frame' }
};
vscode.postMessage(contextMessage);
// Then send the prompt to the chat input
const promptMessage: WebviewMessage = {
command: 'setChatPrompt',
data: { prompt }
};
vscode.postMessage(promptMessage);
};
// Canvas control functions
const handleZoomIn = () => {
if (transformRef.current) {
const currentState = transformRef.current.instance?.transformState;
console.log('🔍 ZOOM IN - Before:', {
scale: currentState?.scale,
positionX: currentState?.positionX,
positionY: currentState?.positionY,
step: 0.05,
minScale: 0.1,
maxScale: 3,
smooth: false
});
transformRef.current.zoomIn(0.05);
// Log after zoom (with small delay to capture the change)
setTimeout(() => {
const newState = transformRef.current?.instance?.transformState;
console.log('🔍 ZOOM IN - After:', {
scale: newState?.scale,
positionX: newState?.positionX,
positionY: newState?.positionY,
scaleDiff: newState?.scale ? (newState.scale - (currentState?.scale || 1)) : 0,
positionXDiff: newState?.positionX ? (newState.positionX - (currentState?.positionX || 0)) : 0,
positionYDiff: newState?.positionY ? (newState.positionY - (currentState?.positionY || 0)) : 0
});
}, 50);
}
};
const handleZoomOut = () => {
if (transformRef.current) {
const currentState = transformRef.current.instance?.transformState;
console.log('🔍 ZOOM OUT - Before:', {
scale: currentState?.scale,
positionX: currentState?.positionX,
positionY: currentState?.positionY,
step: 0.05
});
transformRef.current.zoomOut(0.05);
// Log after zoom (with small delay to capture the change)
setTimeout(() => {
const newState = transformRef.current?.instance?.transformState;
console.log('🔍 ZOOM OUT - After:', {
scale: newState?.scale,
positionX: newState?.positionX,
positionY: newState?.positionY,
scaleDiff: newState?.scale ? (newState.scale - (currentState?.scale || 1)) : 0,
positionXDiff: newState?.positionX ? (newState.positionX - (currentState?.positionX || 0)) : 0,
positionYDiff: newState?.positionY ? (newState.positionY - (currentState?.positionY || 0)) : 0
});
}, 50);
}
};
const handleResetZoom = () => {
if (transformRef.current) {
const currentState = transformRef.current.instance?.transformState;
console.log('🔍 RESET ZOOM - Before:', {
scale: currentState?.scale,
positionX: currentState?.positionX,
positionY: currentState?.positionY
});
transformRef.current.resetTransform();
setTimeout(() => {
const newState = transformRef.current?.instance?.transformState;
console.log('🔍 RESET ZOOM - After:', {
scale: newState?.scale,
positionX: newState?.positionX,
positionY: newState?.positionY
});
}, 50);
}
};
const handleTransformChange = (ref: ReactZoomPanPinchRef) => {
const state = ref.state;
// Prevent negative or zero scales
if (state.scale <= 0) {
console.error('🚨 INVALID SCALE DETECTED:', state.scale, '- Resetting to minimum');
ref.setTransform(state.positionX, state.positionY, 0.1);
return;
}
console.log('🔄 TRANSFORM CHANGE:', {
scale: state.scale,
positionX: state.positionX,
positionY: state.positionY,
previousScale: currentZoom
});
setCurrentZoom(state.scale);
};
// Get frame position (custom, hierarchy, or default grid position)
const getFramePosition = (fileName: string, index: number): GridPosition => {
if (customPositions[fileName]) {
return customPositions[fileName];
}
// Use hierarchy layout if in hierarchy mode and tree is available
if (layoutMode === 'hierarchy' && hierarchyTree) {
return getHierarchicalPosition(fileName, hierarchyTree);
}
// Default grid position calculation
const viewportMode = getFrameViewport(fileName);
const viewportDimensions = currentConfig.viewports[viewportMode];
const actualWidth = viewportDimensions.width;
const actualHeight = viewportDimensions.height + 50;
const col = index % currentConfig.framesPerRow;
const row = Math.floor(index / currentConfig.framesPerRow);
const x = col * (Math.max(actualWidth, currentConfig.frameSize.width) + currentConfig.gridSpacing);
const y = row * (Math.max(actualHeight, currentConfig.frameSize.height) + currentConfig.gridSpacing);
return { x, y };
};
// Drag handlers
const handleDragStart = (fileName: string, startPos: GridPosition, mouseEvent: React.MouseEvent) => {
// Get canvas grid element for proper coordinate calculation
const canvasGrid = document.querySelector('.canvas-grid') as HTMLElement;
if (!canvasGrid) return;
const canvasRect = canvasGrid.getBoundingClientRect();
const canvasMousePos = transformMouseToCanvasSpace(mouseEvent.clientX, mouseEvent.clientY, canvasRect);
// Also ensure this frame is selected
if (!selectedFrames.includes(fileName)) {
setSelectedFrames([fileName]);
}
setDragState({
isDragging: true,
draggedFrame: fileName,
startPosition: startPos,
currentPosition: startPos,
offset: {
x: canvasMousePos.x - startPos.x,
y: canvasMousePos.y - startPos.y
}
});
};
const handleDragMove = (mousePos: GridPosition) => {
if (!dragState.isDragging || !dragState.draggedFrame) return;
const newPosition = {
x: mousePos.x - dragState.offset.x,
y: mousePos.y - dragState.offset.y
};
setDragState(prev => ({
...prev,
currentPosition: newPosition
}));
};
const handleDragEnd = () => {
if (!dragState.isDragging || !dragState.draggedFrame) return;
// Snap to grid (optional - makes positioning cleaner)
const gridSize = 25;
const snappedPosition = {
x: Math.round(dragState.currentPosition.x / gridSize) * gridSize,
y: Math.round(dragState.currentPosition.y / gridSize) * gridSize
};
// Save the new position
setCustomPositions(prev => ({
...prev,
[dragState.draggedFrame!]: snappedPosition
}));
// Reset drag state
setDragState({
isDragging: false,
draggedFrame: null,
startPosition: { x: 0, y: 0 },
currentPosition: { x: 0, y: 0 },
offset: { x: 0, y: 0 }
});
};
// Reset positions to grid
const handleResetPositions = () => {
setCustomPositions({});
};
// Update connection positions based on current frame positions
const updateConnectionPositions = (connections: ConnectionLine[], files: DesignFile[]): ConnectionLine[] => {
return connections.map(connection => {
const fromIndex = files.findIndex(f => f.name === connection.fromFrame);
const toIndex = files.findIndex(f => f.name === connection.toFrame);
if (fromIndex === -1 || toIndex === -1) {
return connection; // Keep original if frame not found
}
// Get current positions (custom or calculated)
const fromPosition = getFramePosition(connection.fromFrame, fromIndex);
const toPosition = getFramePosition(connection.toFrame, toIndex);
// Get frame dimensions for connection point calculation
const fromViewport = getFrameViewport(connection.fromFrame);
const toViewport = getFrameViewport(connection.toFrame);
const fromDimensions = currentConfig.viewports[fromViewport];
const toDimensions = currentConfig.viewports[toViewport];
// Calculate connection points (center-right of from frame to center-left of to frame)
const fromConnectionPoint = {
x: fromPosition.x + fromDimensions.width,
y: fromPosition.y + (fromDimensions.height + 50) / 2 // +50 for header
};
const toConnectionPoint = {
x: toPosition.x,
y: toPosition.y + (toDimensions.height + 50) / 2 // +50 for header
};
return {
...connection,
fromPosition: fromConnectionPoint,
toPosition: toConnectionPoint
};
});
};
// Keyboard shortcuts for zoom
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && !e.shiftKey) {
switch (e.key) {
case '=':
case '+':
e.preventDefault();
handleZoomIn();
break;
case '-':
e.preventDefault();
handleZoomOut();
break;
case '0':
e.preventDefault();
handleResetZoom();
break;
}
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, []);
if (isLoading) {
return (
<div className="canvas-loading">
<div className="loading-spinner">
<div className="spinner"></div>
<p>Loading design files...</p>
</div>
</div>
);
}
if (error) {
return (
<div className="canvas-error">
<div className="error-message">
<h3>Error loading canvas</h3>
<p>{error}</p>
<button onClick={() => window.location.reload()}>
Retry
</button>
</div>
</div>
);
}
if (designFiles.length === 0) {
return (
<div className="canvas-empty">
<div className="empty-state">
<h3>No design files found in <code>.superdesign/design_iterations/</code></h3>
<p>Prompt Superdesign OR Cursor/Windsurf/Claude Code to design UI like <kbd>Help me design a calculator UI</kbd> and preview the UI here</p>
</div>
</div>
);
}
return (
<div className="canvas-container">
{/* Canvas Controls - Clean Minimal Design */}
<div className="canvas-toolbar">
{/* Navigation Section */}
<div className="toolbar-section">
<div className="control-group">
<button className="toolbar-btn zoom-btn" onClick={handleZoomOut} title="Zoom Out (Cmd/Ctrl + -)">
<ZoomOutIcon />
</button>
<div className="zoom-display">
<span className="zoom-value">{Math.round(currentZoom * 100)}%</span>
</div>
<button className="toolbar-btn zoom-btn" onClick={handleZoomIn} title="Zoom In (Cmd/Ctrl + +)">
<ZoomInIcon />
</button>
<div className="toolbar-divider"></div>
<button className="toolbar-btn" onClick={handleResetZoom} title="Reset Zoom (Cmd/Ctrl + 0)">
<HomeIcon />
</button>
<button className="toolbar-btn" onClick={handleResetPositions} title="Reset Frame Positions">
<RefreshIcon />
</button>
</div>
</div>
{/* Layout Section */}
<div className="toolbar-section">
<div className="control-group">
<div className="layout-toggle">
<button
className={`toggle-btn ${layoutMode === 'grid' ? 'active' : ''}`}
onClick={() => setLayoutMode('grid')}
title="Grid Layout"
>
<ScaleIcon />
</button>
<button
className={`toggle-btn ${layoutMode === 'hierarchy' ? 'active' : ''}`}
onClick={() => setLayoutMode('hierarchy')}
title="Hierarchy Layout"
disabled={!hierarchyTree || hierarchyTree.nodes.size === 0}
>
<TreeIcon />
</button>
</div>
{layoutMode === 'hierarchy' && (
<button
className={`toolbar-btn connection-btn ${showConnections ? 'active' : ''}`}
onClick={() => setShowConnections(!showConnections)}
title="Toggle Connection Lines"
>
<LinkIcon />
</button>
)}
</div>
</div>
{/* Viewport Section */}
<div className="toolbar-section">
<div className="control-group">
<button
className={`toolbar-btn viewport-mode-btn ${useGlobalViewport ? 'active' : ''}`}
onClick={toggleGlobalViewport}
title="Toggle Global Viewport Mode"
>
<GlobeIcon />
</button>
<div className="viewport-selector">
<button
className={`viewport-btn ${globalViewportMode === 'mobile' && useGlobalViewport ? 'active' : ''}`}
onClick={() => handleGlobalViewportChange('mobile')}
title="Mobile View (375×667)"
disabled={!useGlobalViewport}
>
<MobileIcon />
</button>
<button
className={`viewport-btn ${globalViewportMode === 'tablet' && useGlobalViewport ? 'active' : ''}`}
onClick={() => handleGlobalViewportChange('tablet')}
title="Tablet View (768×1024)"
disabled={!useGlobalViewport}
>
<TabletIcon />
</button>
<button
className={`viewport-btn ${globalViewportMode === 'desktop' && useGlobalViewport ? 'active' : ''}`}
onClick={() => handleGlobalViewportChange('desktop')}
title="Desktop View (1200×800)"
disabled={!useGlobalViewport}
>
<DesktopIcon />
</button>
</div>
</div>
</div>
</div>
{/* Infinite Canvas */}
<TransformWrapper
ref={transformRef}
initialScale={1}
minScale={0.1} // Lower min scale to prevent negative values
maxScale={3} // Higher max scale for more zoom range
limitToBounds={false}
smooth={false} // Disable smooth for better performance
disablePadding={true} // Disable padding to prevent position jumps
doubleClick={{
disabled: false,
mode: "zoomIn",
step: 50, // Moderate double-click zoom step
animationTime: 150 // Quick double-click zoom
}}
wheel={{
wheelDisabled: true, // Disable wheel zoom
touchPadDisabled: false, // Enable trackpad pan
step: 0.05 // Even smaller zoom steps
}}
panning={{
disabled: dragState.isDragging,
velocityDisabled: true, // Disable velocity for immediate response
wheelPanning: true // Enable trackpad panning
}}
pinch={{
disabled: false, // Keep pinch zoom enabled
step: 1 // Ultra-fine pinch steps
}}
centerOnInit={true}
onTransformed={(ref) => handleTransformChange(ref)}
onZoom={(ref) => {
const state = ref.state;
// Check for invalid scale and fix it
if (state.scale <= 0) {
console.error('🚨 ZOOM EVENT - Invalid scale:', state.scale, '- Fixing...');
ref.setTransform(state.positionX, state.positionY, 0.1);
return;
}
console.log('📏 ZOOM EVENT:', {
scale: state.scale,
positionX: state.positionX,
positionY: state.positionY,
event: 'onZoom'
});
}}
onPanning={(ref) => {
console.log('👆 PAN EVENT:', {
scale: ref.state.scale,
positionX: ref.state.positionX,
positionY: ref.state.positionY,
event: 'onPanning'
});
}}
onZoomStart={(ref) => {
console.log('🔍 ZOOM START:', {
scale: ref.state.scale,
positionX: ref.state.positionX,
positionY: ref.state.positionY,
event: 'onZoomStart'
});
}}
onZoomStop={(ref) => {
console.log('🔍 ZOOM STOP:', {
scale: ref.state.scale,
positionX: ref.state.positionX,
positionY: ref.state.positionY,
event: 'onZoomStop'
});
}}
>
<TransformComponent
wrapperClass="canvas-transform-wrapper"
contentClass="canvas-transform-content"
>
<div
className={`canvas-grid ${dragState.isDragging ? 'dragging' : ''}`}
onMouseMove={(e) => {
if (dragState.isDragging) {
const rect = e.currentTarget.getBoundingClientRect();
const mousePos = transformMouseToCanvasSpace(e.clientX, e.clientY, rect);
handleDragMove(mousePos);
}
}}
onMouseUp={handleDragEnd}
onMouseLeave={handleDragEnd}
onClick={(e) => {
// Clear selection when clicking on empty space
if (e.target === e.currentTarget) {
setSelectedFrames([]);
// Also clear context in chat
const clearContextMessage: WebviewMessage = {
command: 'setContextFromCanvas',
data: { fileName: '', type: 'clear' }
};
vscode.postMessage(clearContextMessage);
}
}}
>
{/* Connection Lines (render behind frames) */}
{layoutMode === 'hierarchy' && hierarchyTree && showConnections && (
<ConnectionLines
connections={updateConnectionPositions(hierarchyTree.connections, designFiles)}
containerBounds={hierarchyTree.bounds}
isVisible={showConnections}
zoomLevel={currentZoom}
/>
)}
{designFiles.map((file, index) => {
const frameViewport = getFrameViewport(file.name);
const viewportDimensions = currentConfig.viewports[frameViewport];
// Use actual viewport dimensions (add frame border/header space)
const actualWidth = viewportDimensions.width;
const actualHeight = viewportDimensions.height + 50; // Add space for header
// Get position (custom or default grid)
const position = getFramePosition(file.name, index);
// If this frame is being dragged, use current drag position
const finalPosition = dragState.isDragging && dragState.draggedFrame === file.name
? dragState.currentPosition
: position;
return (
<DesignFrame
key={file.name}
file={file}
position={finalPosition}
dimensions={{ width: actualWidth, height: actualHeight }}
isSelected={selectedFrames.includes(file.name)}
onSelect={handleFrameSelect}
renderMode={getOptimalRenderMode(currentZoom)}
viewport={frameViewport}
viewportDimensions={viewportDimensions}
onViewportChange={handleFrameViewportChange}
useGlobalViewport={useGlobalViewport}
onDragStart={handleDragStart}
isDragging={dragState.isDragging && dragState.draggedFrame === file.name}
nonce={nonce}
onSendToChat={handleSendToChat}
/>
);
})}
</div>
</TransformComponent>
</TransformWrapper>
</div>
);
};
export default CanvasView;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,132 @@
import React from 'react';
interface ColorPaletteProps {
colors: any;
isDarkMode: boolean;
}
const ColorPalette: React.FC<ColorPaletteProps> = ({ colors, isDarkMode }) => {
const handleColorCopy = (color: string) => {
navigator.clipboard.writeText(color);
};
const renderColorGroup = (groupName: string, colorGroup: any) => {
if (!colorGroup || Object.keys(colorGroup).length === 0) return null;
return (
<div key={groupName} className="color-group">
<h4 className="color-group-title">{groupName}</h4>
<div className="color-grid">
{Object.entries(colorGroup).map(([name, color]) => (
<div
key={name}
className="color-swatch"
onClick={() => handleColorCopy(color as string)}
title={`${name}: ${color}`}
>
<div
className="color-preview"
style={{ backgroundColor: color as string }}
/>
<div className="color-info">
<span className="color-name">{name}</span>
<span className="color-value">{color as string}</span>
</div>
</div>
))}
</div>
</div>
);
};
return (
<>
<style>
{`
.color-palette {
display: flex;
flex-direction: column;
gap: 16px;
}
.color-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.color-group-title {
font-size: 11px;
font-weight: 600;
color: var(--vscode-foreground);
margin: 0;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.color-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 8px;
}
.color-swatch {
display: flex;
align-items: center;
gap: 8px;
padding: 6px;
border: 1px solid var(--vscode-panel-border);
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
background: var(--vscode-editor-background);
}
.color-swatch:hover {
background: var(--vscode-list-hoverBackground);
}
.color-preview {
width: 20px;
height: 20px;
border-radius: 3px;
border: 1px solid var(--vscode-panel-border);
flex-shrink: 0;
}
.color-info {
display: flex;
flex-direction: column;
min-width: 0;
flex: 1;
}
.color-name {
font-size: 11px;
font-weight: 500;
color: var(--vscode-foreground);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.color-value {
font-size: 10px;
color: var(--vscode-descriptionForeground);
font-family: var(--vscode-editor-font-family);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
`}
</style>
<div className="color-palette">
{colors && Object.entries(colors).map(([groupName, colorGroup]) =>
renderColorGroup(groupName, colorGroup)
)}
</div>
</>
);
};
export default ColorPalette;

View File

@@ -0,0 +1,57 @@
import React from 'react';
interface ModeToggleProps {
isDarkMode: boolean;
onToggle: (isDarkMode: boolean) => void;
}
const ModeToggle: React.FC<ModeToggleProps> = ({ isDarkMode, onToggle }) => {
const handleToggle = () => {
onToggle(!isDarkMode);
};
return (
<>
<style>
{`
.mode-toggle {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 8px;
border: 1px solid var(--vscode-panel-border);
border-radius: 4px;
background: var(--vscode-editor-background);
cursor: pointer;
transition: all 0.2s;
}
.mode-toggle:hover {
background: var(--vscode-list-hoverBackground);
}
.mode-toggle-icon {
font-size: 12px;
line-height: 1;
}
.mode-toggle-text {
font-size: 11px;
font-weight: 500;
color: var(--vscode-foreground);
}
`}
</style>
<div className="mode-toggle" onClick={handleToggle}>
<span className="mode-toggle-icon">
{isDarkMode ? '🌙' : '☀️'}
</span>
<span className="mode-toggle-text">
{isDarkMode ? 'Dark' : 'Light'}
</span>
</div>
</>
);
};
export default ModeToggle;

View File

@@ -0,0 +1,438 @@
import React, { useState, useRef, useEffect } from 'react';
import { BrainIcon } from '../Icons';
interface ModelSelectorProps {
selectedModel: string;
onModelChange: (model: string) => void;
disabled?: boolean;
}
interface ModelOption {
id: string;
name: string;
provider: string;
category: string;
}
const ModelSelector: React.FC<ModelSelectorProps> = ({ selectedModel, onModelChange, disabled }) => {
const [isOpen, setIsOpen] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0 });
const triggerRef = useRef<HTMLButtonElement>(null);
const modalRef = useRef<HTMLDivElement>(null);
const models: ModelOption[] = [
// Anthropic
{ id: 'claude-4-opus-20250514', name: 'Claude 4 Opus', provider: 'Anthropic', category: 'Premium' },
{ id: 'claude-4-sonnet-20250514', name: 'Claude 4 Sonnet', provider: 'Anthropic', category: 'Balanced' },
{ id: 'claude-3-7-sonnet-20250219', name: 'Claude 3.7 Sonnet', provider: 'Anthropic', category: 'Balanced' },
{ id: 'claude-3-5-sonnet-20241022', name: 'Claude 3.5 Sonnet', provider: 'Anthropic', category: 'Balanced' },
// Google (OpenRouter)
{ id: 'google/gemini-2.5-pro', name: 'Gemini 2.5 Pro', provider: 'OpenRouter (Google)', category: 'Balanced' },
// Meta (OpenRouter)
{ id: 'meta-llama/llama-4-maverick-17b-128e-instruct', name: 'Llama 4 Maverick 17B', provider: 'OpenRouter (Meta)', category: 'Balanced' },
// DeepSeek (OpenRouter)
{ id: 'deepseek/deepseek-r1', name: 'DeepSeek R1', provider: 'OpenRouter (DeepSeek)', category: 'Balanced' },
// Mistral (OpenRouter)
{ id: 'mistralai/mistral-small-3.2-24b-instruct-2506', name: 'Mistral Small 3.2 24B', provider: 'OpenRouter (Mistral)', category: 'Balanced' },
// xAI (OpenRouter)
{ id: 'x-ai/grok-3', name: 'Grok 3', provider: 'OpenRouter (xAI)', category: 'Balanced' },
// Qwen (OpenRouter)
{ id: 'qwen/qwen3-235b-a22b-04-28', name: 'Qwen3 235B', provider: 'OpenRouter (Qwen)', category: 'Balanced' },
// Perplexity (OpenRouter)
{ id: 'perplexity/sonar-reasoning-pro', name: 'Sonar Reasoning Pro', provider: 'OpenRouter (Perplexity)', category: 'Balanced' },
// Microsoft (OpenRouter)
{ id: 'microsoft/phi-4-reasoning-plus-04-30', name: 'Phi-4 Reasoning Plus', provider: 'OpenRouter (Microsoft)', category: 'Balanced' },
// NVIDIA (OpenRouter)
{ id: 'nvidia/llama-3.3-nemotron-super-49b-v1', name: 'Llama 3.3 Nemotron Super 49B', provider: 'OpenRouter (NVIDIA)', category: 'Balanced' },
// Cohere (OpenRouter)
{ id: 'cohere/command-a-03-2025', name: 'Command A', provider: 'OpenRouter (Cohere)', category: 'Balanced' },
// Amazon (OpenRouter)
{ id: 'amazon/nova-pro-v1', name: 'Nova Pro', provider: 'OpenRouter (Amazon)', category: 'Balanced' },
// Inflection (OpenRouter)
{ id: 'inflection/inflection-3-productivity', name: 'Inflection 3 Productivity', provider: 'OpenRouter (Inflection)', category: 'Balanced' },
// Reka (OpenRouter)
{ id: 'rekaai/reka-flash-3', name: 'Reka Flash 3', provider: 'OpenRouter (Reka)', category: 'Balanced' },
// Existing OpenAI (direct)
{ id: 'openai-auto', name: 'AUTO', provider: 'OpenAI', category: 'Auto' },
{ id: 'gpt-4.1', name: 'GPT-4.1', provider: 'OpenAI', category: 'Balanced' },
{ id: 'gpt-4.1-mini', name: 'GPT-4.1 Mini', provider: 'OpenAI', category: 'Fast' }
];
const filteredModels = models.filter(model =>
model.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
model.provider.toLowerCase().includes(searchTerm.toLowerCase())
);
const selectedModelName = models.find(m => m.id === selectedModel)?.name || selectedModel;
const calculateDropdownPosition = () => {
if (!triggerRef.current) return;
const triggerRect = triggerRef.current.getBoundingClientRect();
const modalHeight = 190; // Reduced from 220 since we removed add models section
const modalWidth = 240;
const padding = 8;
// Calculate vertical position (above the trigger)
let top = triggerRect.top - modalHeight - padding;
// If there's not enough space above, show below
if (top < padding) {
top = triggerRect.bottom + padding;
}
// Calculate horizontal position (align with trigger)
let left = triggerRect.left;
// Ensure modal doesn't go off-screen horizontally
const rightEdge = left + modalWidth;
if (rightEdge > window.innerWidth - padding) {
left = window.innerWidth - modalWidth - padding;
}
if (left < padding) {
left = padding;
}
setDropdownPosition({ top, left });
};
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (modalRef.current && !modalRef.current.contains(event.target as Node) &&
triggerRef.current && !triggerRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
const handleScroll = () => {
if (isOpen) {
calculateDropdownPosition();
}
};
const handleResize = () => {
if (isOpen) {
calculateDropdownPosition();
}
};
if (isOpen) {
calculateDropdownPosition();
document.addEventListener('mousedown', handleClickOutside);
window.addEventListener('scroll', handleScroll, true);
window.addEventListener('resize', handleResize);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
window.removeEventListener('scroll', handleScroll, true);
window.removeEventListener('resize', handleResize);
};
}, [isOpen]);
const handleModelSelect = (modelId: string) => {
onModelChange(modelId);
setIsOpen(false);
};
const handleToggleOpen = () => {
if (!isOpen) {
calculateDropdownPosition();
}
setIsOpen(!isOpen);
};
return (
<>
<style>
{`
.model-selector-wrapper {
position: relative;
display: inline-block;
}
.model-selector-trigger {
background: transparent;
color: var(--vscode-foreground);
border: none;
outline: none;
font-size: 11px;
font-family: inherit;
cursor: pointer;
padding: 2px 20px 2px 6px;
border-radius: 4px;
transition: background-color 0.2s ease;
min-width: 120px;
white-space: nowrap;
position: relative;
display: flex;
align-items: center;
gap: 3px;
}
.model-selector-trigger:hover:not(:disabled) {
background: var(--vscode-list-hoverBackground);
}
.model-selector-trigger:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.model-selector-arrow {
position: absolute;
right: 6px;
top: 50%;
transform: translateY(-50%);
transition: transform 0.2s ease;
color: var(--vscode-descriptionForeground);
}
.model-selector-arrow.open {
transform: translateY(-50%) rotate(180deg);
}
.model-selector-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: transparent;
z-index: 1000;
pointer-events: none;
}
.model-selector-content {
position: absolute;
background: var(--vscode-dropdown-background);
border: 1px solid var(--vscode-dropdown-border);
border-radius: 4px;
width: 240px;
max-height: 190px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
overflow: hidden;
pointer-events: auto;
}
.model-selector-header {
padding: 6px 8px;
border-bottom: 1px solid var(--vscode-dropdown-border);
}
.model-selector-search {
width: 100%;
background: var(--vscode-input-background);
border: 1px solid var(--vscode-input-border);
border-radius: 3px;
padding: 6px 8px;
color: var(--vscode-input-foreground);
font-size: 11px;
outline: none;
box-sizing: border-box;
}
.model-selector-search:focus {
border-color: var(--vscode-focusBorder);
}
.model-selector-search::placeholder {
color: var(--vscode-input-placeholderForeground);
}
.model-selector-list {
max-height: 150px;
overflow-y: auto;
overflow-x: hidden;
}
.model-selector-list::-webkit-scrollbar {
width: 6px;
}
.model-selector-list::-webkit-scrollbar-track {
background: transparent;
}
.model-selector-list::-webkit-scrollbar-thumb {
background: var(--vscode-scrollbarSlider-background);
border-radius: 3px;
}
.model-selector-list::-webkit-scrollbar-thumb:hover {
background: var(--vscode-scrollbarSlider-hoverBackground);
}
.model-option {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 8px 4px 8px;
cursor: pointer;
transition: background-color 0.2s ease;
border: none;
background: none;
width: 100%;
text-align: left;
box-sizing: border-box;
min-height: 28px;
position: relative;
}
.model-option:hover {
background: var(--vscode-list-hoverBackground);
}
.model-option.selected {
background: var(--vscode-list-activeSelectionBackground) !important;
color: var(--vscode-list-activeSelectionForeground);
}
.model-option.selected:hover {
background: var(--vscode-list-activeSelectionBackground) !important;
}
.model-icon {
flex-shrink: 0;
color: var(--vscode-descriptionForeground);
display: flex;
align-items: center;
justify-content: center;
width: 12px;
height: 12px;
}
.model-option.selected .model-icon {
color: var(--vscode-list-activeSelectionForeground);
}
.model-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
justify-content: center;
}
.model-name {
font-size: 11px;
font-weight: 500;
color: var(--vscode-foreground);
margin: 0;
line-height: 1.2;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.model-option.selected .model-name {
color: var(--vscode-list-activeSelectionForeground);
}
.model-provider {
font-size: 9px;
color: var(--vscode-descriptionForeground);
margin: 0;
line-height: 1.1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.model-option.selected .model-provider {
color: var(--vscode-list-activeSelectionForeground);
opacity: 0.8;
}
.model-check {
flex-shrink: 0;
color: var(--vscode-list-activeSelectionForeground);
font-size: 11px;
width: 12px;
height: 12px;
display: flex;
align-items: center;
justify-content: center;
}
`}
</style>
<div className="model-selector-wrapper">
<button
ref={triggerRef}
className="model-selector-trigger"
onClick={handleToggleOpen}
disabled={disabled}
>
<div className="selector-icon model-icon">
<BrainIcon />
</div>
<span>{selectedModelName}</span>
<svg
className={`model-selector-arrow ${isOpen ? 'open' : ''}`}
width="12"
height="12"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
d="m6 12 4-4 4 4"
/>
</svg>
</button>
{isOpen && (
<div className="model-selector-modal">
<div
className="model-selector-content"
ref={modalRef}
style={{
top: dropdownPosition.top,
left: dropdownPosition.left
}}
>
<div className="model-selector-header">
<input
type="text"
className="model-selector-search"
placeholder="Search models..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
autoFocus
/>
</div>
<div className="model-selector-list">
{filteredModels.map((model) => (
<button
key={model.id}
className={`model-option ${model.id === selectedModel ? 'selected' : ''}`}
onClick={() => handleModelSelect(model.id)}
>
<div className="model-icon">
<BrainIcon />
</div>
<div className="model-info">
<div className="model-name">{model.name}</div>
<div className="model-provider">{model.provider}</div>
</div>
{model.id === selectedModel && (
<div className="model-check"></div>
)}
</button>
))}
</div>
</div>
</div>
)}
</div>
</>
);
};
export default ModelSelector;

View File

@@ -0,0 +1,165 @@
import React from 'react';
import { ParsedTheme } from '../../utils/themeParser';
interface ThemeDetailsProps {
reasoning: string;
theme: ParsedTheme | null;
}
const ThemeDetails: React.FC<ThemeDetailsProps> = ({ reasoning, theme }) => {
if (!theme) {
return null;
}
const styles = {
section: {
marginBottom: '1.5rem'
},
sectionTitle: {
margin: '0 0 0.75rem 0',
fontSize: '16px',
fontWeight: '600' as const,
color: 'var(--foreground)',
display: 'flex',
alignItems: 'center',
gap: '0.5rem'
},
reasoningText: {
fontSize: '14px',
lineHeight: '1.5',
color: 'var(--foreground)',
margin: 0,
whiteSpace: 'pre-wrap' as const
},
metadataGrid: {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
gap: '1rem'
},
metadataItem: {
display: 'flex',
flexDirection: 'column' as const,
gap: '0.25rem'
},
metadataLabel: {
fontSize: '12px',
fontWeight: '500' as const,
color: 'var(--muted-foreground)',
textTransform: 'uppercase' as const,
letterSpacing: '0.05em'
},
metadataValue: {
fontSize: '14px',
color: 'var(--foreground)',
fontFamily: 'var(--font-mono)',
padding: '0.25rem 0.5rem',
backgroundColor: 'var(--muted)',
borderRadius: '0.25rem',
border: '1px solid var(--border)'
},
shadowsList: {
display: 'flex',
flexDirection: 'column' as const,
gap: '0.5rem'
},
shadowItem: {
display: 'flex',
alignItems: 'center',
gap: '0.75rem',
fontSize: '12px'
},
shadowPreview: {
width: '3rem',
height: '1.5rem',
backgroundColor: 'var(--card)',
borderRadius: '0.25rem',
border: '1px solid var(--border)'
},
shadowLabel: {
fontFamily: 'var(--font-mono)',
color: 'var(--muted-foreground)',
minWidth: '4rem'
}
};
const shadowEntries = Object.entries(theme.shadows).filter(([_, value]) => value && value !== 'none');
const hasShadows = shadowEntries.length > 0;
return (
<div>
{reasoning && (
<div style={styles.section}>
<h4 style={styles.sectionTitle}>
<span>💭</span>
<span>Design Reasoning</span>
</h4>
<p style={styles.reasoningText}>{reasoning}</p>
</div>
)}
<div style={styles.section}>
<h4 style={styles.sectionTitle}>
<span></span>
<span>Theme Properties</span>
</h4>
<div style={styles.metadataGrid}>
<div style={styles.metadataItem}>
<span style={styles.metadataLabel}>Font Family (Sans)</span>
<span style={styles.metadataValue}>{theme.fonts.sans}</span>
</div>
<div style={styles.metadataItem}>
<span style={styles.metadataLabel}>Font Family (Mono)</span>
<span style={styles.metadataValue}>{theme.fonts.mono}</span>
</div>
<div style={styles.metadataItem}>
<span style={styles.metadataLabel}>Border Radius</span>
<span style={styles.metadataValue}>{theme.radius}</span>
</div>
<div style={styles.metadataItem}>
<span style={styles.metadataLabel}>Base Spacing</span>
<span style={styles.metadataValue}>{theme.spacing}</span>
</div>
<div style={styles.metadataItem}>
<span style={styles.metadataLabel}>Mode Support</span>
<span style={styles.metadataValue}>
{theme.darkMode ? 'Light + Dark' : 'Light Only'}
</span>
</div>
<div style={styles.metadataItem}>
<span style={styles.metadataLabel}>Total Variables</span>
<span style={styles.metadataValue}>
{Object.keys(theme.variables).length}
</span>
</div>
</div>
</div>
{hasShadows && (
<div style={styles.section}>
<h4 style={styles.sectionTitle}>
<span>🌫</span>
<span>Shadow System</span>
</h4>
<div style={styles.shadowsList}>
{shadowEntries.map(([name, value]) => (
<div key={name} style={styles.shadowItem}>
<div
style={{
...styles.shadowPreview,
boxShadow: value
}}
/>
<span style={styles.shadowLabel}>{name}:</span>
<span style={{fontSize: '12px', color: 'var(--muted-foreground)', fontFamily: 'var(--font-mono)'}}>
{value}
</span>
</div>
))}
</div>
</div>
)}
</div>
);
};
export default ThemeDetails;

View File

@@ -0,0 +1,580 @@
import React, { useEffect, useRef } from 'react';
interface ThemePreviewProps {
theme: any;
isDarkMode: boolean;
cssSheet: string;
}
// Google Fonts that we support
const SUPPORTED_GOOGLE_FONTS = [
'JetBrains Mono',
'Fira Code',
'Source Code Pro',
'IBM Plex Mono',
'Roboto Mono',
'Space Mono',
'Geist Mono',
'Inter',
'Roboto',
'Open Sans',
'Poppins',
'Montserrat',
'Outfit',
'Plus Jakarta Sans',
'DM Sans',
'Geist',
'Oxanium',
'Architects Daughter',
'Merriweather',
'Playfair Display',
'Lora',
'Source Serif Pro',
'Libre Baskerville',
'Space Grotesk'
];
// System fonts that should not be loaded from Google Fonts
const SYSTEM_FONTS = [
'system-ui',
'sans-serif',
'serif',
'monospace',
'cursive',
'fantasy',
'ui-sans-serif',
'ui-serif',
'ui-monospace',
'ui-rounded',
'Arial',
'Helvetica',
'Times',
'Times New Roman',
'Courier',
'Courier New',
'Georgia',
'Verdana',
'Tahoma',
'Trebuchet MS',
'Impact',
'Comic Sans MS',
'MS Sans Serif',
'MS Serif',
'Pixelated MS Sans Serif'
];
// Extract font families from CSS variables
const extractFontsFromCSS = (cssSheet: string): string[] => {
const fonts = new Set<string>();
// Look for font-family declarations
const fontRegex = /--font-[^:]*:\s*["']?([^"';,]+)/g;
let match;
while ((match = fontRegex.exec(cssSheet)) !== null) {
const fontName = match[1].trim();
// Skip system fonts and empty values
if (fontName && !SYSTEM_FONTS.includes(fontName)) {
fonts.add(fontName);
}
}
return Array.from(fonts);
};
// Load Google Fonts dynamically
const loadGoogleFonts = (fontNames: string[]): Promise<void> => {
if (fontNames.length === 0) return Promise.resolve();
return new Promise((resolve) => {
try {
// Check if we already have a Google Fonts link
const existingLink = document.querySelector('link[href*="fonts.googleapis.com"]') as HTMLLinkElement;
// Convert font names to Google Fonts URL format
const fontParams = fontNames.map(name => {
try {
const urlName = name.replace(/\s+/g, '+');
// Load multiple weights for better coverage
return `${urlName}:300,400,500,600,700`;
} catch (error) {
console.warn(`Failed to process font name: ${name}`, error);
return null;
}
}).filter(Boolean).join('&family=');
// If no valid fonts to load, just resolve
if (!fontParams) {
resolve();
return;
}
const fontUrl = `https://fonts.googleapis.com/css2?family=${fontParams}&display=swap`;
if (existingLink) {
existingLink.href = fontUrl;
existingLink.onload = () => resolve();
existingLink.onerror = (error) => {
console.warn('Failed to load Google Fonts (existing link):', fontNames, error);
resolve(); // Continue even if fonts fail to load
};
} else {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = fontUrl;
link.onload = () => resolve();
link.onerror = (error) => {
console.warn('Failed to load Google Fonts (new link):', fontNames, error);
resolve(); // Continue even if fonts fail to load
};
document.head.appendChild(link);
}
// Fallback timeout - resolve after 2 seconds even if fonts haven't loaded
setTimeout(() => {
console.warn('Google Fonts loading timeout for:', fontNames);
resolve();
}, 2000);
} catch (error) {
console.warn('Error in loadGoogleFonts:', error);
resolve(); // Always resolve, never reject
}
});
};
const ThemePreview: React.FC<ThemePreviewProps> = ({ theme, isDarkMode, cssSheet }) => {
const previewRef = useRef<HTMLDivElement>(null);
const styleRef = useRef<HTMLStyleElement | null>(null);
const fontsLoadedRef = useRef<boolean>(false);
useEffect(() => {
if (!cssSheet || !previewRef.current) return;
const setupPreview = async () => {
// Remove existing style element first
if (styleRef.current) {
styleRef.current.remove();
}
// Create new style element with the actual CSS immediately
const styleElement = document.createElement('style');
styleElement.textContent = `
${cssSheet}
.theme-preview-live {
background: var(--background);
color: var(--foreground);
font-family: var(--font-sans, 'Inter', system-ui, sans-serif);
border-radius: var(--radius);
overflow: hidden;
min-height: 400px;
border: 1px solid var(--border);
}
.theme-preview-live.dark {
/* Dark mode will use the .dark selector variables from cssSheet */
}
.theme-preview-live * {
box-sizing: border-box;
}
.theme-preview-live .preview-nav {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.5rem;
background: var(--sidebar);
color: var(--sidebar-foreground);
border-bottom: 1px solid var(--sidebar-border);
}
.theme-preview-live .nav-brand {
font-size: 1.25rem;
font-weight: 600;
color: var(--sidebar-primary);
font-family: var(--font-sans, 'Inter', system-ui, sans-serif);
}
.theme-preview-live .nav-links {
display: flex;
gap: 0.5rem;
}
.theme-preview-live .nav-link {
color: var(--sidebar-foreground);
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: var(--radius);
font-size: 0.875rem;
transition: all 0.2s;
font-family: var(--font-sans, 'Inter', system-ui, sans-serif);
}
.theme-preview-live .nav-link:hover {
/* Fallback: subtle opacity change */
background: rgba(255, 255, 255, 0.1);
/* Modern approach with color-mix */
background: color-mix(in srgb, var(--sidebar-foreground) 15%, var(--sidebar) 85%);
color: var(--sidebar-foreground);
}
.theme-preview-live .nav-link.active {
background: var(--sidebar-primary);
color: var(--sidebar-primary-foreground);
}
.theme-preview-live .preview-card {
background: var(--card);
color: var(--card-foreground);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1.5rem;
margin: 1rem;
box-shadow: var(--shadow);
transition: all 0.2s;
}
.theme-preview-live .preview-card:hover {
/* Fallback: enhanced existing shadow */
transform: translateY(-1px);
box-shadow: var(--shadow), 0 4px 12px rgba(0, 0, 0, 0.1);
/* Modern approach with color-mix */
border-color: color-mix(in srgb, var(--border) 60%, var(--foreground) 40%);
box-shadow: var(--shadow), 0 4px 12px color-mix(in srgb, var(--foreground) 10%, transparent 90%);
transform: none;
}
.theme-preview-live .preview-card h3 {
margin: 0 0 0.5rem 0;
color: var(--card-foreground);
font-size: 1.25rem;
font-weight: 600;
font-family: var(--font-sans, 'Inter', system-ui, sans-serif);
}
.theme-preview-live .text-muted {
color: var(--muted-foreground);
margin: 0 0 1rem 0;
line-height: 1.5;
font-family: var(--font-sans, 'Inter', system-ui, sans-serif);
}
.theme-preview-live .card-actions {
display: flex;
gap: 0.5rem;
margin-top: 1rem;
flex-wrap: wrap;
}
.theme-preview-live .preview-buttons {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
margin: 1rem;
}
.theme-preview-live .btn {
padding: 0.5rem 1rem;
border-radius: var(--radius);
border: 1px solid var(--border);
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
font-size: 0.875rem;
font-family: var(--font-sans, 'Inter', system-ui, sans-serif);
}
.theme-preview-live .btn-primary {
background: var(--primary);
color: var(--primary-foreground);
border-color: var(--primary);
}
.theme-preview-live .btn-primary:hover {
/* Fallback: filter approach */
filter: brightness(0.85) saturate(1.1);
/* Modern approach with color-mix */
background: color-mix(in srgb, var(--primary) 70%, black 30%);
border-color: color-mix(in srgb, var(--primary) 70%, black 30%);
filter: none;
}
.theme-preview-live .btn-secondary {
background: var(--secondary);
color: var(--secondary-foreground);
border-color: var(--secondary);
}
.theme-preview-live .btn-secondary:hover {
/* Fallback: filter approach */
filter: brightness(0.9) contrast(1.1);
/* Modern approach with color-mix */
background: color-mix(in srgb, var(--secondary) 70%, var(--foreground) 30%);
border-color: color-mix(in srgb, var(--secondary) 70%, var(--foreground) 30%);
filter: none;
}
.theme-preview-live .btn-destructive {
background: var(--destructive);
color: var(--destructive-foreground);
border-color: var(--destructive);
}
.theme-preview-live .btn-destructive:hover {
/* Fallback: filter approach */
filter: brightness(0.85) saturate(1.1);
/* Modern approach with color-mix */
background: color-mix(in srgb, var(--destructive) 70%, black 30%);
border-color: color-mix(in srgb, var(--destructive) 70%, black 30%);
filter: none;
}
.theme-preview-live .preview-form {
display: flex;
flex-direction: column;
gap: 1rem;
padding: 1rem;
margin: 1rem;
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow);
}
.theme-preview-live .preview-form h3 {
margin: 0 0 1rem 0;
color: var(--card-foreground);
font-size: 1.25rem;
font-weight: 600;
font-family: var(--font-sans, 'Inter', system-ui, sans-serif);
}
.theme-preview-live .input {
padding: 0.75rem;
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--input);
color: var(--foreground);
font-size: 0.875rem;
font-family: var(--font-sans, 'Inter', system-ui, sans-serif);
width: 100%;
}
.theme-preview-live .input:hover {
/* Fallback: slight opacity overlay */
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.2);
/* Modern approach with color-mix */
border-color: color-mix(in srgb, var(--border) 50%, var(--foreground) 50%);
box-shadow: none;
}
.theme-preview-live .input:focus {
outline: 2px solid var(--ring);
outline-offset: 2px;
border-color: var(--ring);
}
.theme-preview-live .input::placeholder {
color: var(--muted-foreground);
}
.theme-preview-live .code-sample {
font-family: var(--font-mono, 'JetBrains Mono', 'Fira Code', monospace);
background: var(--muted);
color: var(--muted-foreground);
padding: 0.75rem;
border-radius: var(--radius);
font-size: 0.8rem;
border: 1px solid var(--border);
margin: 0.5rem 0;
}
.theme-preview-live .serif-text {
font-family: var(--font-serif, 'Merriweather', Georgia, serif);
font-style: italic;
color: var(--muted-foreground);
}
.theme-preview-live .serif-heading {
font-family: var(--font-serif, 'Merriweather', Georgia, serif);
font-size: 1.5rem;
font-weight: 400;
color: var(--foreground);
margin: 0 0 0.5rem 0;
line-height: 1.3;
}
.theme-preview-live .serif-quote {
font-family: var(--font-serif, 'Merriweather', Georgia, serif);
font-size: 1.1rem;
font-style: italic;
color: var(--muted-foreground);
border-left: 3px solid var(--primary);
padding-left: 1rem;
margin: 1rem 0;
line-height: 1.6;
}
.theme-preview-live .mono-terminal {
font-family: var(--font-mono, 'JetBrains Mono', 'Fira Code', monospace);
background: var(--muted);
color: var(--foreground);
padding: 1rem;
border-radius: var(--radius);
font-size: 0.85rem;
border: 1px solid var(--border);
margin: 0.5rem 0;
white-space: pre;
}
.theme-preview-live .mono-data {
font-family: var(--font-mono, 'JetBrains Mono', 'Fira Code', monospace);
font-size: 0.8rem;
color: var(--muted-foreground);
background: var(--card);
padding: 0.5rem;
border-radius: var(--radius);
border: 1px solid var(--border);
}
`;
document.head.appendChild(styleElement);
styleRef.current = styleElement;
// Load Google Fonts asynchronously (non-blocking)
const loadFonts = async () => {
const requiredFonts = extractFontsFromCSS(cssSheet);
if (requiredFonts.length > 0 && !fontsLoadedRef.current) {
try {
await loadGoogleFonts(requiredFonts);
fontsLoadedRef.current = true;
} catch (error) {
console.warn('Failed to load Google Fonts:', error);
// Continue without fonts rather than blocking
}
}
};
// Start font loading in background
loadFonts();
};
setupPreview();
// Cleanup on unmount
return () => {
if (styleRef.current) {
styleRef.current.remove();
}
};
}, [cssSheet, isDarkMode]);
const containerStyles = {
border: '1px solid var(--vscode-panel-border)',
borderRadius: '4px',
overflow: 'hidden',
backgroundColor: 'var(--vscode-editor-background)'
};
const contentStyles = {
padding: '16px',
minHeight: '400px'
};
if (!theme) {
return (
<div style={containerStyles}>
<div style={{...contentStyles, display: 'flex', alignItems: 'center', justifyContent: 'center'}}>
<span style={{color: 'var(--vscode-descriptionForeground)', fontSize: '12px'}}>No theme data available</span>
</div>
</div>
);
}
const sampleHTML = `
<nav class="preview-nav">
<div class="nav-brand">MyApp</div>
<div class="nav-links">
<a href="#" class="nav-link active">Home</a>
<a href="#" class="nav-link">About</a>
<a href="#" class="nav-link">Contact</a>
<a href="#" class="nav-link">Blog</a>
</div>
</nav>
<div class="preview-card">
<h1 class="serif-heading">Design Philosophy</h1>
<p class="text-muted">This showcase demonstrates how different font families work together to create a cohesive design system.</p>
<div class="serif-quote">
"Typography is the craft of endowing human language with a durable visual form."
</div>
<div class="code-sample">const theme = {
fonts: {
sans: "Inter, system-ui",
serif: "Merriweather, Georgia",
mono: "JetBrains Mono, monospace"
}
}</div>
<div class="card-actions">
<button class="btn btn-primary">Primary Action</button>
<button class="btn btn-secondary">Secondary</button>
</div>
</div>
<div class="preview-card">
<h3>Terminal Output</h3>
<div class="mono-terminal">$ npm run build
✓ Built successfully
Output: 2.3 MB (gzipped: 847 KB)
$ git status
On branch main
Your branch is up to date</div>
<div class="mono-data">
API Response: 200 OK
Content-Type: application/json
Cache-Control: max-age=3600
X-Rate-Limit: 1000/hour
</div>
</div>
<div class="preview-card">
<h2 class="serif-heading">Article Preview</h2>
<p class="serif-text">This is an example of how serif fonts can be used for longer form content, providing excellent readability and a classic, editorial feeling that works well for articles, blog posts, and documentation.</p>
<p class="text-muted">Meanwhile, sans-serif fonts remain perfect for UI elements, navigation, and general interface text where clarity and modern aesthetics are prioritized.</p>
</div>
<div class="preview-form">
<h3>Contact Form</h3>
<input class="input" placeholder="Enter your name" />
<input class="input" placeholder="Email address" type="email" />
<textarea class="input" placeholder="Your message..." rows="3"></textarea>
<div class="card-actions">
<button class="btn btn-primary">Submit</button>
<button class="btn btn-secondary">Cancel</button>
</div>
</div>
`;
return (
<div style={containerStyles}>
<div style={contentStyles}>
<div
ref={previewRef}
className={`theme-preview-live ${isDarkMode ? 'dark' : ''}`}
dangerouslySetInnerHTML={{ __html: sampleHTML }}
/>
</div>
</div>
);
};
export default ThemePreview;

View File

@@ -0,0 +1,419 @@
import React, { useState, useEffect } from 'react';
import ThemePreviewHeader from './ThemePreviewHeader';
import ColorPalette from './ColorPalette';
import ThemePreview from './ThemePreview';
import ModeToggle from './ModeToggle';
import { parseThemeCSS, extractColorPalette, type ParsedTheme } from '../../utils/themeParser';
interface ThemePreviewCardProps {
themeName: string;
reasoning?: string;
cssSheet?: string | null;
cssFilePath?: string | null;
isLoading?: boolean;
vscode?: any;
}
const ThemePreviewCard: React.FC<ThemePreviewCardProps> = ({
themeName,
reasoning,
cssSheet,
cssFilePath,
isLoading = false,
vscode
}) => {
const [isExpanded, setIsExpanded] = useState(false);
const [parsedTheme, setParsedTheme] = useState<ParsedTheme | null>(null);
const [activeTab, setActiveTab] = useState<'theme' | 'components'>('theme');
const [isDarkMode, setIsDarkMode] = useState(false);
const [currentCssContent, setCurrentCssContent] = useState<string>('');
const [isLoadingCss, setIsLoadingCss] = useState(false);
const [cssLoadError, setCssLoadError] = useState<string | null>(null);
// Pre-inject minimal CSS to avoid FOUC (Flash of Unstyled Content)
useEffect(() => {
const minimalCssId = 'theme-preview-minimal-css';
let existingStyle = document.getElementById(minimalCssId);
if (!existingStyle) {
const minimalStyle = document.createElement('style');
minimalStyle.id = minimalCssId;
minimalStyle.textContent = `
.theme-preview-live {
background: var(--vscode-editor-background);
color: var(--vscode-editor-foreground);
font-family: var(--vscode-font-family);
border-radius: 4px;
overflow: hidden;
min-height: 400px;
border: 1px solid var(--vscode-panel-border);
}
`;
document.head.appendChild(minimalStyle);
}
// Cleanup on unmount
return () => {
const styleToRemove = document.getElementById(minimalCssId);
if (styleToRemove) {
document.head.removeChild(styleToRemove);
}
};
}, []);
// Set initial loading state immediately when cssFilePath is provided
useEffect(() => {
if (cssFilePath && vscode) {
setIsLoadingCss(true);
setIsExpanded(true); // Auto-expand to show loading state
}
}, [cssFilePath, vscode]);
// Load CSS from file if cssFilePath is provided
useEffect(() => {
const loadCssFromFile = async () => {
// Reset states
setCssLoadError(null);
if (cssFilePath && vscode) {
try {
// Request CSS file content from extension
const response = await new Promise<string>((resolve, reject) => {
const timeoutId = setTimeout(() => {
reject(new Error('Timeout loading CSS file'));
}, 3000); // Reduced timeout
const handler = (event: MessageEvent) => {
const message = event.data;
if (message.command === 'cssFileContentResponse' && message.filePath === cssFilePath) {
clearTimeout(timeoutId);
window.removeEventListener('message', handler);
if (message.error) {
reject(new Error(message.error));
} else {
resolve(message.content);
}
}
};
window.addEventListener('message', handler);
// Request CSS file content
vscode.postMessage({
command: 'getCssFileContent',
filePath: cssFilePath
});
});
setCurrentCssContent(response);
setIsExpanded(true); // Auto-expand when CSS loads successfully
} catch (error) {
console.warn('Failed to load CSS from file, falling back to cssSheet:', error);
setCssLoadError(error instanceof Error ? error.message : 'Failed to load CSS');
setCurrentCssContent(cssSheet || '');
} finally {
setIsLoadingCss(false);
}
} else if (cssSheet) {
setCurrentCssContent(cssSheet);
setIsExpanded(true); // Auto-expand when CSS is available
}
};
loadCssFromFile();
}, [cssFilePath, cssSheet, vscode]);
// Parse CSS when content is available
useEffect(() => {
if (currentCssContent && !isLoadingCss) {
try {
const theme = parseThemeCSS(currentCssContent);
setParsedTheme(theme);
} catch (error) {
console.error('Failed to parse theme:', error);
setCssLoadError('Failed to parse theme CSS');
}
}
}, [currentCssContent, isLoadingCss]);
const handleCopyCSS = () => {
if (currentCssContent) {
navigator.clipboard.writeText(currentCssContent);
}
};
const handleToggleExpanded = () => {
setIsExpanded(!isExpanded);
};
// Convert parsed theme to grouped colors format
const getGroupedColors = (theme: ParsedTheme) => {
const palette = extractColorPalette(theme);
return palette.reduce((acc, color) => {
if (!acc[color.category]) {
acc[color.category] = {};
}
acc[color.category][color.name] = color.value;
return acc;
}, {} as Record<string, Record<string, string>>);
};
// Show component if we're loading, have a theme, or have an error to display
if (!parsedTheme && !isLoading && !isLoadingCss && !cssLoadError) {
return null;
}
return (
<>
<style>
{`
.theme-preview-tabs {
display: flex;
border-bottom: 1px solid var(--vscode-panel-border);
background: var(--vscode-sideBar-background);
}
.theme-preview-tab {
padding: 8px 12px;
background: transparent;
border: none;
color: var(--vscode-foreground);
cursor: pointer;
font-size: 11px;
font-weight: 500;
opacity: 0.7;
transition: opacity 0.2s;
border-bottom: 2px solid transparent;
}
.theme-preview-tab:hover {
opacity: 1;
}
.theme-preview-tab.active {
opacity: 1;
border-bottom-color: var(--vscode-focusBorder);
}
.theme-preview-content {
padding: 12px;
background: var(--vscode-editor-background);
}
.component-preview-section {
position: relative;
}
.component-preview-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.component-preview-title {
font-size: 11px;
font-weight: 500;
color: var(--vscode-foreground);
margin: 0;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}
</style>
<div>
<ThemePreviewHeader
themeName={themeName}
isExpanded={isExpanded}
onToggleExpanded={handleToggleExpanded}
isLoading={isLoading || isLoadingCss}
onCopyCSS={handleCopyCSS}
/>
{isExpanded && (
<>
{/* Loading State */}
{isLoadingCss && (
<div className="theme-preview-content">
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '2rem',
color: 'var(--vscode-descriptionForeground)',
fontSize: '12px'
}}>
<div style={{ marginRight: '8px' }}>
<div className="loading-spinner" style={{
width: '16px',
height: '16px',
border: '2px solid var(--vscode-progressBar-background)',
borderTop: '2px solid var(--vscode-progressBar-background)',
borderRadius: '50%',
animation: 'spin 1s linear infinite'
}}></div>
</div>
Loading theme CSS...
</div>
</div>
)}
{/* Error State */}
{cssLoadError && !isLoadingCss && (
<div className="theme-preview-content">
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '2rem',
color: 'var(--vscode-errorForeground)',
fontSize: '12px',
backgroundColor: 'var(--vscode-inputValidation-errorBackground)',
border: '1px solid var(--vscode-inputValidation-errorBorder)',
borderRadius: '4px',
margin: '8px'
}}>
{cssLoadError}
</div>
</div>
)}
{/* Normal Content */}
{!isLoadingCss && !cssLoadError && parsedTheme && (
<>
<div className="theme-preview-tabs">
<button
className={`theme-preview-tab ${activeTab === 'theme' ? 'active' : ''}`}
onClick={() => setActiveTab('theme')}
>
Theme
</button>
<button
className={`theme-preview-tab ${activeTab === 'components' ? 'active' : ''}`}
onClick={() => setActiveTab('components')}
>
UI Components
</button>
</div>
<div className="theme-preview-content">
{activeTab === 'theme' && (
<>
{/* CSS File Name - Subtle Display */}
{cssFilePath && (
<div style={{
marginBottom: '0.75rem',
fontSize: '10px',
color: 'var(--vscode-descriptionForeground)',
opacity: 0.7,
textAlign: 'right'
}}>
{cssFilePath.split('/').pop()}
</div>
)}
{/* Typography Preview */}
<div style={{
marginBottom: '1rem',
padding: '0.75rem',
backgroundColor: 'var(--vscode-editor-background)',
border: '1px solid var(--vscode-panel-border)',
borderRadius: '4px'
}}>
<div style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr 1fr',
gap: '1rem',
textAlign: 'center'
}}>
<div>
<div style={{
fontSize: '10px',
color: 'var(--vscode-descriptionForeground)',
marginBottom: '0.25rem',
fontWeight: 500
}}>
Sans
</div>
<div style={{
fontSize: '12px',
color: 'var(--vscode-foreground)',
fontFamily: parsedTheme.fonts?.sans || 'inherit'
}}>
{parsedTheme.fonts?.sans?.split(',')[0]?.trim() || 'Default'}
</div>
</div>
<div>
<div style={{
fontSize: '10px',
color: 'var(--vscode-descriptionForeground)',
marginBottom: '0.25rem',
fontWeight: 500
}}>
Serif
</div>
<div style={{
fontSize: '12px',
color: 'var(--vscode-foreground)',
fontFamily: parsedTheme.fonts?.serif || 'inherit'
}}>
{parsedTheme.fonts?.serif?.split(',')[0]?.trim() || 'Default'}
</div>
</div>
<div>
<div style={{
fontSize: '10px',
color: 'var(--vscode-descriptionForeground)',
marginBottom: '0.25rem',
fontWeight: 500
}}>
Mono
</div>
<div style={{
fontSize: '12px',
color: 'var(--vscode-foreground)',
fontFamily: parsedTheme.fonts?.mono || 'inherit'
}}>
{parsedTheme.fonts?.mono?.split(',')[0]?.trim() || 'Default'}
</div>
</div>
</div>
</div>
{/* Color Palette */}
<ColorPalette
colors={getGroupedColors(parsedTheme)}
isDarkMode={isDarkMode}
/>
</>
)}
{activeTab === 'components' && (
<div className="component-preview-section">
<div className="component-preview-header">
<h4 className="component-preview-title">Component Preview</h4>
<ModeToggle isDarkMode={isDarkMode} onToggle={setIsDarkMode} />
</div>
<ThemePreview
theme={parsedTheme}
isDarkMode={isDarkMode}
cssSheet={currentCssContent}
/>
</div>
)}
</div>
</>
)}
</>
)}
</div>
</>
);
};
export default ThemePreviewCard;

View File

@@ -0,0 +1,82 @@
import React from 'react';
interface ThemePreviewHeaderProps {
themeName: string;
isExpanded: boolean;
onToggleExpanded: () => void;
isLoading: boolean;
onCopyCSS: () => void;
}
const ThemePreviewHeader: React.FC<ThemePreviewHeaderProps> = ({
themeName,
isExpanded,
onToggleExpanded,
isLoading,
onCopyCSS
}) => {
const handleActionClick = (e: React.MouseEvent, action: () => void) => {
e.stopPropagation();
action();
};
return (
<div
className="tool-message__header"
onClick={onToggleExpanded}
>
<div className="tool-message__main">
<span className="tool-icon">
{isLoading ? (
<div className="loading-icon-simple">
<div className="loading-ring"></div>
</div>
) : (
'⚙️'
)}
</span>
<div className="tool-info">
<span className="tool-name">
Generate Theme - <span style={{ color: 'var(--vscode-descriptionForeground)', fontWeight: 'normal' }}>{themeName}</span>
</span>
{isLoading && (
<span className="tool-time-remaining">
Generating theme...
</span>
)}
</div>
</div>
<div className="tool-actions">
{!isLoading && (
<button
className="theme-copy-btn"
onClick={(e) => handleActionClick(e, onCopyCSS)}
title="Copy CSS to clipboard"
style={{
background: 'transparent',
border: 'none',
color: 'var(--vscode-descriptionForeground)',
cursor: 'pointer',
padding: '2px 4px',
borderRadius: '2px',
transition: 'all 0.2s ease',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '12px'
}}
>
📋
</button>
)}
<button className={`tool-expand-btn ${isExpanded ? 'expanded' : ''}`}>
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"/>
</svg>
</button>
</div>
</div>
);
};
export default ThemePreviewHeader;

View File

@@ -0,0 +1,101 @@
import React from 'react';
import { ConnectionLine } from '../types/canvas.types';
interface ConnectionLinesProps {
connections: ConnectionLine[];
containerBounds: { width: number; height: number };
isVisible: boolean;
zoomLevel: number;
}
const ConnectionLines: React.FC<ConnectionLinesProps> = ({
connections,
containerBounds,
isVisible,
zoomLevel
}) => {
if (!isVisible || connections.length === 0) {
return null;
}
// Adjust line styling based on zoom level
const getLineStyle = (connection: ConnectionLine) => ({
stroke: connection.color || 'var(--vscode-textLink-foreground)',
strokeWidth: (connection.width || 2) / zoomLevel, // Thinner lines when zoomed out
strokeDasharray: zoomLevel < 0.5 ? '5,5' : 'none', // Dashed when very zoomed out
opacity: Math.max(0.3, Math.min(1, zoomLevel)), // More transparent when zoomed out
markerEnd: 'url(#arrowhead)'
});
// Calculate curve path for organic looking connections
const createCurvePath = (from: { x: number; y: number }, to: { x: number; y: number }) => {
const dx = to.x - from.x;
const dy = to.y - from.y;
// Create a smooth curve with control points
const cp1x = from.x + dx * 0.6;
const cp1y = from.y;
const cp2x = to.x - dx * 0.6;
const cp2y = to.y;
return `M ${from.x} ${from.y} C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${to.x} ${to.y}`;
};
return (
<svg
className="connection-lines"
style={{
position: 'absolute',
top: 0,
left: 0,
width: containerBounds.width,
height: containerBounds.height,
pointerEvents: 'none',
zIndex: 1,
overflow: 'visible'
}}
>
{/* Arrow marker definition */}
<defs>
<marker
id="arrowhead"
markerWidth="10"
markerHeight="7"
refX="9"
refY="3.5"
orient="auto"
fill="var(--vscode-textLink-foreground)"
>
<polygon points="0 0, 10 3.5, 0 7" />
</marker>
</defs>
{/* Render all connection lines */}
{connections.map((connection) => (
<g key={connection.id} className="connection-group">
{/* Main connection line */}
<path
d={createCurvePath(connection.fromPosition, connection.toPosition)}
fill="none"
style={getLineStyle(connection)}
className="connection-line"
/>
{/* Optional: Add a thicker invisible line for easier hover detection */}
<path
d={createCurvePath(connection.fromPosition, connection.toPosition)}
fill="none"
stroke="transparent"
strokeWidth="10"
className="connection-line-hover-target"
style={{ pointerEvents: 'stroke' }}
>
<title>{`${connection.fromFrame}${connection.toFrame}`}</title>
</path>
</g>
))}
</svg>
);
};
export default ConnectionLines;

View File

@@ -0,0 +1,838 @@
import React from 'react';
import { DesignFile, GridPosition, FrameDimensions, ViewportMode, WebviewMessage } from '../types/canvas.types';
import { MobileIcon, TabletIcon, DesktopIcon, GlobeIcon } from './Icons';
// Import logo images
import cursorLogo from '../../assets/cursor_logo.png';
import windsurfLogo from '../../assets/windsurf_logo.png';
import claudeCodeLogo from '../../assets/claude_code_logo.png';
import lovableLogo from '../../assets/lovable_logo.png';
import boltLogo from '../../assets/bolt_logo.jpg';
interface DesignFrameProps {
file: DesignFile;
position: GridPosition;
dimensions: FrameDimensions;
isSelected: boolean;
onSelect: (fileName: string) => void;
renderMode?: 'placeholder' | 'iframe' | 'html';
showMetadata?: boolean;
viewport?: ViewportMode;
viewportDimensions?: FrameDimensions;
onViewportChange?: (fileName: string, viewport: ViewportMode) => void;
useGlobalViewport?: boolean;
onDragStart?: (fileName: string, startPos: GridPosition, mouseEvent: React.MouseEvent) => void;
isDragging?: boolean;
nonce?: string | null;
onSendToChat?: (fileName: string, prompt: string) => void;
}
const DesignFrame: React.FC<DesignFrameProps> = ({
file,
position,
dimensions,
isSelected,
onSelect,
renderMode = 'placeholder',
showMetadata = true,
viewport = 'desktop',
viewportDimensions,
onViewportChange,
useGlobalViewport = false,
onDragStart,
isDragging = false,
nonce = null,
onSendToChat
}) => {
const [isLoading, setIsLoading] = React.useState(renderMode === 'iframe');
const [hasError, setHasError] = React.useState(false);
const [dragPreventOverlay, setDragPreventOverlay] = React.useState(false);
const [showCopyDropdown, setShowCopyDropdown] = React.useState(false);
const [copyButtonState, setCopyButtonState] = React.useState<{ text: string; isSuccess: boolean }>({ text: 'Copy prompt', isSuccess: false });
const [copyPathButtonState, setCopyPathButtonState] = React.useState<{ text: string; isSuccess: boolean }>({ text: 'Copy design path', isSuccess: false });
const handleClick = () => {
onSelect(file.name);
};
const handleMouseDown = (e: React.MouseEvent) => {
if (onDragStart && e.button === 0) { // Left mouse button only
e.preventDefault();
e.stopPropagation();
// Show overlay to prevent iframe interaction during potential drag
setDragPreventOverlay(true);
onDragStart(file.name, position, e);
}
};
// Clear drag prevention overlay when dragging ends
React.useEffect(() => {
if (!isDragging) {
setDragPreventOverlay(false);
}
}, [isDragging]);
// Close dropdown when clicking outside
React.useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (showCopyDropdown) {
const target = event.target as Element;
const dropdownElement = target.closest('.copy-prompt-dropdown');
if (!dropdownElement) {
setShowCopyDropdown(false);
}
}
};
if (showCopyDropdown) {
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}
}, [showCopyDropdown]);
const handleViewportToggle = (newViewport: ViewportMode) => {
if (onViewportChange && !useGlobalViewport) {
onViewportChange(file.name, newViewport);
}
};
const handleCopyPrompt = async (e: React.MouseEvent, platform?: string) => {
e.preventDefault();
e.stopPropagation();
let promptText = '';
let platformName = '';
switch (platform) {
case 'cursor':
promptText = `${file.content}\n\nAbove is the design implementation, please use that as a reference to build a similar UI component. Make sure to follow modern React and TypeScript best practices.`;
platformName = 'Cursor';
break;
case 'windsurf':
promptText = `${file.content}\n\nAbove is the design implementation. Please analyze this design and create a similar UI component using modern web technologies and best practices.`;
platformName = 'Windsurf';
break;
case 'claude-code':
promptText = `${file.content}\n\nAbove is the design implementation. Please use this as a reference to create a similar component. Focus on clean, maintainable code structure.`;
platformName = 'Claude Code';
break;
case 'lovable':
promptText = `${file.content}\n\nAbove is the design implementation. Please recreate this design as a responsive React component with modern styling.`;
platformName = 'Lovable';
break;
case 'bolt':
promptText = `${file.content}\n\nAbove is the design implementation. Please create a similar UI using this as reference. Make it production-ready with proper styling.`;
platformName = 'Bolt';
break;
default:
promptText = `${file.content}\n\nAbove is the design implementation, please use that as a reference`;
platformName = '';
}
try {
await navigator.clipboard.writeText(promptText);
console.log(`✅ Copied ${platformName} prompt to clipboard for:`, file.name);
// Show success state on button
setCopyButtonState({ text: `Copied for ${platformName}!`, isSuccess: true });
setTimeout(() => {
setCopyButtonState({ text: 'Copy prompt', isSuccess: false });
}, 2000);
// Hide dropdown
setShowCopyDropdown(false);
} catch (err) {
console.error('❌ Failed to copy to clipboard:', err);
// Fallback: create a temporary textarea and copy
const textarea = document.createElement('textarea');
textarea.value = promptText;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
console.log(`✅ Copied ${platformName} prompt using fallback method for:`, file.name);
// Show success state on button
setCopyButtonState({ text: `Copied for ${platformName}!`, isSuccess: true });
setTimeout(() => {
setCopyButtonState({ text: 'Copy prompt', isSuccess: false });
}, 2000);
// Hide dropdown
setShowCopyDropdown(false);
}
};
const handleCopyDropdownToggle = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
console.log('Dropdown toggle clicked. Current context:', (window as any).__WEBVIEW_CONTEXT__);
console.log('Logo URIs available:', (window as any).__WEBVIEW_CONTEXT__?.logoUris);
setShowCopyDropdown(!showCopyDropdown);
};
const handleCopyDesignPath = async (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
const designPath = `Design file: ${file.path}`;
try {
await navigator.clipboard.writeText(designPath);
console.log(`✅ Copied design path to clipboard:`, designPath);
// Show success state on button
setCopyPathButtonState({ text: 'Copied!', isSuccess: true });
setTimeout(() => {
setCopyPathButtonState({ text: 'Copy design path', isSuccess: false });
}, 2000);
} catch (err) {
console.error('❌ Failed to copy design path to clipboard:', err);
// Fallback: create a temporary textarea and copy
const textarea = document.createElement('textarea');
textarea.value = designPath;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
console.log(`✅ Copied design path using fallback method:`, designPath);
// Show success state on button
setCopyPathButtonState({ text: 'Copied!', isSuccess: true });
setTimeout(() => {
setCopyPathButtonState({ text: 'Copy design path', isSuccess: false });
}, 2000);
}
};
const handleCreateVariations = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
if (onSendToChat) {
onSendToChat(file.name, 'Create more variations based on this style');
}
};
const handleIterateWithFeedback = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
if (onSendToChat) {
onSendToChat(file.name, 'Please create a few variations with this feedback: ');
}
};
const getViewportIcon = (mode: ViewportMode): React.ReactElement => {
switch (mode) {
case 'mobile': return <MobileIcon />;
case 'tablet': return <TabletIcon />;
case 'desktop': return <DesktopIcon />;
default: return <DesktopIcon />;
}
};
const getViewportLabel = (mode: ViewportMode): string => {
switch (mode) {
case 'mobile': return 'Mobile';
case 'tablet': return 'Tablet';
case 'desktop': return 'Desktop';
default: return 'Desktop';
}
};
const renderContent = () => {
switch (renderMode) {
case 'iframe':
// Handle SVG files differently than HTML files
if (file.fileType === 'svg') {
// For SVG files, wrap in HTML with proper viewport
const svgHtml = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob: https: http:; img-src 'self' data: blob: https: http: *; font-src 'self' data: https: http: *; style-src 'self' 'unsafe-inline' https: http: *; script-src 'self' 'unsafe-inline' 'unsafe-eval' https: http: *; connect-src 'self' https: http: *;">
${viewportDimensions ? `<meta name="viewport" content="width=${viewportDimensions.width}, height=${viewportDimensions.height}, initial-scale=1.0">` : ''}
<style>
body {
margin: 0;
padding: 20px;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
background: white;
box-sizing: border-box;
}
svg {
max-width: 100%;
max-height: 100%;
height: auto;
width: auto;
}
img {
max-width: 100%;
height: auto;
}
</style>
</head>
<body>
${file.content}
<script>
// Auto-render images in SVG context
document.addEventListener('DOMContentLoaded', function() {
const images = document.querySelectorAll('img');
images.forEach(function(img) {
img.loading = 'eager';
if (!img.complete || img.naturalWidth === 0) {
const originalSrc = img.src;
img.src = '';
img.src = originalSrc;
}
});
});
</script>
</body>
</html>
`;
return (
<iframe
srcDoc={svgHtml}
title={`${file.name} - SVG`}
style={{
width: viewportDimensions ? `${viewportDimensions.width}px` : '100%',
height: viewportDimensions ? `${viewportDimensions.height}px` : '100%',
border: 'none',
background: 'white',
borderRadius: '0 0 6px 6px',
pointerEvents: (isSelected && !dragPreventOverlay && !isDragging) ? 'auto' : 'none'
}}
referrerPolicy="no-referrer"
loading="lazy"
onLoad={() => {
setIsLoading(false);
setHasError(false);
console.log(`SVG Frame loaded: ${file.name}`);
}}
onError={(e) => {
setIsLoading(false);
setHasError(true);
console.error(`SVG Frame error for ${file.name}:`, e);
}}
/>
);
}
// HTML file handling (existing logic)
// Function to inject nonce into script tags
const injectNonce = (html: string, nonce: string | null) => {
if (!nonce) return html;
return html.replace(/<script/g, `<script nonce="${nonce}"`);
};
// Inject viewport meta tag and CSP if we have viewport dimensions
let modifiedContent = file.content;
// Use a more permissive CSP that relies on VS Code's built-in security
const iframeCSP = `<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob: https: http:; img-src 'self' data: blob: https: http: *; style-src 'self' 'unsafe-inline' data: https: http: *; script-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob: https: http: *; connect-src 'self' https: http: *; frame-src 'self' data: blob: https: http: *;">`;
// Service worker approach for external resource loading
const serviceWorkerScript = `
<script${nonce ? ` nonce="${nonce}"` : ''}>
// Register service worker to handle external resources
if ('serviceWorker' in navigator) {
const swCode = \`
self.addEventListener('fetch', event => {
const url = event.request.url;
// Only handle external image requests
if (url.startsWith('http') && (url.includes('placehold.co') || url.includes('media.giphy.com') || url.match(/\\.(jpg|jpeg|png|gif|svg|webp)$/i))) {
event.respondWith(
fetch(event.request, {
mode: 'cors',
credentials: 'omit'
}).catch(() => {
// Fallback: return a placeholder image
const canvas = new OffscreenCanvas(200, 120);
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#cccccc';
ctx.fillRect(0, 0, 200, 120);
ctx.fillStyle = '#000000';
ctx.font = '16px Arial';
ctx.textAlign = 'center';
ctx.fillText('IMAGE', 100, 60);
return canvas.convertToBlob().then(blob =>
new Response(blob, {
headers: { 'Content-Type': 'image/png' }
})
);
})
);
}
});
\`;
const blob = new Blob([swCode], { type: 'application/javascript' });
const swUrl = URL.createObjectURL(blob);
navigator.serviceWorker.register(swUrl).then(registration => {
console.log('Service Worker registered successfully');
// Wait for service worker to be active
if (registration.active) {
processImages();
} else {
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'activated') {
processImages();
}
});
});
}
}).catch(error => {
console.log('Service Worker registration failed, falling back to direct loading');
processImages();
});
} else {
// Fallback for browsers without service worker support
processImages();
}
function processImages() {
// Force reload all external images to trigger service worker
const images = document.querySelectorAll('img[src]');
images.forEach(img => {
if (img.src.startsWith('http')) {
const originalSrc = img.src;
img.src = '';
setTimeout(() => {
img.src = originalSrc;
}, 10);
}
});
}
// Process images when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
setTimeout(processImages, 100);
});
} else {
setTimeout(processImages, 100);
}
</script>`;
if (viewportDimensions) {
const viewportMeta = `<meta name="viewport" content="width=${viewportDimensions.width}, height=${viewportDimensions.height}, initial-scale=1.0">`;
if (modifiedContent.includes('<head>')) {
modifiedContent = modifiedContent.replace('<head>', `<head>\n${iframeCSP}\n${viewportMeta}`);
// Inject script before closing body tag
if (modifiedContent.includes('</body>')) {
modifiedContent = modifiedContent.replace('</body>', `${serviceWorkerScript}\n</body>`);
} else {
modifiedContent += serviceWorkerScript;
}
} else if (modifiedContent.includes('<html>')) {
modifiedContent = modifiedContent.replace('<html>', `<html><head>\n${iframeCSP}\n${viewportMeta}\n</head>`);
if (modifiedContent.includes('</body>')) {
modifiedContent = modifiedContent.replace('</body>', `${serviceWorkerScript}\n</body>`);
} else {
modifiedContent += serviceWorkerScript;
}
} else {
modifiedContent = `<head>\n${iframeCSP}\n${viewportMeta}\n</head>\n${modifiedContent}${serviceWorkerScript}`;
}
} else {
// Even without viewport dimensions, we need to inject CSP and script
if (modifiedContent.includes('<head>')) {
modifiedContent = modifiedContent.replace('<head>', `<head>\n${iframeCSP}`);
if (modifiedContent.includes('</body>')) {
modifiedContent = modifiedContent.replace('</body>', `${serviceWorkerScript}\n</body>`);
} else {
modifiedContent += serviceWorkerScript;
}
} else if (modifiedContent.includes('<html>')) {
modifiedContent = modifiedContent.replace('<html>', `<html><head>\n${iframeCSP}\n</head>`);
if (modifiedContent.includes('</body>')) {
modifiedContent = modifiedContent.replace('</body>', `${serviceWorkerScript}\n</body>`);
} else {
modifiedContent += serviceWorkerScript;
}
} else {
modifiedContent = `<head>\n${iframeCSP}\n</head>\n${modifiedContent}${serviceWorkerScript}`;
}
}
// Inject nonce into all script tags
modifiedContent = injectNonce(modifiedContent, nonce);
return (
<iframe
srcDoc={modifiedContent}
title={`${file.name} - ${getViewportLabel(viewport)}`}
style={{
width: viewportDimensions ? `${viewportDimensions.width}px` : '100%',
height: viewportDimensions ? `${viewportDimensions.height}px` : '100%',
border: 'none',
background: 'white',
borderRadius: '0 0 6px 6px',
pointerEvents: (isSelected && !dragPreventOverlay && !isDragging) ? 'auto' : 'none'
}}
referrerPolicy="no-referrer"
loading="lazy"
onLoad={() => {
setIsLoading(false);
setHasError(false);
console.log(`Frame loaded: ${file.name} (${viewport})`);
}}
onError={(e) => {
setIsLoading(false);
setHasError(true);
console.error(`Frame error for ${file.name}:`, e);
}}
/>
);
case 'html':
// Direct HTML/SVG rendering - USE WITH CAUTION (security risk)
// Only use for trusted content or when iframe fails
if (file.fileType === 'svg') {
return (
<div
style={{
width: '100%',
height: '100%',
overflow: 'hidden',
background: 'white',
border: '1px solid var(--vscode-errorForeground)',
borderRadius: '0 0 6px 6px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '20px',
boxSizing: 'border-box'
}}
title="⚠️ Direct SVG rendering - potential security risk"
dangerouslySetInnerHTML={{ __html: file.content }}
/>
);
}
return (
<div
dangerouslySetInnerHTML={{ __html: file.content }}
style={{
width: '100%',
height: '100%',
overflow: 'hidden',
background: 'white',
border: '1px solid var(--vscode-errorForeground)',
borderRadius: '0 0 6px 6px'
}}
title="⚠️ Direct HTML rendering - potential security risk"
/>
);
case 'placeholder':
default:
const placeholderIcon = file.fileType === 'svg' ? '🎨' : '🌐';
const placeholderHint = file.fileType === 'svg' ? 'SVG Vector Graphics' : 'HTML Design';
return (
<div className="frame-placeholder">
<div className="placeholder-icon">{placeholderIcon}</div>
<p className="placeholder-name">{file.name}</p>
<div className="placeholder-meta">
<span>{(file.size / 1024).toFixed(1)} KB</span>
<span>{file.modified.toLocaleDateString()}</span>
<span className="file-type">{file.fileType.toUpperCase()}</span>
</div>
{renderMode === 'placeholder' && (
<small className="placeholder-hint">{placeholderHint} - Zoom in to load</small>
)}
</div>
);
}
};
return (
<div
className={`design-frame ${isSelected ? 'selected' : ''} ${isDragging ? 'dragging' : ''}`}
style={{
position: 'absolute',
left: `${position.x}px`,
top: `${position.y}px`,
width: `${dimensions.width}px`,
height: `${dimensions.height}px`,
cursor: isDragging ? 'grabbing' : 'grab',
zIndex: isDragging ? 1000 : (isSelected ? 10 : 1),
opacity: isDragging ? 0.8 : 1
}}
onClick={handleClick}
title={`${file.name} (${(file.size / 1024).toFixed(1)} KB)`}
onMouseDown={handleMouseDown}
>
<div className="frame-header">
<span className="frame-title">{file.name}</span>
{/* Viewport Controls */}
{onViewportChange && !useGlobalViewport && (
<div className="frame-viewport-controls">
<button
className={`frame-viewport-btn ${viewport === 'mobile' ? 'active' : ''}`}
onClick={() => handleViewportToggle('mobile')}
title="Mobile View"
>
<MobileIcon />
</button>
<button
className={`frame-viewport-btn ${viewport === 'tablet' ? 'active' : ''}`}
onClick={() => handleViewportToggle('tablet')}
title="Tablet View"
>
<TabletIcon />
</button>
<button
className={`frame-viewport-btn ${viewport === 'desktop' ? 'active' : ''}`}
onClick={() => handleViewportToggle('desktop')}
title="Desktop View"
>
<DesktopIcon />
</button>
</div>
)}
{/* Global viewport indicator */}
{useGlobalViewport && (
<div className="frame-viewport-indicator">
<span className="global-indicator"><GlobeIcon /></span>
<span className="viewport-icon">{getViewportIcon(viewport)}</span>
</div>
)}
{showMetadata && (
<div className="frame-meta">
{isLoading && <span className="frame-status loading"></span>}
{hasError && <span className="frame-status error"></span>}
{!isLoading && !hasError && renderMode === 'iframe' && (
<span className="frame-status loaded"></span>
)}
</div>
)}
</div>
<div className="frame-content">
{renderContent()}
{/* Drag prevention overlay - prevents iframe interaction during drag */}
{(dragPreventOverlay || isDragging) && isSelected && renderMode === 'iframe' && (
<div className="frame-drag-overlay">
{dragPreventOverlay && !isDragging && (
<div className="drag-ready-hint">
<span></span>
<p>Ready to drag</p>
</div>
)}
</div>
)}
{/* Loading overlay for iframe */}
{isLoading && renderMode === 'iframe' && (
<div className="frame-loading-overlay">
<div className="frame-loading-spinner">
<div className="spinner-small"></div>
<span>Loading...</span>
</div>
</div>
)}
{/* Error overlay */}
{hasError && (
<div className="frame-error-overlay">
<div className="frame-error-content">
<span></span>
<p>Failed to load</p>
<small>{file.name}</small>
</div>
</div>
)}
</div>
{/* Floating Action Buttons - Outside frame, top-right corner */}
{isSelected && !isDragging && (
<div
className="floating-action-buttons"
onClick={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
>
<button
className="floating-action-btn"
onClick={handleCreateVariations}
title="Create more variations based on this style"
>
<svg className="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="6" cy="6" r="3"/>
<circle cx="18" cy="18" r="3"/>
<circle cx="18" cy="6" r="3"/>
<path d="M18 9v6"/>
<path d="M9 6h6"/>
</svg>
<span className="btn-text">Create variations</span>
</button>
<button
className="floating-action-btn"
onClick={handleIterateWithFeedback}
title="Create variations with feedback"
>
<svg className="btn-icon" viewBox="0 0 24 24" fill="none">
<path d="M17.65 6.35C16.2 4.9 14.21 4 12 4C7.58 4 4 7.58 4 12C4 16.42 7.58 20 12 20C15.73 20 18.84 17.45 19.73 14H17.65C16.83 16.33 14.61 18 12 18C8.69 18 6 15.31 6 12C6 8.69 8.69 6 12 6C13.66 6 15.14 6.69 16.22 7.78L13 11H20V4L17.65 6.35Z" fill="currentColor"/>
</svg>
<span className="btn-text">Iterate with feedback</span>
</button>
{/* Copy Prompt Dropdown */}
<div className="copy-prompt-dropdown">
<button
className={`floating-action-btn copy-prompt-main-btn ${copyButtonState.isSuccess ? 'success' : ''}`}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
console.log('Dropdown toggle clicked. Current context:', (window as any).__WEBVIEW_CONTEXT__);
console.log('Logo URIs available:', (window as any).__WEBVIEW_CONTEXT__?.logoUris);
setShowCopyDropdown(!showCopyDropdown);
}}
title="Copy file content with reference prompt"
>
<svg className="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.582a.5.5 0 0 1 0 .962L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/>
<path d="M20 3v4"/>
<path d="M22 5h-4"/>
<path d="M4 17v2"/>
<path d="M5 18H3"/>
</svg>
<span className="btn-text">{copyButtonState.text}</span>
<svg className="dropdown-arrow" viewBox="0 0 24 24" fill="none">
<path d="M7 10L12 15L17 10H7Z" fill="currentColor"/>
</svg>
</button>
{showCopyDropdown && (
<div className="copy-dropdown-menu">
<button
className="copy-dropdown-item"
onClick={(e) => handleCopyPrompt(e, 'cursor')}
>
<img
src={(window as any).__WEBVIEW_CONTEXT__?.logoUris?.cursor}
alt="Cursor"
className="platform-logo"
onError={(e) => {
console.error('Failed to load Cursor logo:', (window as any).__WEBVIEW_CONTEXT__?.logoUris?.cursor);
console.error('Image error event:', e);
}}
onLoad={() => console.log('Cursor logo loaded successfully')}
/>
<span>Cursor</span>
</button>
<button
className="copy-dropdown-item"
onClick={(e) => handleCopyPrompt(e, 'windsurf')}
>
<img
src={(window as any).__WEBVIEW_CONTEXT__?.logoUris?.windsurf}
alt="Windsurf"
className="platform-logo"
onError={(e) => {
console.error('Failed to load Windsurf logo:', (window as any).__WEBVIEW_CONTEXT__?.logoUris?.windsurf);
console.error('Image error event:', e);
}}
onLoad={() => console.log('Windsurf logo loaded successfully')}
/>
<span>Windsurf</span>
</button>
<button
className="copy-dropdown-item"
onClick={(e) => handleCopyPrompt(e, 'claude-code')}
>
<img
src={(window as any).__WEBVIEW_CONTEXT__?.logoUris?.claudeCode}
alt="Claude Code"
className="platform-logo"
onError={(e) => {
console.error('Failed to load Claude Code logo:', (window as any).__WEBVIEW_CONTEXT__?.logoUris?.claudeCode);
console.error('Image error event:', e);
}}
onLoad={() => console.log('Claude Code logo loaded successfully')}
/>
<span>Claude Code</span>
</button>
<button
className="copy-dropdown-item"
onClick={(e) => handleCopyPrompt(e, 'lovable')}
>
<img
src={(window as any).__WEBVIEW_CONTEXT__?.logoUris?.lovable}
alt="Lovable"
className="platform-logo"
onError={(e) => {
console.error('Failed to load Lovable logo:', (window as any).__WEBVIEW_CONTEXT__?.logoUris?.lovable);
console.error('Image error event:', e);
}}
onLoad={() => console.log('Lovable logo loaded successfully')}
/>
<span>Lovable</span>
</button>
<button
className="copy-dropdown-item"
onClick={(e) => handleCopyPrompt(e, 'bolt')}
>
<img
src={(window as any).__WEBVIEW_CONTEXT__?.logoUris?.bolt}
alt="Bolt"
className="platform-logo"
onError={(e) => {
console.error('Failed to load Bolt logo:', (window as any).__WEBVIEW_CONTEXT__?.logoUris?.bolt);
console.error('Image error event:', e);
}}
onLoad={() => console.log('Bolt logo loaded successfully')}
/>
<span>Bolt</span>
</button>
</div>
)}
</div>
{/* Copy Design Path Button */}
<button
className={`floating-action-btn copy-path-btn ${copyPathButtonState.isSuccess ? 'success' : ''}`}
onClick={handleCopyDesignPath}
title="Copy absolute path of design file"
>
<svg className="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect width="14" height="14" x="8" y="8" rx="2" ry="2"/>
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>
</svg>
<span className="btn-text">{copyPathButtonState.text}</span>
</button>
</div>
)}
</div>
);
};
export default DesignFrame;

View File

@@ -0,0 +1,96 @@
import React, { useState } from 'react';
interface DesignPanelProps {
vscode: any;
}
const DesignPanel: React.FC<DesignPanelProps> = ({ vscode }) => {
const [activeTab, setActiveTab] = useState('colors');
const [colors, setColors] = useState([
{ name: 'Primary', value: '#007ACC' },
{ name: 'Secondary', value: '#1E1E1E' },
{ name: 'Success', value: '#4CAF50' },
{ name: 'Warning', value: '#FF9800' }
]);
const handleExportDesign = () => {
vscode.postMessage({
command: 'exportDesign',
data: { colors, activeTab }
});
};
return (
<div className="design-panel">
<nav className="tab-nav">
<button
className={`tab ${activeTab === 'colors' ? 'active' : ''}`}
onClick={() => setActiveTab('colors')}
>
🎨 Colors
</button>
<button
className={`tab ${activeTab === 'typography' ? 'active' : ''}`}
onClick={() => setActiveTab('typography')}
>
Typography
</button>
<button
className={`tab ${activeTab === 'components' ? 'active' : ''}`}
onClick={() => setActiveTab('components')}
>
🧩 Components
</button>
</nav>
<div className="tab-content">
{activeTab === 'colors' && (
<div className="colors-panel">
<h3>Color Palette</h3>
<div className="color-grid">
{colors.map((color, index) => (
<div key={index} className="color-item">
<div
className="color-swatch"
style={{ backgroundColor: color.value }}
></div>
<div className="color-info">
<span className="color-name">{color.name}</span>
<span className="color-value">{color.value}</span>
</div>
</div>
))}
</div>
</div>
)}
{activeTab === 'typography' && (
<div className="typography-panel">
<h3>Typography Scale</h3>
<div className="font-samples">
<div className="font-sample h1">Heading 1</div>
<div className="font-sample h2">Heading 2</div>
<div className="font-sample body">Body Text</div>
<div className="font-sample caption">Caption</div>
</div>
</div>
)}
{activeTab === 'components' && (
<div className="components-panel">
<h3>Design Components</h3>
<p>Component library coming soon...</p>
</div>
)}
</div>
<div className="actions">
<button className="export-btn" onClick={handleExportDesign}>
📤 Export Design System
</button>
</div>
</div>
);
};
export default DesignPanel;

View File

@@ -0,0 +1,68 @@
import React from 'react';
import {
ZoomIn,
ZoomOut,
Home,
Maximize2,
RotateCcw,
Globe,
Smartphone,
Tablet,
Monitor,
Wrench,
Clock,
Check,
Lightbulb,
Folder,
AlertTriangle,
CheckSquare,
List,
Link,
Settings,
Brain
} from 'lucide-react';
const iconStyle: React.CSSProperties = {
width: '18px',
height: '18px',
};
const toolIconStyle: React.CSSProperties = {
width: '14px',
height: '14px',
};
const selectorIconStyle: React.CSSProperties = {
width: '12px',
height: '12px',
};
// Canvas Control Icons
export const ZoomInIcon = () => <ZoomIn style={iconStyle} />;
export const ZoomOutIcon = () => <ZoomOut style={iconStyle} />;
export const HomeIcon = () => <Home style={iconStyle} />;
export const ScaleIcon = () => <Maximize2 style={iconStyle} />;
export const RefreshIcon = () => <RotateCcw style={iconStyle} />;
export const SettingsIcon = () => <Settings style={iconStyle} />;
// Viewport Icons
export const GlobeIcon = () => <Globe style={iconStyle} />;
export const MobileIcon = () => <Smartphone style={iconStyle} />;
export const TabletIcon = () => <Tablet style={iconStyle} />;
export const DesktopIcon = () => <Monitor style={iconStyle} />;
// Layout Icons
export const TreeIcon = () => <List style={iconStyle} />;
export const LinkIcon = () => <Link style={iconStyle} />;
// Tool Icons (smaller size)
export const ToolIcon = () => <Wrench style={toolIconStyle} />;
export const ClockIcon = () => <Clock style={toolIconStyle} />;
export const CheckIcon = () => <Check style={toolIconStyle} />;
export const LightBulbIcon = () => <Lightbulb style={toolIconStyle} />;
export const GroupIcon = () => <Folder style={toolIconStyle} />;
export const WarningIcon = () => <AlertTriangle style={toolIconStyle} />;
export const TaskIcon = () => <CheckSquare style={toolIconStyle} />;
// Selector Icons (smallest size)
export const BrainIcon = () => <Brain style={selectorIconStyle} />;

View File

@@ -0,0 +1,66 @@
import React from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import rehypeHighlight from 'rehype-highlight';
import 'highlight.js/styles/vs2015.css'; // Dark theme that matches VS Code
interface MarkdownRendererProps {
content: string;
className?: string;
}
const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({ content, className }) => {
return (
<div className={`markdown-content ${className || ''}`}>
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeHighlight]}
components={{
// Custom rendering for code blocks
code: ({ node, className, children, ...props }: any) => {
const match = /language-(\w+)/.exec(className || '');
const inline = !className?.includes('language-');
return !inline && match ? (
<pre className={`language-${match[1]} hljs`}>
<code className={className} {...props}>
{children}
</code>
</pre>
) : (
<code className={`inline-code ${className || ''}`} {...props}>
{children}
</code>
);
},
// Custom rendering for links to open externally
a: ({ node, children, href, ...props }) => (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
{...props}
>
{children}
</a>
),
// Custom rendering for tables
table: ({ node, children, ...props }) => (
<div className="table-wrapper">
<table {...props}>{children}</table>
</div>
),
// Custom rendering for blockquotes
blockquote: ({ node, children, ...props }) => (
<blockquote className="markdown-blockquote" {...props}>
{children}
</blockquote>
),
}}
>
{content}
</ReactMarkdown>
</div>
);
};
export default MarkdownRenderer;

View File

@@ -0,0 +1,302 @@
/* Welcome Section Styles - Following SuperDesign Design System */
.welcome-section {
padding: 20px 16px;
background: var(--vscode-sideBar-background);
color: var(--vscode-sideBar-foreground);
height: 90%;
overflow-y: auto;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 24px;
text-align: center;
}
.welcome-header {
display: flex;
flex-direction: column;
align-items: center;
}
.welcome-logo {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.logo-icon {
font-size: 24px;
width: 48px;
height: 48px;
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 6px;
}
.welcome-header h1 {
margin: 0 0 6px 0;
font-size: 20px;
font-weight: 600;
color: var(--vscode-foreground);
font-family: var(--vscode-font-family);
}
.welcome-subtitle {
font-size: 14px;
color: var(--vscode-descriptionForeground);
margin: 0;
font-weight: 400;
line-height: 1.4;
max-width: 300px;
}
/* Form Styles */
.welcome-form {
display: flex;
flex-direction: column;
gap: 16px;
width: 100%;
max-width: 260px;
}
.email-input-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.email-input {
padding: 8px 12px;
border: 1px solid var(--vscode-input-border);
border-radius: 4px;
background: var(--vscode-input-background);
color: var(--vscode-input-foreground);
font-size: 12px;
font-family: var(--vscode-font-family);
transition: border-color 0.2s ease;
width: 100%;
box-sizing: border-box;
}
.email-input:focus {
outline: none;
border-color: var(--vscode-focusBorder);
background: var(--vscode-input-background);
}
.email-input::placeholder {
color: var(--vscode-input-placeholderForeground);
}
.email-input:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.email-input.error {
border-color: var(--vscode-inputValidation-errorBorder);
background: var(--vscode-inputValidation-errorBackground);
}
.error-message {
font-size: 11px;
color: var(--vscode-inputValidation-errorForeground);
font-family: var(--vscode-font-family);
text-align: left;
margin-top: 2px;
}
.welcome-actions {
display: flex;
justify-content: center;
}
/* Button styles following VS Code design system */
.btn-primary, .btn-secondary {
padding: 8px 16px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
font-family: var(--vscode-font-family);
cursor: pointer;
transition: all 0.2s ease;
border: none;
min-width: 100px;
text-align: center;
}
.btn-primary {
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
}
.btn-primary:hover:not(:disabled) {
background: var(--vscode-button-hoverBackground);
}
.btn-primary:active:not(:disabled) {
background: var(--vscode-button-activeBackground);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
background: var(--vscode-button-background);
}
.btn-secondary {
background: transparent;
color: var(--vscode-foreground);
border: 1px solid var(--vscode-input-border);
}
.btn-secondary:hover {
background: var(--vscode-list-hoverBackground);
border-color: var(--vscode-focusBorder);
}
.btn-secondary:active {
background: var(--vscode-list-activeSelectionBackground);
}
/* Responsive adjustments */
@media (max-width: 480px) {
.welcome-section {
padding: 16px;
gap: 24px;
}
.welcome-header h1 {
font-size: 20px;
}
.welcome-subtitle {
font-size: 13px;
}
.welcome-form {
max-width: 100%;
}
.btn-primary {
width: 100%;
}
}
/* Scrollbar styling to match VS Code */
.welcome-section::-webkit-scrollbar {
width: 6px;
}
.welcome-section::-webkit-scrollbar-track {
background: transparent;
}
.welcome-section::-webkit-scrollbar-thumb {
background: var(--vscode-scrollbarSlider-background);
border-radius: 3px;
}
.welcome-section::-webkit-scrollbar-thumb:hover {
background: var(--vscode-scrollbarSlider-hoverBackground);
}
/* Keep the feature/step styles for potential future use */
.welcome-features {
/* Features section spacing */
}
.feature-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 16px;
}
.feature-card {
background: var(--vscode-input-background);
border: 1px solid var(--vscode-input-border);
border-radius: 8px;
padding: 16px;
transition: all 0.2s ease;
cursor: default;
text-align: center;
}
.feature-card:hover {
background: var(--vscode-list-hoverBackground);
border-color: var(--vscode-focusBorder);
}
.feature-icon {
font-size: 24px;
margin-bottom: 8px;
display: block;
}
.feature-card h3 {
margin: 0 0 6px 0;
font-size: 14px;
font-weight: 600;
color: var(--vscode-foreground);
font-family: var(--vscode-font-family);
}
.feature-card p {
margin: 0;
font-size: 12px;
color: var(--vscode-descriptionForeground);
line-height: 1.3;
}
.welcome-getting-started h3 {
margin: 0 0 16px 0;
font-size: 16px;
font-weight: 600;
color: var(--vscode-foreground);
font-family: var(--vscode-font-family);
text-align: center;
}
.getting-started-steps {
display: flex;
flex-direction: column;
gap: 12px;
}
.step {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
background: var(--vscode-input-background);
border: 1px solid var(--vscode-input-border);
border-radius: 8px;
}
.step-number {
width: 24px;
height: 24px;
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 12px;
flex-shrink: 0;
}
.step p {
margin: 0;
font-size: 12px;
color: var(--vscode-foreground);
line-height: 1.3;
}

View File

@@ -0,0 +1,111 @@
import React, { useState, useEffect } from 'react';
interface WelcomeProps {
onGetStarted: () => void;
vscode: any;
}
const Welcome: React.FC<WelcomeProps> = ({ onGetStarted, vscode }) => {
const [email, setEmail] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const validateEmail = (email: string): boolean => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
};
// Listen for messages from the extension
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
const message = event.data;
if (message.command === 'emailSubmitSuccess') {
console.log('✅ Email submitted successfully via extension:', message.email);
setIsLoading(false);
onGetStarted();
} else if (message.command === 'emailSubmitError') {
console.error('❌ Email submission failed via extension:', message.error);
setError(message.error);
setIsLoading(false);
}
};
window.addEventListener('message', handleMessage);
return () => window.removeEventListener('message', handleMessage);
}, [onGetStarted]);
const submitEmail = async (emailToSubmit: string): Promise<void> => {
// Send message to extension to handle the API call
vscode.postMessage({
command: 'submitEmail',
email: emailToSubmit
});
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
// Validate email
if (!email.trim()) {
setError('Email is required');
return;
}
if (!validateEmail(email.trim())) {
setError('Invalid email');
return;
}
setIsLoading(true);
// Submit email via extension (response will be handled by useEffect)
await submitEmail(email.trim());
};
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setEmail(e.target.value);
if (error) {
setError(''); // Clear error when user starts typing
}
};
return (
<div className="welcome-section">
<div className="welcome-header">
<div className="welcome-logo">
<div className="logo-icon"></div>
<h1>Welcome to Super Design</h1>
</div>
<p className="welcome-subtitle">Your AI-powered canvas for rapid UI exploration</p>
</div>
<form className="welcome-form" onSubmit={handleSubmit}>
<div className="email-input-group">
<input
type="email"
value={email}
onChange={handleEmailChange}
placeholder="Enter your email address"
className={`email-input ${error ? 'error' : ''}`}
disabled={isLoading}
autoComplete="email"
/>
{error && <span className="error-message">{error}</span>}
</div>
<div className="welcome-actions">
<button
type="submit"
className="btn-primary"
disabled={isLoading || !email.trim()}
>
{isLoading ? 'Getting Started...' : 'Get Started'}
</button>
</div>
</form>
</div>
);
};
export default Welcome;

View File

@@ -0,0 +1 @@
export { default } from './Welcome';

View File

@@ -0,0 +1,378 @@
import React, { useState, useEffect, useCallback } from 'react';
import { CoreMessage } from 'ai';
// Additional metadata for UI state
interface MessageMetadata {
timestamp?: number;
is_loading?: boolean;
estimated_duration?: number;
start_time?: number;
elapsed_time?: number;
progress_percentage?: number;
session_id?: string;
result_type?: string;
is_error?: boolean;
duration_ms?: number;
total_cost_usd?: number;
// Tool-related metadata
tool_name?: string;
tool_id?: string;
tool_input?: any;
tool_result?: any;
result_is_error?: boolean;
result_received?: boolean;
actions?: Array<{
text: string;
command: string;
args?: string;
}>;
}
// Message with metadata for UI
export type ChatMessage = CoreMessage & {
metadata?: MessageMetadata;
};
export interface ChatHookResult {
chatHistory: ChatMessage[];
isLoading: boolean;
sendMessage: (message: string) => void;
clearHistory: () => void;
setChatHistory: React.Dispatch<React.SetStateAction<ChatMessage[]>>;
}
// Tool time estimation map (in seconds)
const TOOL_TIME_ESTIMATES: { [key: string]: number } = {
'mcp_taskmaster-ai_initialize_project': 45,
'mcp_taskmaster-ai_parse_prd': 180,
'mcp_taskmaster-ai_analyze_project_complexity': 120,
'mcp_taskmaster-ai_expand_task': 90,
'mcp_taskmaster-ai_expand_all': 200,
'mcp_taskmaster-ai_update_task': 60,
'mcp_taskmaster-ai_update_subtask': 45,
'mcp_taskmaster-ai_add_task': 75,
'mcp_taskmaster-ai_research': 150,
'codebase_search': 30,
'read_file': 15,
'edit_file': 45,
'run_terminal_cmd': 60,
'default': 90
};
function getToolTimeEstimate(toolName: string): number {
if (TOOL_TIME_ESTIMATES[toolName]) {
return TOOL_TIME_ESTIMATES[toolName];
}
for (const [key, value] of Object.entries(TOOL_TIME_ESTIMATES)) {
if (toolName.includes(key) || key.includes(toolName)) {
return value;
}
}
if (toolName.includes('taskmaster') || toolName.includes('task')) {
return 120;
}
if (toolName.includes('search') || toolName.includes('grep')) {
return 30;
}
if (toolName.includes('file') || toolName.includes('read') || toolName.includes('write')) {
return 25;
}
return TOOL_TIME_ESTIMATES.default;
}
export function useChat(vscode: any): ChatHookResult {
const [chatHistory, setChatHistory] = useState<ChatMessage[]>(() => {
// Initialize with persisted chat history from localStorage
try {
const saved = localStorage.getItem('superdesign-chat-history');
return saved ? JSON.parse(saved) : [];
} catch (error) {
console.warn('Failed to load chat history from localStorage:', error);
return [];
}
});
const [isLoading, setIsLoading] = useState(false);
// Persist chat history to localStorage whenever it changes
useEffect(() => {
try {
localStorage.setItem('superdesign-chat-history', JSON.stringify(chatHistory));
} catch (error) {
console.warn('Failed to save chat history to localStorage:', error);
}
}, [chatHistory]);
const clearHistory = useCallback(() => {
setChatHistory([]);
// Also clear from localStorage
try {
localStorage.removeItem('superdesign-chat-history');
} catch (error) {
console.warn('Failed to clear chat history from localStorage:', error);
}
}, []);
const sendMessage = useCallback((message: string) => {
setIsLoading(true);
// Add user message to history
const userMessage: ChatMessage = {
role: 'user',
content: message,
metadata: {
timestamp: Date.now()
}
};
setChatHistory(prev => [...prev, userMessage]);
// Send to extension
vscode.postMessage({
command: 'chatMessage',
message: message,
chatHistory: [...chatHistory, userMessage]
});
}, [chatHistory, vscode]);
useEffect(() => {
const messageHandler = (event: MessageEvent) => {
const message = event.data;
switch (message.command) {
case 'chatResponseChunk':
setChatHistory(prev => {
const newHistory = [...prev];
if (message.messageType === 'assistant') {
// Handle assistant text messages
const lastMessage = newHistory[newHistory.length - 1];
if (lastMessage && lastMessage.role === 'assistant' && typeof lastMessage.content === 'string') {
// Append to existing assistant message
newHistory[newHistory.length - 1] = {
...lastMessage,
content: lastMessage.content + message.content
};
} else {
// Create new assistant message
newHistory.push({
role: 'assistant',
content: message.content,
metadata: {
timestamp: Date.now(),
session_id: message.metadata?.session_id
}
});
}
} else if (message.messageType === 'tool-call') {
// Handle tool calls - append to existing assistant message
const toolCallPart = {
type: 'tool-call' as const,
toolCallId: message.metadata?.tool_id || 'unknown',
toolName: message.metadata?.tool_name || 'unknown',
args: message.metadata?.tool_input || {}
};
// Find the last assistant message and append tool call to it
const lastMessage = newHistory[newHistory.length - 1];
const lastIndex = newHistory.length - 1;
if (lastMessage && lastMessage.role === 'assistant') {
// Convert content to array format and append tool call
let newContent;
if (typeof lastMessage.content === 'string') {
// Convert string to array with text part + tool call part
newContent = [
{ type: 'text', text: lastMessage.content },
toolCallPart
];
} else if (Array.isArray(lastMessage.content)) {
// Append to existing array
newContent = [...lastMessage.content, toolCallPart];
} else {
// Fallback: create new array
newContent = [toolCallPart];
}
newHistory[lastIndex] = {
...lastMessage,
content: newContent as any,
metadata: {
...lastMessage.metadata,
is_loading: true,
estimated_duration: 90,
start_time: Date.now(),
progress_percentage: 0
}
};
} else {
// No assistant message to append to, create new one
newHistory.push({
role: 'assistant',
content: [toolCallPart],
metadata: {
timestamp: Date.now(),
session_id: message.metadata?.session_id,
is_loading: true,
estimated_duration: 90,
start_time: Date.now(),
progress_percentage: 0
}
});
}
} else if (message.messageType === 'tool-result') {
// Add separate tool result message (correct CoreMessage structure)
const toolResultPart = {
type: 'tool-result' as const,
toolCallId: message.metadata?.tool_id || 'unknown',
toolName: message.metadata?.tool_name || 'unknown',
result: message.content || '',
isError: message.metadata?.is_error || false
};
newHistory.push({
role: 'tool',
content: [toolResultPart],
metadata: {
timestamp: Date.now(),
session_id: message.metadata?.session_id
}
});
}
return newHistory;
});
break;
case 'chatToolUpdate':
// Update tool parameters during streaming
setChatHistory(prev => {
const newHistory = [...prev];
// Find the most recent tool call message with matching ID
for (let i = newHistory.length - 1; i >= 0; i--) {
const msg = newHistory[i];
if (msg.role === 'assistant' && Array.isArray(msg.content)) {
const toolCallIndex = msg.content.findIndex(
part => part.type === 'tool-call' && (part as any).toolCallId === message.tool_use_id
);
if (toolCallIndex !== -1) {
// Update the tool call args
const updatedContent = [...msg.content];
updatedContent[toolCallIndex] = {
...updatedContent[toolCallIndex],
args: message.tool_input
} as any;
newHistory[i] = {
...msg,
content: updatedContent
};
break;
}
}
}
return newHistory;
});
break;
case 'chatToolResult':
// Complete tool loading state
console.log('Received tool result for:', message.tool_use_id);
setChatHistory(prev => {
const newHistory = [...prev];
// Find and complete tool loading
for (let i = newHistory.length - 1; i >= 0; i--) {
const msg = newHistory[i];
if (msg.role === 'assistant' && Array.isArray(msg.content) && msg.metadata?.is_loading) {
const hasMatchingToolCall = msg.content.some(
part => part.type === 'tool-call' && (part as any).toolCallId === message.tool_use_id
);
if (hasMatchingToolCall) {
newHistory[i] = {
...msg,
metadata: {
...msg.metadata,
is_loading: false,
progress_percentage: 100,
elapsed_time: msg.metadata.estimated_duration || 90
}
};
break;
}
}
}
return newHistory;
});
break;
case 'chatStreamEnd':
console.log('Chat stream ended');
setIsLoading(false);
break;
case 'chatErrorWithActions':
// Handle API key and authentication errors with action buttons
console.log('Chat error with actions:', message.error);
setIsLoading(false);
const errorMessage: ChatMessage = {
role: 'assistant',
content: `❌ **${message.error}**\n\nPlease configure your API key to use this AI model.`,
metadata: {
timestamp: Date.now(),
is_error: true,
actions: message.actions || []
}
};
setChatHistory(prev => [...prev, errorMessage]);
break;
case 'chatError':
// Handle general errors
console.log('Chat error:', message.error);
setIsLoading(false);
const generalErrorMessage: ChatMessage = {
role: 'assistant',
content: `❌ **Error**: ${message.error}`,
metadata: {
timestamp: Date.now(),
is_error: true
}
};
setChatHistory(prev => [...prev, generalErrorMessage]);
break;
case 'chatStopped':
console.log('Chat was stopped');
setIsLoading(false);
break;
default:
break;
}
};
window.addEventListener('message', messageHandler);
return () => window.removeEventListener('message', messageHandler);
}, []);
return {
chatHistory,
isLoading,
sendMessage,
clearHistory,
setChatHistory
};
}

View File

@@ -0,0 +1,55 @@
import { useState, useEffect } from 'react';
const FIRST_TIME_USER_KEY = 'superdesign-first-time-user';
export const useFirstTimeUser = () => {
const [isFirstTime, setIsFirstTime] = useState<boolean>(true);
const [isLoading, setIsLoading] = useState<boolean>(true);
useEffect(() => {
try {
const hasVisited = localStorage.getItem(FIRST_TIME_USER_KEY);
const isFirstTimeUser = hasVisited === null;
setIsFirstTime(isFirstTimeUser);
setIsLoading(false);
console.log('First time user check:', {
hasVisited: hasVisited,
isFirstTimeUser: isFirstTimeUser
});
} catch (error) {
console.warn('Failed to check first-time user status:', error);
// If localStorage fails, assume not first time to avoid showing welcome repeatedly
setIsFirstTime(false);
setIsLoading(false);
}
}, []);
const markAsReturningUser = () => {
try {
localStorage.setItem(FIRST_TIME_USER_KEY, 'visited');
setIsFirstTime(false);
console.log('User marked as returning user');
} catch (error) {
console.warn('Failed to mark user as returning user:', error);
}
};
const resetFirstTimeUser = () => {
try {
localStorage.removeItem(FIRST_TIME_USER_KEY);
setIsFirstTime(true);
console.log('First time user status reset');
} catch (error) {
console.warn('Failed to reset first-time user status:', error);
}
};
return {
isFirstTime,
isLoading,
markAsReturningUser,
resetFirstTimeUser
};
};

43
src/webview/index.tsx Normal file
View File

@@ -0,0 +1,43 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import ChatInterface from './components/Chat/ChatInterface';
import { WebviewContext } from '../types/context';
// Import main App styles for panel layout
import App from './App';
const container = document.getElementById('root');
if (container) {
const root = createRoot(container);
console.log('🚀 Index.tsx starting...');
// Check if this is a canvas view (doesn't need context)
const viewType = container.getAttribute('data-view');
console.log('🎯 View type from index.tsx:', viewType);
if (viewType === 'canvas') {
console.log('🎨 Canvas view detected, rendering App without context check');
// Canvas view - render App component directly (it will handle the canvas routing)
root.render(<App />);
} else {
console.log('💬 Chat view detected, checking for context...');
// Chat view - needs context
const context: WebviewContext = (window as any).__WEBVIEW_CONTEXT__;
console.log('🌐 Context found:', !!context);
if (!context) {
console.error('❌ No context provided for chat view');
root.render(<div>Error: No context provided for chat view</div>);
} else if (context.layout === 'panel') {
console.log('📋 Panel layout, rendering full App');
// Use full App component for panel (includes header and styling)
root.render(<App />);
} else {
console.log('🔲 Sidebar layout, rendering ChatInterface directly');
// Use ChatInterface directly for sidebar (compact layout)
const vscode = acquireVsCodeApi();
root.render(<ChatInterface layout="sidebar" vscode={vscode} />);
}
}
}

49
src/webview/types.d.ts vendored Normal file
View File

@@ -0,0 +1,49 @@
declare module '*.css' {
const content: string;
export default content;
}
declare module '*.scss' {
const content: string;
export default content;
}
// Image file declarations
declare module '*.png' {
const value: string;
export default value;
}
declare module '*.jpg' {
const value: string;
export default value;
}
declare module '*.jpeg' {
const value: string;
export default value;
}
declare module '*.gif' {
const value: string;
export default value;
}
declare module '*.svg' {
const value: string;
export default value;
}
// VS Code webview API
declare function acquireVsCodeApi(): {
postMessage(message: any): void;
getState(): any;
setState(state: any): void;
};
// Add csp property to React's iframe attributes
declare namespace React {
interface IframeHTMLAttributes<T> {
csp?: string;
}
}

View File

@@ -0,0 +1,179 @@
// Canvas view type definitions
export interface DesignFile {
name: string;
path: string;
content: string;
size: number;
modified: Date;
fileType: 'html' | 'svg'; // File type for proper rendering
// New hierarchy properties
version?: string; // e.g., "v1", "v2", "v3"
parentDesign?: string; // Reference to parent design file name
children?: string[]; // Array of child design file names
generation?: number; // 0 for root designs, 1 for first children, etc.
branchIndex?: number; // Index within the same generation/branch
}
export interface CanvasState {
designFiles: DesignFile[];
selectedFrames: string[];
isLoading: boolean;
error: string | null;
zoom: number;
pan: { x: number; y: number };
}
// Message types for communication between extension and webview
export interface ExtensionMessage {
command: string;
data?: any;
}
export interface LoadDesignFilesMessage extends ExtensionMessage {
command: 'loadDesignFiles';
}
export interface DesignFilesLoadedMessage extends ExtensionMessage {
command: 'designFilesLoaded';
data: {
files: DesignFile[];
};
}
export interface SelectFrameMessage extends ExtensionMessage {
command: 'selectFrame';
data: {
fileName: string;
};
}
export interface SetContextFromCanvasMessage extends ExtensionMessage {
command: 'setContextFromCanvas';
data: {
fileName: string;
type: 'frame' | 'clear';
};
}
export interface SetChatPromptMessage extends ExtensionMessage {
command: 'setChatPrompt';
data: {
prompt: string;
};
}
export interface ErrorMessage extends ExtensionMessage {
command: 'error';
data: {
error: string;
};
}
export interface FileWatchMessage extends ExtensionMessage {
command: 'fileChanged';
data: {
fileName: string;
changeType: 'created' | 'modified' | 'deleted';
};
}
export type WebviewMessage =
| LoadDesignFilesMessage
| SelectFrameMessage
| SetContextFromCanvasMessage
| SetChatPromptMessage;
export type ExtensionToWebviewMessage =
| DesignFilesLoadedMessage
| ErrorMessage
| FileWatchMessage;
// Canvas grid layout types
export interface GridPosition {
x: number;
y: number;
}
export interface FrameDimensions {
width: number;
height: number;
}
export type ViewportMode = 'desktop' | 'mobile' | 'tablet';
export interface ViewportConfig {
desktop: FrameDimensions;
mobile: FrameDimensions;
tablet: FrameDimensions;
}
export interface FrameViewportState {
[fileName: string]: ViewportMode;
}
export interface FramePositionState {
[fileName: string]: GridPosition;
}
export interface DragState {
isDragging: boolean;
draggedFrame: string | null;
startPosition: GridPosition;
currentPosition: GridPosition;
offset: GridPosition;
}
export interface CanvasConfig {
frameSize: FrameDimensions;
gridSpacing: number;
framesPerRow: number;
minZoom: number;
maxZoom: number;
// Responsive settings
responsive: {
enableScaling: boolean;
minFrameSize: FrameDimensions;
maxFrameSize: FrameDimensions;
scaleWithZoom: boolean;
};
// Viewport configurations
viewports: ViewportConfig;
// New hierarchy settings
hierarchy: {
horizontalSpacing: number; // Space between generations (horizontal)
verticalSpacing: number; // Space between siblings (vertical)
connectionLineWidth: number; // Width of connection lines
connectionLineColor: string; // Color of connection lines
showConnections: boolean; // Toggle connection visibility
};
}
// New types for hierarchical layout
export type LayoutMode = 'grid' | 'hierarchy';
export interface ConnectionLine {
id: string;
fromFrame: string;
toFrame: string;
fromPosition: GridPosition;
toPosition: GridPosition;
color?: string;
width?: number;
}
export interface HierarchyNode {
fileName: string;
position: GridPosition;
generation: number;
branchIndex: number;
parent?: string;
children: string[];
}
export interface HierarchyTree {
roots: string[];
nodes: Map<string, HierarchyNode>;
connections: ConnectionLine[];
bounds: { width: number; height: number };
}

View File

@@ -0,0 +1,466 @@
import { GridPosition, FrameDimensions, CanvasConfig, DesignFile, HierarchyTree, HierarchyNode, ConnectionLine } from '../types/canvas.types';
/**
* Calculate grid position for a frame based on its index
*/
export function calculateGridPosition(
index: number,
config: CanvasConfig
): GridPosition {
const row = Math.floor(index / config.framesPerRow);
const col = index % config.framesPerRow;
const x = col * (config.frameSize.width + config.gridSpacing);
const y = row * (config.frameSize.height + config.gridSpacing);
return { x, y };
}
/**
* Calculate total canvas bounds based on number of items
*/
export function calculateCanvasBounds(
itemCount: number,
config: CanvasConfig
): { width: number; height: number } {
if (itemCount === 0) {
return { width: 0, height: 0 };
}
const rows = Math.ceil(itemCount / config.framesPerRow);
const cols = Math.min(itemCount, config.framesPerRow);
const width = cols * config.frameSize.width + (cols - 1) * config.gridSpacing;
const height = rows * config.frameSize.height + (rows - 1) * config.gridSpacing;
return { width, height };
}
/**
* Calculate optimal fit-to-view scale and position
*/
export function calculateFitToView(
itemCount: number,
config: CanvasConfig,
containerWidth: number,
containerHeight: number,
padding: number = 50
): { scale: number; x: number; y: number } {
if (itemCount === 0) {
return { scale: 1, x: 0, y: 0 };
}
const bounds = calculateCanvasBounds(itemCount, config);
// Available space after padding
const availableWidth = containerWidth - 2 * padding;
const availableHeight = containerHeight - 2 * padding;
// Calculate scale to fit
const scaleX = availableWidth / bounds.width;
const scaleY = availableHeight / bounds.height;
const scale = Math.min(scaleX, scaleY, 1); // Don't zoom in beyond 100%
// Calculate centering position
const scaledWidth = bounds.width * scale;
const scaledHeight = bounds.height * scale;
const x = (containerWidth - scaledWidth) / 2;
const y = (containerHeight - scaledHeight) / 2;
return { scale, x, y };
}
/**
* Find the nearest frame to a given position
*/
export function findNearestFrame(
targetPosition: GridPosition,
itemCount: number,
config: CanvasConfig
): number | null {
if (itemCount === 0) {
return null;
}
let nearestIndex = 0;
let minDistance = Infinity;
for (let i = 0; i < itemCount; i++) {
const framePos = calculateGridPosition(i, config);
const distance = Math.sqrt(
Math.pow(framePos.x - targetPosition.x, 2) +
Math.pow(framePos.y - targetPosition.y, 2)
);
if (distance < minDistance) {
minDistance = distance;
nearestIndex = i;
}
}
return nearestIndex;
}
/**
* Generate layout configurations for different screen sizes
*/
export function generateResponsiveConfig(
baseConfig: CanvasConfig,
containerWidth: number
): CanvasConfig {
// Adjust frames per row based on container width (optimized for more frames)
let framesPerRow = baseConfig.framesPerRow;
let gridSpacing = baseConfig.gridSpacing;
if (containerWidth < 600) {
framesPerRow = 1;
gridSpacing = 30; // Tight spacing on mobile
} else if (containerWidth < 900) {
framesPerRow = 2;
gridSpacing = 40; // Moderate spacing on tablet
} else if (containerWidth < 1300) {
framesPerRow = 3;
gridSpacing = 45; // Good spacing for medium screens
} else if (containerWidth < 1800) {
framesPerRow = 4;
gridSpacing = 50; // Our default spacing
} else {
framesPerRow = 5; // Extra wide screens can fit 5 frames
gridSpacing = 60; // Slightly more breathing room
}
return {
...baseConfig,
framesPerRow,
gridSpacing
};
}
/**
* Calculate grid metrics for display
*/
export function getGridMetrics(
itemCount: number,
config: CanvasConfig
): {
rows: number;
cols: number;
totalFrames: number;
bounds: { width: number; height: number };
} {
const rows = Math.ceil(itemCount / config.framesPerRow);
const cols = Math.min(itemCount, config.framesPerRow);
const bounds = calculateCanvasBounds(itemCount, config);
return {
rows,
cols,
totalFrames: itemCount,
bounds
};
}
/**
* Build hierarchy tree from design files
*/
export function buildHierarchyTree(designs: DesignFile[]): HierarchyTree {
const nodes = new Map<string, HierarchyNode>();
const roots: string[] = [];
const connections: ConnectionLine[] = [];
// First pass: Create nodes and identify roots
designs.forEach(design => {
const node: HierarchyNode = {
fileName: design.name,
position: { x: 0, y: 0 }, // Will be calculated later
generation: design.generation || 0,
branchIndex: design.branchIndex || 0,
parent: design.parentDesign,
children: design.children || []
};
nodes.set(design.name, node);
if (!design.parentDesign) {
roots.push(design.name);
}
});
// Second pass: Build connections
nodes.forEach((node, fileName) => {
if (node.parent && nodes.has(node.parent)) {
const parentNode = nodes.get(node.parent)!;
connections.push({
id: `${node.parent}-${fileName}`,
fromFrame: node.parent,
toFrame: fileName,
fromPosition: parentNode.position,
toPosition: node.position
});
}
});
return {
roots,
nodes,
connections,
bounds: { width: 0, height: 0 }
};
}
/**
* Calculate hierarchical positions for design tree
*/
export function calculateHierarchyPositions(
tree: HierarchyTree,
config: CanvasConfig,
actualFrameDimensions?: { width: number; height: number }
): HierarchyTree {
const { horizontalSpacing, verticalSpacing } = config.hierarchy;
// Use actual frame dimensions if provided, otherwise fall back to config or defaults
const frameWidth = actualFrameDimensions?.width || Math.max(config.frameSize.width, 400);
const frameHeight = actualFrameDimensions?.height || Math.max(config.frameSize.height, 550);
// Position root nodes first with generous spacing
let currentRootY = 100; // Start with some padding
tree.roots.forEach(rootName => {
const rootNode = tree.nodes.get(rootName)!;
rootNode.position = {
x: 50, // Start with some padding from left edge
y: currentRootY
};
// Calculate subtree height to determine spacing for next root
const subtreeHeight = calculateSubtreeHeight(rootNode, tree.nodes, config, { width: frameWidth, height: frameHeight });
// Position children recursively
const nextAvailableY = positionChildrenImproved(rootNode, tree.nodes, config, currentRootY, { width: frameWidth, height: frameHeight });
// Update position for next root with large spacing to avoid overlaps
currentRootY = Math.max(
currentRootY + frameHeight + verticalSpacing * 2,
nextAvailableY + verticalSpacing * 2
);
});
// Update connection positions
tree.connections.forEach(connection => {
const fromNode = tree.nodes.get(connection.fromFrame);
const toNode = tree.nodes.get(connection.toFrame);
if (fromNode && toNode) {
connection.fromPosition = {
x: fromNode.position.x + frameWidth,
y: fromNode.position.y + frameHeight / 2
};
connection.toPosition = {
x: toNode.position.x,
y: toNode.position.y + frameHeight / 2
};
}
});
// Calculate total bounds
let maxX = 0, maxY = 0;
tree.nodes.forEach(node => {
maxX = Math.max(maxX, node.position.x + frameWidth + 100);
maxY = Math.max(maxY, node.position.y + frameHeight + 100);
});
tree.bounds = { width: maxX, height: maxY };
return tree;
}
/**
* Calculate the total height needed for a subtree
*/
function calculateSubtreeHeight(
node: HierarchyNode,
nodes: Map<string, HierarchyNode>,
config: CanvasConfig,
frameDimensions: { width: number; height: number }
): number {
const { verticalSpacing } = config.hierarchy;
const frameHeight = frameDimensions.height;
const children = node.children
.map(childName => nodes.get(childName))
.filter(child => child !== undefined) as HierarchyNode[];
if (children.length === 0) {
return frameHeight;
}
// Calculate total height needed for all children
let totalChildrenHeight = 0;
children.forEach(child => {
totalChildrenHeight += calculateSubtreeHeight(child, nodes, config, frameDimensions);
});
// Add spacing between children
totalChildrenHeight += (children.length - 1) * verticalSpacing;
return Math.max(frameHeight, totalChildrenHeight);
}
/**
* Position children nodes recursively without overlaps
*/
function positionChildrenImproved(
parentNode: HierarchyNode,
nodes: Map<string, HierarchyNode>,
config: CanvasConfig,
startY: number,
frameDimensions: { width: number; height: number }
): number {
const { horizontalSpacing, verticalSpacing } = config.hierarchy;
// Use passed frame dimensions
const frameWidth = frameDimensions.width;
const frameHeight = frameDimensions.height;
const children = parentNode.children
.map(childName => nodes.get(childName))
.filter(child => child !== undefined) as HierarchyNode[];
if (children.length === 0) {return startY + frameHeight;}
let currentY = startY;
// Position each child without overlapping
children.forEach((child) => {
child.position = {
x: parentNode.position.x + frameWidth + horizontalSpacing,
y: currentY
};
// Recursively position grandchildren and get the next available Y
const nextY = positionChildrenImproved(child, nodes, config, currentY, frameDimensions);
// Move to next position with generous spacing to avoid overlaps
currentY = Math.max(currentY + frameHeight + verticalSpacing, nextY + verticalSpacing);
});
return currentY;
}
/**
* Get hierarchical position for a specific design
*/
export function getHierarchicalPosition(
fileName: string,
tree: HierarchyTree
): GridPosition {
const node = tree.nodes.get(fileName);
return node ? node.position : { x: 0, y: 0 };
}
/**
* Parse hierarchical path from filename (e.g., "text_1_3_1.html" -> ["text", "1", "3", "1"])
*/
export function parseHierarchicalPath(filename: string): string[] {
// Remove file extension
const nameWithoutExt = filename.replace(/\.[^/.]+$/, "");
// Split by underscores
const parts = nameWithoutExt.split('_');
return parts;
}
/**
* Get parent path from hierarchical filename (e.g., "text_1_3_1" -> "text_1_3")
*/
export function getParentPath(filename: string): string | null {
const parts = parseHierarchicalPath(filename);
// If only one part or two parts (like "text_1"), it's a root
if (parts.length <= 2) {
return null;
}
// Remove the last number to get parent path
const parentParts = parts.slice(0, -1);
return parentParts.join('_');
}
/**
* Get generation level from hierarchical filename (e.g., "text_1_3_1" -> 2, "text_1" -> 0)
*/
export function getGenerationLevel(filename: string): number {
const parts = parseHierarchicalPath(filename);
// Generation is the number of numeric parts minus 1
// text_1 -> generation 0
// text_1_3 -> generation 1
// text_1_3_1 -> generation 2
const numericParts = parts.slice(1); // Skip the first part (like "text")
return Math.max(0, numericParts.length - 1);
}
/**
* Get version at current level (e.g., "text_1_3_1" -> "1", "text_1_3" -> "3")
*/
export function getCurrentLevelVersion(filename: string): string {
const parts = parseHierarchicalPath(filename);
// Return the last part (current level version)
return parts[parts.length - 1];
}
/**
* Detect design relationships based on hierarchical naming patterns
*/
export function detectDesignRelationships(designs: DesignFile[]): DesignFile[] {
const updatedDesigns = designs.map(design => ({ ...design }));
// Create a map for quick lookup
const designMap = new Map<string, DesignFile>();
updatedDesigns.forEach(design => {
// Use filename without extension as the key
const nameWithoutExt = design.name.replace(/\.[^/.]+$/, "");
designMap.set(nameWithoutExt, design);
});
// Auto-detect versions and relationships
updatedDesigns.forEach(design => {
const nameWithoutExt = design.name.replace(/\.[^/.]+$/, "");
// Set version (current level version)
design.version = getCurrentLevelVersion(design.name);
// Set generation level
design.generation = getGenerationLevel(design.name);
// Find parent
const parentPath = getParentPath(design.name);
if (parentPath) {
const parentDesign = designMap.get(parentPath);
if (parentDesign) {
design.parentDesign = parentDesign.name;
// Add this design as a child to parent
if (!parentDesign.children) {
parentDesign.children = [];
}
if (!parentDesign.children.includes(design.name)) {
parentDesign.children.push(design.name);
}
}
}
// Set branch index (order among siblings)
if (design.parentDesign) {
const parentDesign = designMap.get(getParentPath(design.name)!);
if (parentDesign && parentDesign.children) {
design.branchIndex = parentDesign.children.indexOf(design.name);
}
} else {
// For root designs, use the version number as branch index
design.branchIndex = parseInt(design.version) - 1;
}
});
return updatedDesigns;
}

View File

@@ -0,0 +1,356 @@
interface ParsedTheme {
variables: Record<string, string>;
lightMode: ThemeColors;
darkMode?: ThemeColors;
fonts: {
sans: string;
serif: string;
mono: string;
};
radius: string;
shadows: Record<string, string>;
spacing: string;
}
interface ThemeColors {
// Basic colors
background: string;
foreground: string;
card: string;
cardForeground: string;
popover: string;
popoverForeground: string;
// Semantic colors
primary: string;
primaryForeground: string;
secondary: string;
secondaryForeground: string;
muted: string;
mutedForeground: string;
accent: string;
accentForeground: string;
destructive: string;
destructiveForeground: string;
// Form elements
border: string;
input: string;
ring: string;
// Charts
chart1: string;
chart2: string;
chart3: string;
chart4: string;
chart5: string;
// Sidebar
sidebar: string;
sidebarForeground: string;
sidebarPrimary: string;
sidebarPrimaryForeground: string;
sidebarAccent: string;
sidebarAccentForeground: string;
sidebarBorder: string;
sidebarRing: string;
}
export type { ParsedTheme, ThemeColors };
export function parseThemeCSS(cssSheet: string): ParsedTheme {
const rootMatch = cssSheet.match(/:root\s*\{([^}]+)\}/);
const darkMatch = cssSheet.match(/\.dark\s*\{([^}]+)\}/);
if (!rootMatch) {
throw new Error('No :root selector found in CSS');
}
const lightMode = parseCSSVariables(rootMatch[1]);
const darkMode = darkMatch ? parseCSSVariables(darkMatch[1]) : undefined;
return {
variables: lightMode.raw,
lightMode: mapToThemeColors(lightMode.raw),
darkMode: darkMode ? mapToThemeColors(darkMode.raw) : undefined,
fonts: {
sans: lightMode.raw['--font-sans'] || 'system-ui, sans-serif',
serif: lightMode.raw['--font-serif'] || 'Georgia, serif',
mono: lightMode.raw['--font-mono'] || 'Monaco, monospace'
},
radius: lightMode.raw['--radius'] || '0.5rem',
shadows: extractShadows(lightMode.raw),
spacing: lightMode.raw['--spacing'] || '0.25rem'
};
}
function parseCSSVariables(cssBlock: string): { raw: Record<string, string> } {
const variables: Record<string, string> = {};
// Match CSS custom properties
const variableRegex = /--([^:]+):\s*([^;]+);/g;
let match;
while ((match = variableRegex.exec(cssBlock)) !== null) {
const property = `--${match[1].trim()}`;
const value = match[2].trim();
variables[property] = value;
}
return { raw: variables };
}
function mapToThemeColors(variables: Record<string, string>): ThemeColors {
return {
background: variables['--background'] || 'white',
foreground: variables['--foreground'] || 'black',
card: variables['--card'] || 'white',
cardForeground: variables['--card-foreground'] || 'black',
popover: variables['--popover'] || 'white',
popoverForeground: variables['--popover-foreground'] || 'black',
primary: variables['--primary'] || 'blue',
primaryForeground: variables['--primary-foreground'] || 'white',
secondary: variables['--secondary'] || 'gray',
secondaryForeground: variables['--secondary-foreground'] || 'black',
muted: variables['--muted'] || 'lightgray',
mutedForeground: variables['--muted-foreground'] || 'gray',
accent: variables['--accent'] || 'blue',
accentForeground: variables['--accent-foreground'] || 'white',
destructive: variables['--destructive'] || 'red',
destructiveForeground: variables['--destructive-foreground'] || 'white',
border: variables['--border'] || 'lightgray',
input: variables['--input'] || 'white',
ring: variables['--ring'] || 'blue',
chart1: variables['--chart-1'] || 'blue',
chart2: variables['--chart-2'] || 'green',
chart3: variables['--chart-3'] || 'yellow',
chart4: variables['--chart-4'] || 'red',
chart5: variables['--chart-5'] || 'purple',
sidebar: variables['--sidebar'] || 'white',
sidebarForeground: variables['--sidebar-foreground'] || 'black',
sidebarPrimary: variables['--sidebar-primary'] || 'blue',
sidebarPrimaryForeground: variables['--sidebar-primary-foreground'] || 'white',
sidebarAccent: variables['--sidebar-accent'] || 'lightgray',
sidebarAccentForeground: variables['--sidebar-accent-foreground'] || 'black',
sidebarBorder: variables['--sidebar-border'] || 'lightgray',
sidebarRing: variables['--sidebar-ring'] || 'blue'
};
}
function extractShadows(variables: Record<string, string>): Record<string, string> {
const shadows: Record<string, string> = {};
const shadowKeys = ['shadow-2xs', 'shadow-xs', 'shadow-sm', 'shadow', 'shadow-md', 'shadow-lg', 'shadow-xl', 'shadow-2xl'];
shadowKeys.forEach(key => {
const varKey = `--${key}`;
if (variables[varKey]) {
shadows[key] = variables[varKey];
}
});
return shadows;
}
export function generatePreviewCSS(theme: ParsedTheme, mode: 'light' | 'dark'): string {
const colors = mode === 'dark' && theme.darkMode ? theme.darkMode : theme.lightMode;
return `
.theme-preview {
background-color: ${colors.background};
color: ${colors.foreground};
font-family: ${theme.fonts.sans};
border-radius: ${theme.radius};
padding: ${theme.spacing};
min-height: 200px;
border: 1px solid ${colors.border};
}
.theme-preview .btn {
padding: 0.5rem 1rem;
border-radius: ${theme.radius};
border: 1px solid ${colors.border};
font-family: ${theme.fonts.sans};
cursor: pointer;
transition: all 0.2s;
margin: 0.25rem;
display: inline-block;
text-decoration: none;
font-size: 0.875rem;
font-weight: 500;
}
.theme-preview .btn:hover {
opacity: 0.9;
}
.theme-preview .btn-primary {
background-color: ${colors.primary};
color: ${colors.primaryForeground};
border-color: ${colors.primary};
}
.theme-preview .btn-secondary {
background-color: ${colors.secondary};
color: ${colors.secondaryForeground};
border-color: ${colors.secondary};
}
.theme-preview .btn-destructive {
background-color: ${colors.destructive};
color: ${colors.destructiveForeground};
border-color: ${colors.destructive};
}
.theme-preview .preview-card {
background-color: ${colors.card};
color: ${colors.cardForeground};
border: 1px solid ${colors.border};
border-radius: ${theme.radius};
padding: 1rem;
margin: 1rem 0;
box-shadow: ${theme.shadows['shadow'] || 'none'};
}
.theme-preview .preview-card h3 {
margin: 0 0 0.5rem 0;
font-size: 1.125rem;
font-weight: 600;
}
.theme-preview .preview-card p {
margin: 0 0 1rem 0;
line-height: 1.5;
}
.theme-preview .card-actions {
display: flex;
gap: 0.5rem;
margin-top: 1rem;
}
.theme-preview .input {
background-color: ${colors.input};
color: ${colors.foreground};
border: 1px solid ${colors.border};
border-radius: ${theme.radius};
padding: 0.5rem;
font-family: ${theme.fonts.sans};
width: 100%;
margin: 0.25rem 0;
font-size: 0.875rem;
}
.theme-preview .input:focus {
outline: 2px solid ${colors.ring};
outline-offset: 2px;
border-color: ${colors.ring};
}
.theme-preview .input::placeholder {
color: ${colors.mutedForeground};
}
.theme-preview .text-muted {
color: ${colors.mutedForeground};
}
.theme-preview .preview-nav {
background-color: ${colors.sidebar};
color: ${colors.sidebarForeground};
border: 1px solid ${colors.sidebarBorder};
border-radius: ${theme.radius};
padding: 1rem;
display: flex;
justify-content: space-between;
align-items: center;
margin: 1rem 0;
}
.theme-preview .nav-brand {
font-weight: 600;
font-size: 1.125rem;
}
.theme-preview .nav-links {
display: flex;
gap: 0.5rem;
}
.theme-preview .nav-link {
color: ${colors.sidebarForeground};
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: ${theme.radius};
font-size: 0.875rem;
transition: all 0.2s;
}
.theme-preview .nav-link:hover {
background-color: ${colors.sidebarAccent};
color: ${colors.sidebarAccentForeground};
}
.theme-preview .nav-link.active {
background-color: ${colors.sidebarPrimary};
color: ${colors.sidebarPrimaryForeground};
}
.theme-preview .preview-form {
padding: 1rem 0;
}
.theme-preview .preview-buttons {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
padding: 1rem 0;
}
`;
}
export function extractColorPalette(theme: ParsedTheme): Array<{name: string, value: string, category: string}> {
const palette: Array<{name: string, value: string, category: string}> = [];
const colors = theme.lightMode;
// Primary colors
palette.push(
{ name: 'Primary', value: colors.primary, category: 'Brand' },
{ name: 'Primary Foreground', value: colors.primaryForeground, category: 'Brand' },
{ name: 'Secondary', value: colors.secondary, category: 'Brand' },
{ name: 'Secondary Foreground', value: colors.secondaryForeground, category: 'Brand' }
);
// Background colors
palette.push(
{ name: 'Background', value: colors.background, category: 'Surface' },
{ name: 'Foreground', value: colors.foreground, category: 'Surface' },
{ name: 'Card', value: colors.card, category: 'Surface' },
{ name: 'Card Foreground', value: colors.cardForeground, category: 'Surface' }
);
// Interactive colors
palette.push(
{ name: 'Accent', value: colors.accent, category: 'Interactive' },
{ name: 'Destructive', value: colors.destructive, category: 'Interactive' },
{ name: 'Border', value: colors.border, category: 'Interactive' },
{ name: 'Ring', value: colors.ring, category: 'Interactive' }
);
// Chart colors
palette.push(
{ name: 'Chart 1', value: colors.chart1, category: 'Data' },
{ name: 'Chart 2', value: colors.chart2, category: 'Data' },
{ name: 'Chart 3', value: colors.chart3, category: 'Data' },
{ name: 'Chart 4', value: colors.chart4, category: 'Data' },
{ name: 'Chart 5', value: colors.chart5, category: 'Data' }
);
return palette;
}
// Utility function to convert OKLCH to hex for better compatibility
export function oklchToHex(oklchValue: string): string {
// For now, return the original value
// In a real implementation, you'd convert OKLCH to hex
return oklchValue;
}

32
tsconfig.json Normal file
View File

@@ -0,0 +1,32 @@
{
"compilerOptions": {
"module": "ESNext",
"target": "ES2020",
"outDir": "dist",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"sourceMap": true,
"rootDir": "src",
"strict": true,
"jsx": "react-jsx",
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"allowSyntheticDefaultImports": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"declaration": false,
"noEmit": false
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist",
".vscode-test"
]
/* Additional Checks */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
}

16
tsconfig.test.json Normal file
View File

@@ -0,0 +1,16 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "CommonJS",
"target": "ES2020",
"outDir": "dist-test",
"moduleResolution": "node",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"noEmit": false
},
"include": [
"src/test/**/*",
"src/core/**/*"
]
}

BIN
v0.0.11.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB