diff --git a/.gitignore b/.gitignore
index 4dbe70c8..69fd76c0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -27,6 +27,7 @@ npm-debug.log*
# Claude Code settings (contains private configuration)
.claude/
.vscode/workflows/
+.mcp.json
# Temporary files
*.tmp
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f0715683..c0d60646 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,17 @@
+## [3.22.0](https://github.com/breaking-brake/cc-wf-studio/compare/v3.21.0...v3.22.0) (2026-02-16)
+
+### Features
+
+* add Gemini CLI provider integration ([#565](https://github.com/breaking-brake/cc-wf-studio/issues/565)) ([8b0fd76](https://github.com/breaking-brake/cc-wf-studio/commit/8b0fd7643c12c817d723b25f68e60d56cb4dff94))
+
+### Bug Fixes
+
+* clarify connection constraints for parallel execution support ([#568](https://github.com/breaking-brake/cc-wf-studio/issues/568)) ([8f9d58f](https://github.com/breaking-brake/cc-wf-studio/commit/8f9d58fdbf9b712a79d5852f5a7895bab97a7f28)), closes [#564](https://github.com/breaking-brake/cc-wf-studio/issues/564)
+
+### Improvements
+
+* enhance workflow schema for AI editing quality ([#559](https://github.com/breaking-brake/cc-wf-studio/issues/559)) ([1a53ccc](https://github.com/breaking-brake/cc-wf-studio/commit/1a53ccc6d406727a70d5f727ffc1fbaf557dccb3))
+
## [3.21.0](https://github.com/breaking-brake/cc-wf-studio/compare/v3.20.0...v3.21.0) (2026-02-08)
### Features
diff --git a/CLAUDE.md b/CLAUDE.md
index b03e58d2..b9bd5ef0 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -6,13 +6,8 @@ Auto-generated from all feature plans. Last updated: 2025-11-01
- ローカルファイルシステム (`.vscode/workflows/*.json`, `.claude/skills/*.md`, `.claude/commands/*.md`) (001-cc-wf-studio)
- TypeScript 5.3 (VSCode Extension Host), React 18.2 (Webview UI) (001-node-types-extension)
- ローカルファイルシステム (`.vscode/workflows/*.json`) (001-node-types-extension)
-- TypeScript 5.3 (Extension Host & Webview shared types), React 18.2 (Webview UI) (001-ai-workflow-generation)
-- File system (workflow schema JSON in resources/, generated workflows in canvas state) (001-ai-workflow-generation)
- TypeScript 5.3.0 (001-skill-node)
- File system (SKILL.md files in `~/.claude/skills/` and `.claude/skills/`), workflow JSON files in `.vscode/workflows/` (001-skill-node)
-- TypeScript 5.3 (Extension Host), React 18.2 (Webview UI) (001-ai-skill-generation)
-- File system (existing SKILL.md files in `~/.claude/skills/` and `.claude/skills/`, workflow-schema.json in resources/) (001-ai-skill-generation)
-- Workflow JSON files in `.vscode/workflows/` directory (conversation history embedded in workflow metadata) (001-ai-workflow-refinement)
- TypeScript 5.3.0 (VSCode Extension Host), TypeScript/React 18.2 (Webview UI) + VSCode Extension API 1.80.0+, React 18.2, React Flow (visual canvas), Zustand (state management), child_process (Claude Code CLI execution) (001-mcp-node)
- Workflow JSON files in `.vscode/workflows/` directory, Claude Code MCP configuration (user/project/enterprise scopes) (001-mcp-node)
- TypeScript 5.3.0 (VSCode Extension Host), TypeScript/React 18.2 (Webview UI) + VSCode Extension API 1.80.0+, React 18.2, React Flow (visual canvas), Zustand (state management), existing MCP SDK client services (001-mcp-natural-language-mode)
@@ -205,6 +200,57 @@ TypeScript 5.x (VSCode Extension Host), React 18.x (Webview UI): Follow standard
+## Planning Guidelines
+
+### Gather Context from github-knowledge MCP (Required for Plan Mode)
+
+**When entering Plan Mode to design or plan implementation, always gather related knowledge from the github-knowledge MCP first.**
+
+#### Steps
+1. Use `search_decisions` to find past technical decisions by relevant modules, tags, or keywords
+2. Use `search_domain_knowledge` to find related business rules, domain terms, and constraints
+3. Use `get_decision_detail` as needed for full context on specific decisions
+4. Use `get_module_history` as needed to understand how a module has evolved
+
+#### Purpose
+- Maintain consistency with past technical decisions
+- Avoid re-proposing previously rejected alternatives
+- Incorporate domain knowledge (business rules, constraints) into design
+- Align implementation with established architectural patterns
+
+## AI Editing Features
+
+### MCP Server-based AI Editing (Active)
+- The built-in MCP server (`cc-workflow-ai-editor` skill) is the primary interface for external AI agents to create and edit workflows.
+- All new AI editing development should go through the MCP server approach.
+
+```mermaid
+sequenceDiagram
+ actor User
+ participant VSCode as CC Workflow Studio
+ participant MCP as MCP Server
+ participant Agent as AI Agent
+
+ User->>VSCode: Click agent button
+ VSCode->>MCP: Auto start server
+ VSCode->>Agent: Launch with editing skill
+
+ loop AI edits workflow
+ Agent->>MCP: get_workflow
+ MCP-->>Agent: workflow JSON
+ Agent->>MCP: apply_workflow
+ MCP->>VSCode: Update canvas
+ end
+```
+
+### Chat UI-based AI Editing (Discontinued)
+- The chat UI-based AI editing features (Refinement Chat Panel, AI Workflow Generation Dialog) are **no longer under active development**.
+- Existing functionality will be maintained but no new features or enhancements will be added.
+- Affected features:
+ - `001-ai-workflow-generation`: AI Workflow Generation via AiGenerationDialog
+ - `001-ai-workflow-refinement`: AI Workflow Refinement via RefinementChatPanel
+ - `001-ai-skill-generation`: AI Skill Node Generation via AiGenerationDialog
+
## Architecture Sequence Diagrams
このセクションでは、cc-wf-studioの主要なデータフローをMermaid形式のシーケンス図で説明します。
@@ -263,45 +309,6 @@ sequenceDiagram
Toolbar->>User: Show notification
```
-### AI ワークフロー改善フロー (Refinement)
-
-```mermaid
-sequenceDiagram
- actor User
- participant Panel as RefinementChatPanel.tsx
- participant Store as refinement-store.ts
- participant Cmd as workflow-refinement.ts
- participant Svc as refinement-service.ts
- participant CLI as claude-code-service.ts
-
- User->>Panel: Enter refinement request
- Panel->>Store: addMessage(userMessage)
- Panel->>Store: addLoadingAiMessage()
- Panel->>Cmd: postMessage(REFINE_WORKFLOW)
- Cmd->>Svc: refineWorkflow(workflow, history, onProgress)
- Svc->>Svc: buildPromptWithHistory()
- Svc->>CLI: executeClaudeCodeCLIStreaming()
-
- Note over CLI,Panel: Streaming Phase
- loop For each chunk
- CLI->>Svc: onProgress(chunk)
- Svc->>Cmd: Parse chunk & extract text
- Cmd->>Panel: postMessage(REFINEMENT_PROGRESS)
- Panel->>Store: updateMessageContent()
- Store->>Panel: Update UI in real-time
- Panel->>User: Display AI response progressively
- end
-
- CLI-->>Svc: Refined workflow JSON
- Svc->>Svc: validateRefinedWorkflow()
- Svc-->>Cmd: result
- Cmd->>Panel: postMessage(REFINEMENT_SUCCESS)
- Panel->>Store: updateWorkflow() & updateMessageContent()
- Store->>Store: updateConversationHistory()
- Store->>Panel: Update canvas & chat
- Panel->>User: Show refined workflow
-```
-
### Slack ワークフロー共有フロー
```mermaid
@@ -392,161 +399,6 @@ sequenceDiagram
---
-## AI-Assisted Skill Node Generation (Feature 001-ai-skill-generation)
-
-### Key Files and Components
-
-#### Extension Host Services
-- **src/extension/services/skill-relevance-matcher.ts**
- - Calculates relevance scores between user descriptions and Skills using keyword matching
- - `tokenize()`: Removes stopwords, filters by min length (3 chars)
- - `calculateSkillRelevance()`: Formula: `score = |intersection| / sqrt(|userTokens| * |skillTokens|)`
- - `filterSkillsByRelevance()`: Filters by threshold (0.6), limits to 20, prefers project scope
- - No new library dependencies (per user constraint)
-
-- **src/extension/commands/ai-generation.ts** (Enhanced)
- - Scans personal + project Skills in parallel (`Promise.all`)
- - Filters Skills by relevance to user description
- - Constructs AI prompt with "Available Skills" section (JSON format)
- - Resolves `skillPath` post-generation for AI-generated Skill nodes
- - Marks missing Skills as `validationStatus: 'missing'`
-
-- **src/extension/utils/validate-workflow.ts** (Extended)
- - `validateSkillNode()`: Validates required fields, name format, length constraints
- - Error codes: SKILL_MISSING_FIELD, SKILL_INVALID_NAME, SKILL_NAME_TOO_LONG, etc.
- - Integrated into `validateAIGeneratedWorkflow()` flow
-
-#### Resources
-- **resources/workflow-schema.json** (Updated)
- - Added Skill node type documentation (~1.5KB addition)
- - Instructions for AI: "Use when user description matches Skill's purpose"
- - Field descriptions: name, description, scope, skillPath (auto-resolved), validationStatus
- - File size: 16.5KB (within tolerance)
-
-### Message Flow
-```
-Webview (AiGenerationDialog)
- → postMessage(GENERATE_WORKFLOW)
- → Extension (ai-generation.ts)
- → scanAllSkills() + loadWorkflowSchema() (parallel)
- → filterSkillsByRelevance(userDescription, availableSkills)
- → constructPrompt(description, schema, filteredSkills)
- → ClaudeCodeService.executeClaudeCodeCLI()
- → Parse & resolveSkillPaths(workflow, availableSkills)
- → Validate (including Skill nodes)
- → postMessage(GENERATION_SUCCESS | GENERATION_FAILED)
- → Webview (workflow-store.addGeneratedWorkflow())
-```
-
-### Key Constraints
-- Max 20 Skills in AI prompt (prevent timeout)
-- Relevance threshold: 0.3 (30%) - tested 0.5 but 0.3 provides better recall without sacrificing quality
-- Keyword matching: O(n+m) complexity
-- Duplicate handling: Project scope preferred over personal
-- Generation timeout: 90 seconds
-
-### Error Handling
-- Skill not found → `validationStatus: 'missing'`
-- Skill file malformed → `validationStatus: 'invalid'`
-- All errors logged to "CC Workflow Studio" Output Channel
-
-### Design Decisions & Lessons Learned
-
-**Phase 5 (User Skill Selection) - Rejected**
-
-During development, we attempted to implement a UI feature allowing users to manually select which Skills to include/exclude in AI generation. This was intended to prevent timeouts when users have many Skills installed.
-
-**Why it was rejected:**
-- **AI generation control has inherent limitations**: The AI prompt is a "suggestion" not a "command"
-- **Unpredictable behavior**: Even when Skills are excluded from the prompt, the AI may still generate Skill nodes based on its own interpretation of the user's description
-- **Poor UX**: Users selecting "don't use this Skill" would experience confusion when the AI uses it anyway
-- **Uncontrollable AI behavior**: The final decision of which nodes to generate belongs to the AI, not the prompt engineering
-
-**Key lesson:**
-> Do not implement user-facing features that promise control over AI behavior that cannot be guaranteed. AI generation is inherently probabilistic, and features requiring deterministic outcomes should be avoided.
-
-**Alternative approaches for timeout prevention:**
-- Dynamic timeout adjustment based on Skill count
-- Adaptive relevance threshold tuning (e.g., 0.3 → 0.5 for high Skill counts)
-- Maintain strict MAX_SKILLS_IN_PROMPT limit (currently 20)
-
----
-
-## AI-Assisted Workflow Generation (Feature 001-ai-workflow-generation)
-
-### Key Files and Components
-
-#### Extension Host Services
-- **src/extension/services/claude-code-service.ts**
- - Executes Claude Code CLI via child_process.spawn()
- - Handles timeout (30s default), error mapping (COMMAND_NOT_FOUND, TIMEOUT, etc.)
- - Includes comprehensive logging to VSCode Output Channel
-
-- **src/extension/services/schema-loader-service.ts**
- - Loads workflow-schema.json from resources/ directory
- - Implements in-memory caching for performance
- - Provides schema to AI for context during generation
-
-- **src/extension/commands/ai-generation.ts**
- - Main command handler for GENERATE_WORKFLOW messages from Webview
- - Orchestrates: schema loading → CLI execution → parsing → validation
- - Sends success/failure messages back to Webview with execution metrics
-
-- **src/extension/utils/validate-workflow.ts**
- - Validates AI-generated workflows against VALIDATION_RULES
- - Checks node count (<50), connection validity, required fields
- - Returns structured validation errors for user feedback
-
-#### Webview Components
-- **src/webview/src/services/ai-generation-service.ts**
- - Bridge between Webview UI and Extension Host
- - Sends GENERATE_WORKFLOW messages via postMessage
- - Returns Promise that resolves to workflow or AIGenerationError
-
-- **src/webview/src/components/dialogs/AiGenerationDialog.tsx**
- - Modal dialog for user description input (max 2000 chars)
- - Handles loading states, error display, success notifications
- - Fully internationalized (5 languages: en, ja, ko, zh-CN, zh-TW)
- - Keyboard shortcuts: Ctrl/Cmd+Enter (generate), Esc (cancel)
-
-#### Resources
-- **resources/workflow-schema.json**
- - Comprehensive schema documentation for AI context
- - Documents all 7 node types (Start, End, Prompt, SubAgent, AskUserQuestion, IfElse, Switch)
- - Includes validation rules and 3 example workflows
- - Size: <10KB (optimized for token efficiency)
- - **IMPORTANT**: Included in VSIX package (not excluded by .vscodeignore)
-
-#### Documentation
-- **docs/schema-maintenance.md**
- - Maintenance guide for workflow-schema.json
- - Synchronization procedures between TypeScript types and JSON schema
- - Update workflows, validation rules mapping, common tasks
- - File size optimization guidelines (target <10KB, max 15KB)
-
-### Message Flow
-```
-Webview (AiGenerationDialog)
- → postMessage(GENERATE_WORKFLOW)
- → Extension (ai-generation.ts)
- → ClaudeCodeService.executeClaudeCodeCLI()
- → Parse & Validate
- → postMessage(GENERATION_SUCCESS | GENERATION_FAILED)
- → Webview (workflow-store.addGeneratedWorkflow())
-```
-
-### Error Handling
-- All errors mapped to specific error codes for i18n
-- Comprehensive logging to "CC Workflow Studio" Output Channel
-- Execution time tracking for all operations (success and failure)
-
-### Testing Notes
-- T052-T054: Manual testing scenarios (simple/medium/complex workflows, error scenarios)
-- T055: VSCode Output Channel logging implemented ✓
-- Unit/integration tests deferred (T011-T015, T019, T023, T028, T032, T035, T040)
-
----
-
## Dialog Component Design Guidelines
### ライブラリ選択
diff --git a/README.md b/README.md
index 006bdd31..fb4eadaf 100644
--- a/README.md
+++ b/README.md
@@ -17,11 +17,25 @@
- Accelerate Claude Code/GitHub Copilot(※1)/OpenAI Codex(※2)/Roo Code(※3) automation with a visual workflow editor
+ Visual Agentic Engineering Toolkit for AI Coding Agents
+
+
+| Agent | Export Format | Requires |
+|-------|--------------|----------|
+| Claude Code | `.claude/agents/` `.claude/commands/` | [Claude Code](https://docs.anthropic.com/en/docs/claude-code) |
+| GitHub Copilot (β) | `.github/prompts/` `.github/skills/` | [Copilot Chat](https://marketplace.visualstudio.com/items?itemName=GitHub.copilot-chat) or [Copilot CLI](https://docs.github.com/en/copilot/concepts/agents/about-copilot-cli) |
+| OpenAI Codex CLI (β) | `.codex/skills/` | [Codex CLI](https://github.com/openai/codex) |
+| Roo Code (β) | `.roo/skills/` | [Roo Code](https://marketplace.visualstudio.com/items?itemName=RooVeterinaryInc.roo-cline) |
+| Gemini CLI (β) | `.gemini/skills/` | [Gemini CLI](https://github.com/google-gemini/gemini-cli) |
+
+> **Note:** β-supported agents require activation from Toolbar's **More** menu. Some workflows may not work as expected.
+
- Design complex AI agent workflows by conversing with AI – or use intuitive drag-and-drop. Build Sub-Agent orchestrations and conditional branching with natural language, then export directly to .claude format.
+ Design, orchestrate, and run AI agent workflows by conversing with AI – or use intuitive drag-and-drop.
+ Build agent orchestrations and conditional branching with natural language.
+ Export as custom slash commands or agent skills and run directly in your favorite AI coding agent.
@@ -53,37 +67,13 @@
## Key Features
-🔀 **Visual Workflow Editor** - Intuitive drag-and-drop canvas for designing AI workflows without code
-
-✨ **Edit with AI** - Iteratively improve workflows through conversational AI - ask for changes, add features, or refine logic with natural language feedback
-
-⚡ **One-Click Export & Run** - Export workflows to ready-to-use formats and run directly from the editor:
- - **Claude Code**: `.claude/agents/` and `.claude/commands/`
- - **GitHub Copilot Chat**(※1): `.github/prompts/`
- - **GitHub Copilot CLI**(※1): `.github/skills/`
- - **OpenAI Codex CLI**(※2): `.codex/skills/`
- - **Roo Code**(※3): `.roo/skills/`
-
-🤖 **GitHub Copilot Support (※1 β)** - Export & Run workflows to Copilot Chat or Copilot CLI, and use Copilot as AI provider for Edit with AI.
-
- **Note:**
- - Enable **Copilot** option in Toolbar's **More** menu to activate
- - Requires [GitHub Copilot Chat](https://marketplace.visualstudio.com/items?itemName=GitHub.copilot-chat) extension or [Copilot CLI](https://docs.github.com/en/copilot/concepts/agents/about-copilot-cli) to be installed
- - Experimental feature; some workflows may not work as expected
+🔀 **Visual Workflow Editor** - Intuitive drag-and-drop canvas for designing AI agent orchestrations without code
-🤖 **OpenAI Codex CLI Support (※2 β)** - Export & Run workflows to Codex CLI (Skills format).
+🤖 **Agentic Engineering** - Design multi-agent workflows with Sub-Agent orchestration, Agent Skills, and MCP tool integration — the building blocks of agentic engineering
- **Note:**
- - Enable **Codex** option in Toolbar's **More** menu to activate
- - Requires [Codex CLI](https://github.com/openai/codex) to be installed
- - Experimental feature; some workflows may not work as expected
-
-🤖 **Roo Code Support (※3 β)** - Export & Run workflows to Roo Code (Skills format). Run launches Roo Code directly via Extension API.
+✨ **Edit with AI** - Iteratively improve workflows through conversational AI - ask for changes, add features, or refine logic with natural language feedback
- **Note:**
- - Enable **Roo Code** option in Toolbar's **More** menu to activate
- - Requires [Roo Code](https://marketplace.visualstudio.com/items?itemName=RooVeterinaryInc.roo-cline) extension to be installed
- - Experimental feature; some workflows may not work as expected
+⚡ **One-Click Export & Run** - Export workflows to ready-to-use formats and run directly from the editor
## How to Use
diff --git a/docs/ai-coding-tools-config-reference.md b/docs/ai-coding-tools-config-reference.md
index be608301..ddbcc4f6 100644
--- a/docs/ai-coding-tools-config-reference.md
+++ b/docs/ai-coding-tools-config-reference.md
@@ -8,6 +8,7 @@ Created by referencing official documentation for each tool.
| Tool | Rules | Skills | Commands/Prompts | Agents/Modes | MCP | Ignore |
|------|-------|--------|------------------|--------------|-----|--------|
| Claude Code | Project User | Project User | Project User | Project User | Project User | Project |
+| Gemini CLI | Project User | Project User | Project User | - | Project User | Project |
| Roo Code | Project Global | Project Global | Project Global | Project Global | Project Global | Project |
| VSCode Copilot Chat | Project User | Project User | Project User | Project | Project User | - |
| Copilot CLI | Project | Project Global | - | Project Global | Global | - |
@@ -130,6 +131,236 @@ disable-model-invocation: false # Optional
---
+## Gemini CLI
+
+Google Gemini CLI is a terminal-based AI coding agent.
+
+> **Reference:**
+> - [Gemini CLI Installation](https://geminicli.com/docs/get-started/installation/)
+> - [Gemini CLI Configuration](https://geminicli.com/docs/get-started/configuration/)
+> - [GEMINI.md Files](https://geminicli.com/docs/cli/gemini-md/)
+> - [Agent Skills](https://geminicli.com/docs/cli/skills/)
+> - [Custom Commands](https://geminicli.com/docs/cli/custom-commands/)
+> - [MCP Servers](https://geminicli.com/docs/tools/mcp-server/)
+> - [Extensions](https://geminicli.com/docs/extensions/)
+
+### Installation
+
+| Method | Command |
+|--------|---------|
+| **npm (global)** | `npm install -g @google/gemini-cli` |
+| **Homebrew (macOS/Linux)** | `brew install gemini-cli` |
+| **npx (no install)** | `npx @google/gemini-cli` |
+
+**Prerequisites:** Node.js 20.0.0+
+
+### Rules (GEMINI.md)
+
+| Scope | Path | Description |
+|-------|------|-------------|
+| **Project** | `./GEMINI.md` | Project instructions (discovered from CWD up to `.git` root) |
+| **Project (subdirs)** | `**/GEMINI.md` | Subdirectory instructions (recursive scan) |
+| **User** | `~/.gemini/GEMINI.md` | User instructions (all projects) |
+
+**Features:**
+- `@file.md` syntax to import other Markdown files
+- `/memory show` to display current combined context
+- `/memory refresh` to reload all context files
+- `/memory add ` to append text to `~/.gemini/GEMINI.md`
+- `GEMINI_SYSTEM_MD` environment variable to override system prompt
+
+**Custom context file names (settings.json):**
+```json
+{
+ "context": {
+ "fileName": ["AGENTS.md", "CONTEXT.md", "GEMINI.md"]
+ }
+}
+```
+
+### Skills
+
+| Scope | Path | Description |
+|-------|------|-------------|
+| **Project** | `./.gemini/skills/{skill-name}/SKILL.md` | Project skills |
+| **User** | `~/.gemini/skills/{skill-name}/SKILL.md` | User skills (all projects) |
+| **Extension** | Extension-bundled | Skills from installed extensions |
+
+**Directory structure:**
+```
+.gemini/skills/my-skill/
+├── SKILL.md # Required: metadata + instructions
+├── scripts/ # Optional: executable scripts
+├── references/ # Optional: static documentation
+└── assets/ # Optional: templates/resources
+```
+
+**Frontmatter Schema:**
+```yaml
+---
+name: skill-name # Required
+description: Skill description # Required (single-line string)
+---
+```
+
+**Lifecycle:**
+1. **Discovery**: CLI scans 3 layers; only `name` and `description` are injected into system prompt
+2. **Activation**: Model calls `activate_skill` tool when task matches (user confirmation required)
+3. **Injection**: After approval, SKILL.md body + folder structure are added to conversation
+
+**CLI commands:** `gemini skills list`, `gemini skills install`, `gemini skills link`
+
+**settings.json:**
+```json
+{
+ "skills": {
+ "enabled": true,
+ "disabled": ["skill-name"]
+ }
+}
+```
+
+### Commands (Custom Commands)
+
+| Scope | Path | Description |
+|-------|------|-------------|
+| **Project** | `./.gemini/commands/*.toml` | Project commands |
+| **User** | `~/.gemini/commands/*.toml` | User commands |
+| **Extension** | Extension-bundled | Commands from installed extensions |
+
+> **Note:** Project commands override same-named user commands. Filename becomes command name (e.g., `test.toml` → `/test`, `git/commit.toml` → `/git:commit`).
+
+**TOML Schema:**
+```toml
+description = "Generate a commit message" # Optional
+prompt = """
+Review the following staged changes:
+
+!{git diff --staged}
+
+{{args}}
+"""
+```
+
+**Template syntax:**
+- `{{args}}` — User input placeholder
+- `!{command}` — Shell command output injection
+- `@{file.md}` — File content injection
+
+### MCP
+
+MCP servers are configured in `settings.json` under `mcpServers` key.
+
+| Scope | Path | Description |
+|-------|------|-------------|
+| **Project** | `./.gemini/settings.json` | Project MCP configuration (within `mcpServers` key) |
+| **User** | `~/.gemini/settings.json` | User MCP configuration (within `mcpServers` key) |
+
+**JSON Schema:**
+```json
+{
+ "mcpServers": {
+ "server-name": {
+ "command": "npx",
+ "args": ["-y", "package-name"],
+ "env": {
+ "API_KEY": "$ENV_VAR"
+ },
+ "cwd": "/path/to/dir",
+ "timeout": 600000,
+ "trust": false,
+ "includeTools": ["tool1"],
+ "excludeTools": ["tool2"]
+ }
+ }
+}
+```
+
+**Transport types:** `httpUrl` > `url` > `command` (priority order, at least one required)
+
+**Global MCP settings:**
+```json
+{
+ "mcp": {
+ "allowed": ["serverName"],
+ "excluded": ["serverName"]
+ }
+}
+```
+
+### Configuration
+
+| Scope | Path | Description |
+|-------|------|-------------|
+| **Project** | `./.gemini/settings.json` | Project settings |
+| **User** | `~/.gemini/settings.json` | User settings |
+| **System** | See below | System settings (admin-deployed) |
+
+**System settings locations:**
+- Linux: `/etc/gemini-cli/settings.json`
+- macOS: `/Library/Application Support/GeminiCli/settings.json`
+
+**Precedence order (high → low):**
+1. CLI arguments (`--model`, `--sandbox`, etc.)
+2. Environment variables (`GEMINI_API_KEY`, `GEMINI_MODEL`, etc.)
+3. System settings
+4. Project settings (`.gemini/settings.json`)
+5. User settings (`~/.gemini/settings.json`)
+6. System defaults
+7. Hard-coded defaults
+
+> **Note:** `GEMINI_CLI_HOME` environment variable can change the `~/.gemini` path
+
+**Key environment variables:**
+
+| Variable | Description |
+|----------|-------------|
+| `GEMINI_API_KEY` | API authentication key |
+| `GEMINI_MODEL` | Default model |
+| `GEMINI_CLI_HOME` | Config root directory (overrides `~/.gemini`) |
+| `GEMINI_SANDBOX` | Sandbox mode |
+| `GEMINI_SYSTEM_MD` | System prompt override |
+
+### Ignore
+
+| Scope | Path |
+|-------|------|
+| **Project** | `./.geminiignore` |
+
+> **Note:** Uses `.gitignore` syntax. Session restart required after changes.
+
+**settings.json related:**
+```json
+{
+ "context": {
+ "fileFiltering": {
+ "respectGitIgnore": true,
+ "respectGeminiIgnore": true
+ }
+ }
+}
+```
+
+### Extensions
+
+| Scope | Path | Description |
+|-------|------|-------------|
+| **User** | `~/.gemini/extensions/{name}/` | Installed extensions |
+
+**Extension directory structure:**
+```
+.gemini/extensions/my-extension/
+├── gemini-extension.json # Required: manifest
+├── GEMINI.md # Optional: context
+├── commands/*.toml # Optional: custom commands
+├── skills/{name}/SKILL.md # Optional: skills
+└── hooks/hooks.json # Optional: hook definitions
+```
+
+**CLI commands:** `gemini extensions install `, `gemini extensions list`, `gemini extensions enable/disable`
+
+---
+
## VSCode Copilot Chat
GitHub Copilot Chat functionality within VSCode.
@@ -633,10 +864,12 @@ customModes:
```
Project Root/
├── CLAUDE.md # Claude Code (root rule)
+├── GEMINI.md # Gemini CLI (project instructions)
├── AGENTS.md # Codex CLI, Copilot CLI, VSCode Copilot Chat, Roo Code (root rule)
├── AGENTS.override.md # Codex CLI (override)
├── .mcp.json # Claude Code (MCP)
├── .claudeignore # Claude Code (ignore)
+├── .geminiignore # Gemini CLI (ignore)
├── .rooignore # Roo Code (ignore)
├── .roomodes # Roo Code (project custom modes, YAML/JSON)
├── .roorules # Roo Code (fallback rules)
@@ -653,6 +886,13 @@ Project Root/
├── .codex/
│ └── skills/{name}/SKILL.md # Codex CLI (skills)
│
+├── .gemini/
+│ ├── settings.json # Gemini CLI (project settings + MCP)
+│ ├── commands/*.toml # Gemini CLI (project custom commands)
+│ └── skills/{name}/SKILL.md # Gemini CLI (project skills)
+│
+├── .geminiignore # Gemini CLI (ignore)
+│
├── .github/
│ ├── copilot-instructions.md # VSCode Copilot Chat, Copilot CLI (root rule)
│ ├── instructions/*.instructions.md # VSCode Copilot Chat, Copilot CLI (modular rules)
@@ -692,6 +932,13 @@ User Home (~)/
│ ├── config.toml # Codex CLI (config + MCP)
│ └── skills/{name}/SKILL.md # Codex CLI (user skills)
│
+├── .gemini/
+│ ├── GEMINI.md # Gemini CLI (user instructions)
+│ ├── settings.json # Gemini CLI (user settings + MCP)
+│ ├── commands/*.toml # Gemini CLI (user custom commands)
+│ ├── skills/{name}/SKILL.md # Gemini CLI (user skills)
+│ └── extensions/{name}/ # Gemini CLI (installed extensions)
+│
├── .copilot/
│ ├── config.json # Copilot CLI (main config)
│ ├── mcp-config.json # Copilot CLI (global MCP)
@@ -742,6 +989,15 @@ User Home (~)/
- [Claude Code Documentation](https://code.claude.com/docs/en)
- [Claude Code Settings](https://code.claude.com/docs/en/settings)
- [Claude Code Skills](https://code.claude.com/docs/en/skills)
+- [Gemini CLI GitHub Repository](https://github.com/google-gemini/gemini-cli)
+- [Gemini CLI Installation](https://geminicli.com/docs/get-started/installation/)
+- [Gemini CLI Configuration](https://geminicli.com/docs/get-started/configuration/)
+- [Gemini CLI GEMINI.md](https://geminicli.com/docs/cli/gemini-md/)
+- [Gemini CLI Skills](https://geminicli.com/docs/cli/skills/)
+- [Gemini CLI Custom Commands](https://geminicli.com/docs/cli/custom-commands/)
+- [Gemini CLI MCP Servers](https://geminicli.com/docs/tools/mcp-server/)
+- [Gemini CLI .geminiignore](https://geminicli.com/docs/cli/gemini-ignore/)
+- [Gemini CLI Extensions](https://geminicli.com/docs/extensions/)
- [Roo Code Documentation](https://docs.roocode.com/)
- [Roo Code Custom Instructions](https://docs.roocode.com/features/custom-instructions)
- [Roo Code Skills](https://docs.roocode.com/features/skills)
diff --git a/package-lock.json b/package-lock.json
index 57a6268e..5ec1bc66 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "cc-wf-studio",
- "version": "3.21.0",
+ "version": "3.22.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "cc-wf-studio",
- "version": "3.21.0",
+ "version": "3.22.0",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.26.0",
@@ -2135,9 +2135,9 @@
}
},
"node_modules/ajv": {
- "version": "8.17.1",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
- "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
+ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
@@ -2261,13 +2261,13 @@
"license": "MIT"
},
"node_modules/axios": {
- "version": "1.13.2",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
- "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
+ "version": "1.13.5",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
+ "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
"license": "MIT",
"dependencies": {
- "follow-redirects": "^1.15.6",
- "form-data": "^4.0.4",
+ "follow-redirects": "^1.15.11",
+ "form-data": "^4.0.5",
"proxy-from-env": "^1.1.0"
}
},
@@ -8266,9 +8266,9 @@
"license": "MIT"
},
"node_modules/qs": {
- "version": "6.14.1",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
- "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
+ "version": "6.15.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz",
+ "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.1.0"
diff --git a/package.json b/package.json
index 4c77104b..8544e435 100644
--- a/package.json
+++ b/package.json
@@ -1,8 +1,8 @@
{
"name": "cc-wf-studio",
- "displayName": "CC Workflow Studio - for Claude Code, Copilot, Codex, Roo Code",
- "description": "Accelerate Claude Code, GitHub Copilot, Codex CLI, Roo Code automation with a visual workflow editor",
- "version": "3.21.0",
+ "displayName": "CC Workflow Studio - Visual Agentic Engineering Toolkit for AI Coding Agents",
+ "description": "Design, orchestrate, and run AI agent workflows for Claude Code, GitHub Copilot, Codex CLI, Roo Code, and Gemini CLI",
+ "version": "3.22.0",
"publisher": "breaking-brake",
"icon": "resources/icon.png",
"repository": {
@@ -13,12 +13,15 @@
"license": "AGPL-3.0-or-later",
"keywords": [
"ai",
+ "agentic-engineering",
+ "agent-orchestration",
"claude",
"claude-code",
"cli",
"codex",
"copilot",
- "roo-code"
+ "roo-code",
+ "gemini-cli"
],
"engines": {
"vscode": "^1.80.0"
diff --git a/resources/workflow-schema-basic.json b/resources/workflow-schema-basic.json
index 60f37b9f..b2049a0a 100644
--- a/resources/workflow-schema-basic.json
+++ b/resources/workflow-schema-basic.json
@@ -194,7 +194,7 @@
"outputPorts": "2-10"
},
"skill": {
- "description": "Reference a Claude Code Skill (specialized agent capability defined in SKILL.md files). Use when user description matches a Skill's documented purpose. **Important: Skills always have exactly 1 output port. For conditional branching based on Skill results, add an ifElse or switch node after the Skill node.**",
+ "description": "Reference a Claude Code Skill (specialized agent capability defined in SKILL.md files). ONLY use Skills that are available on the system - do NOT fabricate Skills. If no matching Skill exists, use a prompt or subAgent node instead. **Important: Skills always have exactly 1 output port. For conditional branching based on Skill results, add an ifElse or switch node after the Skill node.**",
"fields": {
"name": {
"type": "string",
@@ -467,20 +467,272 @@
"outputPorts": 1
}
},
- "connectionRules": {
- "forbidden": [
- "Start node cannot have input connections",
- "End node cannot have output connections",
- "No cycles allowed",
- "No self-connections",
- "One connection per output port"
- ],
- "required": [
- "Exactly one Start node per workflow",
- "At least one End node per workflow",
- "All non-Start nodes must have input connection",
- "All non-End nodes must have output connection"
- ]
+ "connections": {
+ "description": "Comprehensive specification for workflow connections. Defines connection object structure, port naming conventions, and completeness rules for AI-generated workflows.",
+ "overview": {
+ "description": "High-level connection constraints that apply to all workflows",
+ "forbidden": [
+ "Start node cannot have input connections",
+ "End node cannot have output connections",
+ "No cycles allowed",
+ "No self-connections"
+ ],
+ "required": [
+ "At least one connection per output port",
+ "Exactly one Start node per workflow",
+ "At least one End node per workflow",
+ "All non-Start nodes must have input connection",
+ "All non-End nodes must have output connection"
+ ]
+ },
+ "format": {
+ "description": "Connection object structure in connections array",
+ "objectStructure": {
+ "id": {
+ "type": "string",
+ "required": true,
+ "example": "c1",
+ "description": "Unique connection identifier. Must be unique across all connections in the workflow."
+ },
+ "from": {
+ "type": "string",
+ "required": true,
+ "example": "start-1",
+ "description": "Source node ID. Must reference an existing node in the nodes array."
+ },
+ "to": {
+ "type": "string",
+ "required": true,
+ "example": "prompt-1",
+ "description": "Target node ID. Must reference an existing node in the nodes array."
+ },
+ "fromPort": {
+ "type": "string",
+ "required": true,
+ "description": "Source port identifier. See portNamingRules for complete mapping by node type. Examples: 'output', 'branch-0', 'branch-1'"
+ },
+ "toPort": {
+ "type": "string",
+ "required": true,
+ "enum": [
+ "input"
+ ],
+ "description": "Target port is always 'input'. Every node has exactly one input port."
+ },
+ "condition": {
+ "type": "string",
+ "required": false,
+ "example": "User selected Option A",
+ "description": "Optional condition description for documenting branch semantics."
+ }
+ }
+ },
+ "portNamingRules": {
+ "description": "Port identifier conventions for each node type. Linear nodes: single output. Conditional nodes: multiple outputs (one per branch).",
+ "linearNodes": {
+ "types": [
+ "start",
+ "end",
+ "prompt",
+ "skill",
+ "mcp",
+ "codex"
+ ],
+ "inputPortId": "input",
+ "outputPortId": "output",
+ "connectionExample": {
+ "id": "c1",
+ "from": "start-1",
+ "to": "prompt-1",
+ "fromPort": "output",
+ "toPort": "input"
+ }
+ },
+ "ifElseNode": {
+ "type": "ifElse",
+ "inputPortId": "input",
+ "outputPortIds": [
+ "branch-0",
+ "branch-1"
+ ],
+ "semantics": "branch-0 = true/if condition, branch-1 = false/else condition",
+ "constraint": "BOTH branch-0 and branch-1 MUST each have exactly one outgoing connection",
+ "connectionExample": {
+ "id": "c2",
+ "from": "ifelse-1",
+ "to": "prompt-true",
+ "fromPort": "branch-0",
+ "toPort": "input"
+ }
+ },
+ "switchNode": {
+ "type": "switch",
+ "inputPortId": "input",
+ "outputPortIds": "branch-0, branch-1, ..., branch-N (where N = branchesLength - 1)",
+ "constraint": "ALL branches (branch-0 through branch-N) MUST each have exactly one outgoing connection",
+ "note": "Last branch is typically the 'default' case",
+ "connectionExample": {
+ "id": "c3",
+ "from": "switch-1",
+ "to": "handler-case-1",
+ "fromPort": "branch-1",
+ "toPort": "input"
+ }
+ },
+ "askUserQuestionNode": {
+ "type": "askUserQuestion",
+ "inputPortId": "input",
+ "outputPortIds": "DYNAMIC - depends on configuration",
+ "modes": {
+ "mode_aiSuggestions": {
+ "condition": "useAiSuggestions: true",
+ "outputPortId": "output",
+ "description": "AI generates options at runtime. Single output port 'output' used."
+ },
+ "mode_multiSelect": {
+ "condition": "multiSelect: true",
+ "outputPortId": "output",
+ "description": "Multi-select enabled. Single output port 'output' used."
+ },
+ "mode_singleSelect": {
+ "condition": "useAiSuggestions: false AND multiSelect: false",
+ "outputPortIds": "branch-0, branch-1, ..., branch-N (where N = optionsLength - 1)",
+ "constraint": "ALL branches (branch-0 through branch-N) MUST each have exactly one outgoing connection",
+ "description": "Single-select with user-defined options. One port per option."
+ }
+ },
+ "connectionExample": {
+ "id": "c4",
+ "from": "ask-1",
+ "to": "handler-option-a",
+ "fromPort": "branch-0",
+ "toPort": "input"
+ }
+ }
+ },
+ "completenessRules": {
+ "description": "Completeness specification for workflow connections. Distinguishes between AI generation guidelines (what AI should produce) and export validation rules (what must be true at export/execution time). Manual canvas editing allows incomplete states - validation only occurs at export.",
+ "aiGenerationRules": {
+ "description": "Guidelines for AI-generated and AI-refined workflows. The AI system should ensure these rules are satisfied when generating/refining workflows. These guarantee that generated workflows are complete and ready to export.",
+ "rules": [
+ {
+ "id": "START_OUTPUT",
+ "rule": "Every Start node's 'output' port MUST have at least one outgoing connection",
+ "aiGuidance": "When generating/adding a Start node, immediately create a connection from its 'output' port to the next node"
+ },
+ {
+ "id": "NON_END_OUTPUT",
+ "rule": "Every non-End node's output port(s) MUST have at least one outgoing connection per port",
+ "example": "IfElse node with branch-0 and branch-1 requires both branches to be connected"
+ },
+ {
+ "id": "NON_START_INPUT",
+ "rule": "Every non-Start node's 'input' port MUST have exactly one incoming connection",
+ "aiGuidance": "When creating or moving a node, ensure it receives an input connection from a predecessor"
+ },
+ {
+ "id": "END_INPUT",
+ "rule": "Every End node's 'input' port can have one OR MORE incoming connections (merge point allowed)",
+ "example": "Multiple upstream paths can converge to a single End node"
+ },
+ {
+ "id": "IFELSE_BOTH_BRANCHES",
+ "rule": "IfElse node: BOTH branch-0 and branch-1 MUST have outgoing connections",
+ "aiGuidance": "Create both connections before moving to next node"
+ },
+ {
+ "id": "SWITCH_ALL_BRANCHES",
+ "rule": "Switch node: ALL branch ports (branch-0 through branch-N) MUST have outgoing connections",
+ "aiGuidance": "Count branches, create N connections (one per branch)"
+ },
+ {
+ "id": "ASKUSERQUESTION_ALL_OPTIONS",
+ "rule": "AskUserQuestion (single-select): ALL branch ports (branch-0 through branch-N) MUST have outgoing connections",
+ "aiGuidance": "For single-select with N options, create N connections"
+ },
+ {
+ "id": "NODE_ADDITION",
+ "rule": "When adding a new node, immediately add an input connection to it (except Start)",
+ "aiGuidance": "Never leave nodes disconnected. Node creation must be paired with connection creation."
+ },
+ {
+ "id": "NODE_REMOVAL",
+ "rule": "When removing a node, remove ALL connections associated with that node",
+ "aiGuidance": "Before deleting a node, remove all edges where node is source or target"
+ },
+ {
+ "id": "NODE_ID_VALIDITY",
+ "rule": "Node IDs referenced in connections must exist in nodes array",
+ "validation": "For each connection: verify 'from' node exists, verify 'to' node exists"
+ }
+ ]
+ },
+ "exportValidationRules": {
+ "description": "Mandatory validation rules checked when exporting or executing a workflow. These rules apply to both manually-edited and AI-generated workflows. Workflows must pass these validations before execution.",
+ "rules": [
+ {
+ "id": "EXACTLY_ONE_START",
+ "rule": "Workflow MUST have exactly one Start node",
+ "failureMessage": "Workflow must have exactly one Start node"
+ },
+ {
+ "id": "AT_LEAST_ONE_END",
+ "rule": "Workflow MUST have at least one End node",
+ "failureMessage": "Workflow must have at least one End node"
+ },
+ {
+ "id": "START_OUTPUT_CONNECTED",
+ "rule": "Start node's 'output' port MUST be connected",
+ "failureMessage": "Start node output must be connected to downstream node"
+ },
+ {
+ "id": "END_INPUT_CONNECTED",
+ "rule": "At least one End node's 'input' port MUST be connected",
+ "failureMessage": "At least one End node must have incoming connection"
+ },
+ {
+ "id": "ALL_NODES_REACHABLE",
+ "rule": "All nodes MUST be reachable from Start node",
+ "failureMessage": "Unreachable nodes found - ensure all nodes form a connected path from Start"
+ },
+ {
+ "id": "ALL_PATHS_LEAD_TO_END",
+ "rule": "All execution paths MUST lead to at least one End node",
+ "failureMessage": "Dead-end paths found - ensure all branches converge to an End node"
+ },
+ {
+ "id": "NO_CYCLES",
+ "rule": "Workflow MUST NOT contain cycles",
+ "failureMessage": "Circular reference detected - workflows must be acyclic (DAG)"
+ },
+ {
+ "id": "NODE_ID_REFERENCES_VALID",
+ "rule": "All node IDs in connections must reference existing nodes",
+ "failureMessage": "Connection references non-existent node"
+ },
+ {
+ "id": "CONDITIONAL_BRANCHES_CONNECTED",
+ "rule": "All conditional node branches (IfElse, Switch, AskUserQuestion single-select) MUST have outgoing connections",
+ "failureMessage": "Conditional node has disconnected branch"
+ }
+ ]
+ },
+ "postGenerationChecklist": {
+ "description": "Quick checklist for verifying AI-generated workflows before returning to user",
+ "items": [
+ "✓ Exactly 1 Start node with output connected",
+ "✓ At least 1 End node with input(s) connected",
+ "✓ All non-Start nodes have input connected",
+ "✓ All non-End nodes have output(s) connected",
+ "✓ IfElse nodes: both branches connected",
+ "✓ Switch nodes: all branches connected",
+ "✓ AskUserQuestion nodes (single-select): all options connected",
+ "✓ No dangling nodes",
+ "✓ No circular references",
+ "✓ All node IDs in connections exist in nodes array"
+ ]
+ }
+ }
},
"validationRules": {
"workflow": {
@@ -520,11 +772,23 @@
},
"nodes": {
"type": "array",
- "required": true
+ "required": true,
+ "nodePositionGuidelines": {
+ "description": "Each node must have a position: { x, y } in pixels. Follow these spacing rules for readable layouts.",
+ "horizontalSpacing": 300,
+ "verticalSpacing": 350,
+ "rules": [
+ "Place nodes left-to-right in execution order. Increment x by 300 for each step.",
+ "For linear flows, keep all nodes at the same y value.",
+ "For conditional branches (ifElse, switch, askUserQuestion), fan out vertically: center the parent, offset children by ±175 (2 branches) or ±350/0 (3 branches).",
+ "After branches merge back, return to the parent's y value."
+ ]
+ }
},
"connections": {
"type": "array",
- "required": true
+ "required": true,
+ "description": "Array of connections between workflow nodes. Each connection defines a directed edge from one node's output port to another node's input port. See 'connections' section for detailed specifications: format, portNamingRules, and completenessRules."
},
"createdAt": {
"type": "string",
diff --git a/resources/workflow-schema-basic.toon b/resources/workflow-schema-basic.toon
index b7bbf446..249a1261 100644
--- a/resources/workflow-schema-basic.toon
+++ b/resources/workflow-schema-basic.toon
@@ -141,7 +141,7 @@ nodeTypes:
inputPorts: 1
outputPorts: 2-10
skill:
- description: "Reference a Claude Code Skill (specialized agent capability defined in SKILL.md files). Use when user description matches a Skill's documented purpose. **Important: Skills always have exactly 1 output port. For conditional branching based on Skill results, add an ifElse or switch node after the Skill node.**"
+ description: "Reference a Claude Code Skill (specialized agent capability defined in SKILL.md files). ONLY use Skills that are available on the system - do NOT fabricate Skills. If no matching Skill exists, use a prompt or subAgent node instead. **Important: Skills always have exactly 1 output port. For conditional branching based on Skill results, add an ifElse or switch node after the Skill node.**"
fields:
name:
type: string
@@ -317,9 +317,154 @@ nodeTypes:
description: "Skip Git repository trust check. When true, allows execution outside trusted Git repositories. Use with caution."
inputPorts: 1
outputPorts: 1
-connectionRules:
- forbidden[5]: Start node cannot have input connections,End node cannot have output connections,No cycles allowed,No self-connections,One connection per output port
- required[4]: Exactly one Start node per workflow,At least one End node per workflow,All non-Start nodes must have input connection,All non-End nodes must have output connection
+connections:
+ description: "Comprehensive specification for workflow connections. Defines connection object structure, port naming conventions, and completeness rules for AI-generated workflows."
+ overview:
+ description: High-level connection constraints that apply to all workflows
+ forbidden[4]: Start node cannot have input connections,End node cannot have output connections,No cycles allowed,No self-connections
+ required[5]: At least one connection per output port,Exactly one Start node per workflow,At least one End node per workflow,All non-Start nodes must have input connection,All non-End nodes must have output connection
+ format:
+ description: Connection object structure in connections array
+ objectStructure:
+ id:
+ type: string
+ required: true
+ example: c1
+ description: Unique connection identifier. Must be unique across all connections in the workflow.
+ from:
+ type: string
+ required: true
+ example: start-1
+ description: Source node ID. Must reference an existing node in the nodes array.
+ to:
+ type: string
+ required: true
+ example: prompt-1
+ description: Target node ID. Must reference an existing node in the nodes array.
+ fromPort:
+ type: string
+ required: true
+ description: "Source port identifier. See portNamingRules for complete mapping by node type. Examples: 'output', 'branch-0', 'branch-1'"
+ toPort:
+ type: string
+ required: true
+ enum[1]: input
+ description: Target port is always 'input'. Every node has exactly one input port.
+ condition:
+ type: string
+ required: false
+ example: User selected Option A
+ description: Optional condition description for documenting branch semantics.
+ portNamingRules:
+ description: "Port identifier conventions for each node type. Linear nodes: single output. Conditional nodes: multiple outputs (one per branch)."
+ linearNodes:
+ types[6]: start,end,prompt,skill,mcp,codex
+ inputPortId: input
+ outputPortId: output
+ connectionExample:
+ id: c1
+ from: start-1
+ to: prompt-1
+ fromPort: output
+ toPort: input
+ ifElseNode:
+ type: ifElse
+ inputPortId: input
+ outputPortIds[2]: branch-0,branch-1
+ semantics: "branch-0 = true/if condition, branch-1 = false/else condition"
+ constraint: BOTH branch-0 and branch-1 MUST each have exactly one outgoing connection
+ connectionExample:
+ id: c2
+ from: ifelse-1
+ to: prompt-true
+ fromPort: branch-0
+ toPort: input
+ switchNode:
+ type: switch
+ inputPortId: input
+ outputPortIds: "branch-0, branch-1, ..., branch-N (where N = branchesLength - 1)"
+ constraint: ALL branches (branch-0 through branch-N) MUST each have exactly one outgoing connection
+ note: Last branch is typically the 'default' case
+ connectionExample:
+ id: c3
+ from: switch-1
+ to: handler-case-1
+ fromPort: branch-1
+ toPort: input
+ askUserQuestionNode:
+ type: askUserQuestion
+ inputPortId: input
+ outputPortIds: DYNAMIC - depends on configuration
+ modes:
+ mode_aiSuggestions:
+ condition: "useAiSuggestions: true"
+ outputPortId: output
+ description: AI generates options at runtime. Single output port 'output' used.
+ mode_multiSelect:
+ condition: "multiSelect: true"
+ outputPortId: output
+ description: Multi-select enabled. Single output port 'output' used.
+ mode_singleSelect:
+ condition: "useAiSuggestions: false AND multiSelect: false"
+ outputPortIds: "branch-0, branch-1, ..., branch-N (where N = optionsLength - 1)"
+ constraint: ALL branches (branch-0 through branch-N) MUST each have exactly one outgoing connection
+ description: Single-select with user-defined options. One port per option.
+ connectionExample:
+ id: c4
+ from: ask-1
+ to: handler-option-a
+ fromPort: branch-0
+ toPort: input
+ completenessRules:
+ description: Completeness specification for workflow connections. Distinguishes between AI generation guidelines (what AI should produce) and export validation rules (what must be true at export/execution time). Manual canvas editing allows incomplete states - validation only occurs at export.
+ aiGenerationRules:
+ description: Guidelines for AI-generated and AI-refined workflows. The AI system should ensure these rules are satisfied when generating/refining workflows. These guarantee that generated workflows are complete and ready to export.
+ rules[10]:
+ - id: START_OUTPUT
+ rule: Every Start node's 'output' port MUST have at least one outgoing connection
+ aiGuidance: "When generating/adding a Start node, immediately create a connection from its 'output' port to the next node"
+ - id: NON_END_OUTPUT
+ rule: Every non-End node's output port(s) MUST have at least one outgoing connection per port
+ example: IfElse node with branch-0 and branch-1 requires both branches to be connected
+ - id: NON_START_INPUT
+ rule: Every non-Start node's 'input' port MUST have exactly one incoming connection
+ aiGuidance: "When creating or moving a node, ensure it receives an input connection from a predecessor"
+ - id: END_INPUT
+ rule: Every End node's 'input' port can have one OR MORE incoming connections (merge point allowed)
+ example: Multiple upstream paths can converge to a single End node
+ - id: IFELSE_BOTH_BRANCHES
+ rule: "IfElse node: BOTH branch-0 and branch-1 MUST have outgoing connections"
+ aiGuidance: Create both connections before moving to next node
+ - id: SWITCH_ALL_BRANCHES
+ rule: "Switch node: ALL branch ports (branch-0 through branch-N) MUST have outgoing connections"
+ aiGuidance: "Count branches, create N connections (one per branch)"
+ - id: ASKUSERQUESTION_ALL_OPTIONS
+ rule: "AskUserQuestion (single-select): ALL branch ports (branch-0 through branch-N) MUST have outgoing connections"
+ aiGuidance: "For single-select with N options, create N connections"
+ - id: NODE_ADDITION
+ rule: "When adding a new node, immediately add an input connection to it (except Start)"
+ aiGuidance: Never leave nodes disconnected. Node creation must be paired with connection creation.
+ - id: NODE_REMOVAL
+ rule: "When removing a node, remove ALL connections associated with that node"
+ aiGuidance: "Before deleting a node, remove all edges where node is source or target"
+ - id: NODE_ID_VALIDITY
+ rule: Node IDs referenced in connections must exist in nodes array
+ validation: "For each connection: verify 'from' node exists, verify 'to' node exists"
+ exportValidationRules:
+ description: Mandatory validation rules checked when exporting or executing a workflow. These rules apply to both manually-edited and AI-generated workflows. Workflows must pass these validations before execution.
+ rules[9]{id,rule,failureMessage}:
+ EXACTLY_ONE_START,Workflow MUST have exactly one Start node,Workflow must have exactly one Start node
+ AT_LEAST_ONE_END,Workflow MUST have at least one End node,Workflow must have at least one End node
+ START_OUTPUT_CONNECTED,Start node's 'output' port MUST be connected,Start node output must be connected to downstream node
+ END_INPUT_CONNECTED,At least one End node's 'input' port MUST be connected,At least one End node must have incoming connection
+ ALL_NODES_REACHABLE,All nodes MUST be reachable from Start node,Unreachable nodes found - ensure all nodes form a connected path from Start
+ ALL_PATHS_LEAD_TO_END,All execution paths MUST lead to at least one End node,Dead-end paths found - ensure all branches converge to an End node
+ NO_CYCLES,Workflow MUST NOT contain cycles,Circular reference detected - workflows must be acyclic (DAG)
+ NODE_ID_REFERENCES_VALID,All node IDs in connections must reference existing nodes,Connection references non-existent node
+ CONDITIONAL_BRANCHES_CONNECTED,"All conditional node branches (IfElse, Switch, AskUserQuestion single-select) MUST have outgoing connections",Conditional node has disconnected branch
+ postGenerationChecklist:
+ description: Quick checklist for verifying AI-generated workflows before returning to user
+ items[10]: ✓ Exactly 1 Start node with output connected,✓ At least 1 End node with input(s) connected,✓ All non-Start nodes have input connected,✓ All non-End nodes have output(s) connected,"✓ IfElse nodes: both branches connected","✓ Switch nodes: all branches connected","✓ AskUserQuestion nodes (single-select): all options connected",✓ No dangling nodes,✓ No circular references,✓ All node IDs in connections exist in nodes array
validationRules:
workflow:
maxNodes: 50
@@ -352,9 +497,15 @@ workflowStructure:
nodes:
type: array
required: true
+ nodePositionGuidelines:
+ description: "Each node must have a position: { x, y } in pixels. Follow these spacing rules for readable layouts."
+ horizontalSpacing: 300
+ verticalSpacing: 350
+ rules[4]: Place nodes left-to-right in execution order. Increment x by 300 for each step.,"For linear flows, keep all nodes at the same y value.","For conditional branches (ifElse, switch, askUserQuestion), fan out vertically: center the parent, offset children by ±175 (2 branches) or ±350/0 (3 branches).","After branches merge back, return to the parent's y value."
connections:
type: array
required: true
+ description: "Array of connections between workflow nodes. Each connection defines a directed edge from one node's output port to another node's input port. See 'connections' section for detailed specifications: format, portNamingRules, and completenessRules."
createdAt:
type: string
required: true
diff --git a/resources/workflow-schema.json b/resources/workflow-schema.json
index 61dc8a45..c28c04f4 100644
--- a/resources/workflow-schema.json
+++ b/resources/workflow-schema.json
@@ -142,7 +142,7 @@
"outputPorts": "2-10"
},
"skill": {
- "description": "Reference a Claude Code Skill (specialized agent capability defined in SKILL.md files). Use when user description matches a Skill's documented purpose. **Important: Skills always have exactly 1 output port. For conditional branching based on Skill results, add an ifElse or switch node after the Skill node.**",
+ "description": "Reference a Claude Code Skill (specialized agent capability defined in SKILL.md files). ONLY use Skills that are available on the system - do NOT fabricate Skills. If no matching Skill exists, use a prompt or subAgent node instead. **Important: Skills always have exactly 1 output port. For conditional branching based on Skill results, add an ifElse or switch node after the Skill node.**",
"fields": {
"name": {
"type": "string",
@@ -378,14 +378,14 @@
"id": "sf-start",
"type": "start",
"name": "Start",
- "position": { "x": 100, "y": 200 },
+ "position": { "x": 100, "y": 300 },
"data": { "label": "Start" }
},
{
"id": "sf-end",
"type": "end",
"name": "End",
- "position": { "x": 400, "y": 200 },
+ "position": { "x": 400, "y": 300 },
"data": { "label": "End" }
}
],
@@ -461,20 +461,260 @@
"outputPorts": 1
}
},
- "connectionRules": {
- "forbidden": [
- "Start node cannot have input connections",
- "End node cannot have output connections",
- "No cycles allowed",
- "No self-connections",
- "One connection per output port"
- ],
- "required": [
- "Exactly one Start node per workflow",
- "At least one End node per workflow",
- "All non-Start nodes must have input connection",
- "All non-End nodes must have output connection"
- ]
+ "connections": {
+ "description": "Comprehensive specification for workflow connections. Defines connection object structure, port naming conventions, and completeness rules for AI-generated workflows.",
+ "overview": {
+ "description": "High-level connection constraints that apply to all workflows",
+ "forbidden": [
+ "Start node cannot have input connections",
+ "End node cannot have output connections",
+ "No cycles allowed",
+ "No self-connections"
+ ],
+ "required": [
+ "At least one connection per output port",
+ "Exactly one Start node per workflow",
+ "At least one End node per workflow",
+ "All non-Start nodes must have input connection",
+ "All non-End nodes must have output connection"
+ ]
+ },
+ "format": {
+ "description": "Connection object structure in connections array",
+ "objectStructure": {
+ "id": {
+ "type": "string",
+ "required": true,
+ "example": "c1",
+ "description": "Unique connection identifier. Must be unique across all connections in the workflow."
+ },
+ "from": {
+ "type": "string",
+ "required": true,
+ "example": "start-1",
+ "description": "Source node ID. Must reference an existing node in the nodes array."
+ },
+ "to": {
+ "type": "string",
+ "required": true,
+ "example": "prompt-1",
+ "description": "Target node ID. Must reference an existing node in the nodes array."
+ },
+ "fromPort": {
+ "type": "string",
+ "required": true,
+ "description": "Source port identifier. See portNamingRules for complete mapping by node type. Examples: 'output', 'branch-0', 'branch-1'"
+ },
+ "toPort": {
+ "type": "string",
+ "required": true,
+ "enum": ["input"],
+ "description": "Target port is always 'input'. Every node has exactly one input port."
+ },
+ "condition": {
+ "type": "string",
+ "required": false,
+ "example": "User selected Option A",
+ "description": "Optional condition description for documenting branch semantics."
+ }
+ }
+ },
+ "portNamingRules": {
+ "description": "Port identifier conventions for each node type. Linear nodes: single output. Conditional nodes: multiple outputs (one per branch).",
+ "linearNodes": {
+ "types": ["start", "end", "prompt", "subAgent", "skill", "mcp", "subAgentFlow", "codex"],
+ "inputPortId": "input",
+ "outputPortId": "output",
+ "connectionExample": {
+ "id": "c1",
+ "from": "start-1",
+ "to": "prompt-1",
+ "fromPort": "output",
+ "toPort": "input"
+ }
+ },
+ "ifElseNode": {
+ "type": "ifElse",
+ "inputPortId": "input",
+ "outputPortIds": ["branch-0", "branch-1"],
+ "semantics": "branch-0 = true/if condition, branch-1 = false/else condition",
+ "constraint": "BOTH branch-0 and branch-1 MUST each have exactly one outgoing connection",
+ "connectionExample": {
+ "id": "c2",
+ "from": "ifelse-1",
+ "to": "prompt-true",
+ "fromPort": "branch-0",
+ "toPort": "input"
+ }
+ },
+ "switchNode": {
+ "type": "switch",
+ "inputPortId": "input",
+ "outputPortIds": "branch-0, branch-1, ..., branch-N (where N = branchesLength - 1)",
+ "constraint": "ALL branches (branch-0 through branch-N) MUST each have exactly one outgoing connection",
+ "note": "Last branch is typically the 'default' case",
+ "connectionExample": {
+ "id": "c3",
+ "from": "switch-1",
+ "to": "handler-case-1",
+ "fromPort": "branch-1",
+ "toPort": "input"
+ }
+ },
+ "askUserQuestionNode": {
+ "type": "askUserQuestion",
+ "inputPortId": "input",
+ "outputPortIds": "DYNAMIC - depends on configuration",
+ "modes": {
+ "mode_aiSuggestions": {
+ "condition": "useAiSuggestions: true",
+ "outputPortId": "output",
+ "description": "AI generates options at runtime. Single output port 'output' used."
+ },
+ "mode_multiSelect": {
+ "condition": "multiSelect: true",
+ "outputPortId": "output",
+ "description": "Multi-select enabled. Single output port 'output' used."
+ },
+ "mode_singleSelect": {
+ "condition": "useAiSuggestions: false AND multiSelect: false",
+ "outputPortIds": "branch-0, branch-1, ..., branch-N (where N = optionsLength - 1)",
+ "constraint": "ALL branches (branch-0 through branch-N) MUST each have exactly one outgoing connection",
+ "description": "Single-select with user-defined options. One port per option."
+ }
+ },
+ "connectionExample": {
+ "id": "c4",
+ "from": "ask-1",
+ "to": "handler-option-a",
+ "fromPort": "branch-0",
+ "toPort": "input"
+ }
+ }
+ },
+ "completenessRules": {
+ "description": "Completeness specification for workflow connections. Distinguishes between AI generation guidelines (what AI should produce) and export validation rules (what must be true at export/execution time). Manual canvas editing allows incomplete states - validation only occurs at export.",
+ "aiGenerationRules": {
+ "description": "Guidelines for AI-generated and AI-refined workflows. The AI system should ensure these rules are satisfied when generating/refining workflows. These guarantee that generated workflows are complete and ready to export.",
+ "rules": [
+ {
+ "id": "START_OUTPUT",
+ "rule": "Every Start node's 'output' port MUST have at least one outgoing connection",
+ "aiGuidance": "When generating/adding a Start node, immediately create a connection from its 'output' port to the next node"
+ },
+ {
+ "id": "NON_END_OUTPUT",
+ "rule": "Every non-End node's output port(s) MUST have at least one outgoing connection per port",
+ "example": "IfElse node with branch-0 and branch-1 requires both branches to be connected"
+ },
+ {
+ "id": "NON_START_INPUT",
+ "rule": "Every non-Start node's 'input' port MUST have exactly one incoming connection",
+ "aiGuidance": "When creating or moving a node, ensure it receives an input connection from a predecessor"
+ },
+ {
+ "id": "END_INPUT",
+ "rule": "Every End node's 'input' port can have one OR MORE incoming connections (merge point allowed)",
+ "example": "Multiple upstream paths can converge to a single End node"
+ },
+ {
+ "id": "IFELSE_BOTH_BRANCHES",
+ "rule": "IfElse node: BOTH branch-0 and branch-1 MUST have outgoing connections",
+ "aiGuidance": "Create both connections before moving to next node"
+ },
+ {
+ "id": "SWITCH_ALL_BRANCHES",
+ "rule": "Switch node: ALL branch ports (branch-0 through branch-N) MUST have outgoing connections",
+ "aiGuidance": "Count branches, create N connections (one per branch)"
+ },
+ {
+ "id": "ASKUSERQUESTION_ALL_OPTIONS",
+ "rule": "AskUserQuestion (single-select): ALL branch ports (branch-0 through branch-N) MUST have outgoing connections",
+ "aiGuidance": "For single-select with N options, create N connections"
+ },
+ {
+ "id": "NODE_ADDITION",
+ "rule": "When adding a new node, immediately add an input connection to it (except Start)",
+ "aiGuidance": "Never leave nodes disconnected. Node creation must be paired with connection creation."
+ },
+ {
+ "id": "NODE_REMOVAL",
+ "rule": "When removing a node, remove ALL connections associated with that node",
+ "aiGuidance": "Before deleting a node, remove all edges where node is source or target"
+ },
+ {
+ "id": "NODE_ID_VALIDITY",
+ "rule": "Node IDs referenced in connections must exist in nodes array",
+ "validation": "For each connection: verify 'from' node exists, verify 'to' node exists"
+ }
+ ]
+ },
+ "exportValidationRules": {
+ "description": "Mandatory validation rules checked when exporting or executing a workflow. These rules apply to both manually-edited and AI-generated workflows. Workflows must pass these validations before execution.",
+ "rules": [
+ {
+ "id": "EXACTLY_ONE_START",
+ "rule": "Workflow MUST have exactly one Start node",
+ "failureMessage": "Workflow must have exactly one Start node"
+ },
+ {
+ "id": "AT_LEAST_ONE_END",
+ "rule": "Workflow MUST have at least one End node",
+ "failureMessage": "Workflow must have at least one End node"
+ },
+ {
+ "id": "START_OUTPUT_CONNECTED",
+ "rule": "Start node's 'output' port MUST be connected",
+ "failureMessage": "Start node output must be connected to downstream node"
+ },
+ {
+ "id": "END_INPUT_CONNECTED",
+ "rule": "At least one End node's 'input' port MUST be connected",
+ "failureMessage": "At least one End node must have incoming connection"
+ },
+ {
+ "id": "ALL_NODES_REACHABLE",
+ "rule": "All nodes MUST be reachable from Start node",
+ "failureMessage": "Unreachable nodes found - ensure all nodes form a connected path from Start"
+ },
+ {
+ "id": "ALL_PATHS_LEAD_TO_END",
+ "rule": "All execution paths MUST lead to at least one End node",
+ "failureMessage": "Dead-end paths found - ensure all branches converge to an End node"
+ },
+ {
+ "id": "NO_CYCLES",
+ "rule": "Workflow MUST NOT contain cycles",
+ "failureMessage": "Circular reference detected - workflows must be acyclic (DAG)"
+ },
+ {
+ "id": "NODE_ID_REFERENCES_VALID",
+ "rule": "All node IDs in connections must reference existing nodes",
+ "failureMessage": "Connection references non-existent node"
+ },
+ {
+ "id": "CONDITIONAL_BRANCHES_CONNECTED",
+ "rule": "All conditional node branches (IfElse, Switch, AskUserQuestion single-select) MUST have outgoing connections",
+ "failureMessage": "Conditional node has disconnected branch"
+ }
+ ]
+ },
+ "postGenerationChecklist": {
+ "description": "Quick checklist for verifying AI-generated workflows before returning to user",
+ "items": [
+ "✓ Exactly 1 Start node with output connected",
+ "✓ At least 1 End node with input(s) connected",
+ "✓ All non-Start nodes have input connected",
+ "✓ All non-End nodes have output(s) connected",
+ "✓ IfElse nodes: both branches connected",
+ "✓ Switch nodes: all branches connected",
+ "✓ AskUserQuestion nodes (single-select): all options connected",
+ "✓ No dangling nodes",
+ "✓ No circular references",
+ "✓ All node IDs in connections exist in nodes array"
+ ]
+ }
+ }
},
"validationRules": {
"workflow": {
@@ -515,8 +755,26 @@
},
"description": { "type": "string", "required": false },
"version": { "type": "string", "required": true, "pattern": "semver" },
- "nodes": { "type": "array", "required": true },
- "connections": { "type": "array", "required": true },
+ "nodes": {
+ "type": "array",
+ "required": true,
+ "nodePositionGuidelines": {
+ "description": "Each node must have a position: { x, y } in pixels. Follow these spacing rules for readable layouts.",
+ "horizontalSpacing": 300,
+ "verticalSpacing": 350,
+ "rules": [
+ "Place nodes left-to-right in execution order. Increment x by 300 for each step.",
+ "For linear flows, keep all nodes at the same y value.",
+ "For conditional branches (ifElse, switch, askUserQuestion), fan out vertically: center the parent, offset children by ±175 (2 branches) or ±350/0 (3 branches).",
+ "After branches merge back, return to the parent's y value."
+ ]
+ }
+ },
+ "connections": {
+ "type": "array",
+ "required": true,
+ "description": "Array of connections between workflow nodes. Each connection defines a directed edge from one node's output port to another node's input port. See 'connections' section for detailed specifications: format, portNamingRules, and completenessRules."
+ },
"subAgentFlows": {
"type": "array",
"required": false,
@@ -558,14 +816,14 @@
"id": "start-1",
"type": "start",
"name": "start-node",
- "position": { "x": 100, "y": 200 },
+ "position": { "x": 100, "y": 300 },
"data": { "label": "Start" }
},
{
"id": "agent-1",
"type": "subAgent",
"name": "analyzer",
- "position": { "x": 350, "y": 200 },
+ "position": { "x": 400, "y": 300 },
"data": {
"description": "Analyze data",
"prompt": "Analyze data and generate insights.",
@@ -577,7 +835,7 @@
"id": "end-1",
"type": "end",
"name": "end-node",
- "position": { "x": 700, "y": 200 },
+ "position": { "x": 700, "y": 300 },
"data": { "label": "End" }
}
],
@@ -614,7 +872,7 @@
"id": "scanner-1",
"type": "subAgent",
"name": "scanner",
- "position": { "x": 350, "y": 300 },
+ "position": { "x": 400, "y": 300 },
"data": {
"description": "Scan code",
"prompt": "Scan for bugs and issues.",
@@ -625,7 +883,7 @@
"id": "ask-1",
"type": "askUserQuestion",
"name": "priority",
- "position": { "x": 650, "y": 300 },
+ "position": { "x": 700, "y": 300 },
"data": {
"questionText": "Priority level?",
"options": [
@@ -639,7 +897,7 @@
"id": "fix-1",
"type": "subAgent",
"name": "critical-fixer",
- "position": { "x": 950, "y": 150 },
+ "position": { "x": 1000, "y": 125 },
"data": {
"description": "Critical fixes",
"prompt": "Generate critical fixes.",
@@ -650,7 +908,7 @@
"id": "fix-2",
"type": "subAgent",
"name": "all-fixer",
- "position": { "x": 950, "y": 450 },
+ "position": { "x": 1000, "y": 475 },
"data": {
"description": "All fixes",
"prompt": "Generate all fixes.",
@@ -708,7 +966,7 @@
"id": "validator-1",
"type": "subAgent",
"name": "validator",
- "position": { "x": 350, "y": 350 },
+ "position": { "x": 400, "y": 350 },
"data": {
"description": "Validate doc",
"prompt": "Validate document format.",
@@ -719,7 +977,7 @@
"id": "if-1",
"type": "ifElse",
"name": "valid-check",
- "position": { "x": 650, "y": 350 },
+ "position": { "x": 700, "y": 350 },
"data": {
"branches": [
{ "label": "Valid", "condition": "Passed" },
@@ -732,7 +990,7 @@
"id": "analyzer-1",
"type": "subAgent",
"name": "analyzer",
- "position": { "x": 950, "y": 200 },
+ "position": { "x": 1000, "y": 175 },
"data": {
"description": "Analyze content",
"prompt": "Analyze content.",
@@ -743,7 +1001,7 @@
"id": "error-1",
"type": "subAgent",
"name": "error-handler",
- "position": { "x": 950, "y": 500 },
+ "position": { "x": 1000, "y": 525 },
"data": {
"description": "Error report",
"prompt": "Generate error report.",
@@ -754,7 +1012,7 @@
"id": "ask-1",
"type": "askUserQuestion",
"name": "format",
- "position": { "x": 1250, "y": 200 },
+ "position": { "x": 1300, "y": 175 },
"data": {
"questionText": "Output format?",
"options": [
@@ -769,14 +1027,14 @@
"id": "fmt-pdf",
"type": "subAgent",
"name": "pdf-fmt",
- "position": { "x": 1550, "y": 50 },
+ "position": { "x": 1600, "y": -175 },
"data": { "description": "PDF format", "prompt": "Format as PDF.", "outputPorts": 1 }
},
{
"id": "fmt-md",
"type": "subAgent",
"name": "md-fmt",
- "position": { "x": 1550, "y": 200 },
+ "position": { "x": 1600, "y": 175 },
"data": {
"description": "MD format",
"prompt": "Format as Markdown.",
@@ -787,21 +1045,21 @@
"id": "fmt-html",
"type": "subAgent",
"name": "html-fmt",
- "position": { "x": 1550, "y": 350 },
+ "position": { "x": 1600, "y": 525 },
"data": { "description": "HTML format", "prompt": "Format as HTML.", "outputPorts": 1 }
},
{
"id": "end-ok",
"type": "end",
"name": "success",
- "position": { "x": 1900, "y": 200 },
+ "position": { "x": 1900, "y": 175 },
"data": { "label": "Success" }
},
{
"id": "end-err",
"type": "end",
"name": "error",
- "position": { "x": 1300, "y": 500 },
+ "position": { "x": 1300, "y": 525 },
"data": { "label": "Error" }
}
],
diff --git a/resources/workflow-schema.toon b/resources/workflow-schema.toon
index 22500f96..78268322 100644
--- a/resources/workflow-schema.toon
+++ b/resources/workflow-schema.toon
@@ -176,7 +176,7 @@ nodeTypes:
inputPorts: 1
outputPorts: 2-10
skill:
- description: "Reference a Claude Code Skill (specialized agent capability defined in SKILL.md files). Use when user description matches a Skill's documented purpose. **Important: Skills always have exactly 1 output port. For conditional branching based on Skill results, add an ifElse or switch node after the Skill node.**"
+ description: "Reference a Claude Code Skill (specialized agent capability defined in SKILL.md files). ONLY use Skills that are available on the system - do NOT fabricate Skills. If no matching Skill exists, use a prompt or subAgent node instead. **Important: Skills always have exactly 1 output port. For conditional branching based on Skill results, add an ifElse or switch node after the Skill node.**"
fields:
name:
type: string
@@ -368,7 +368,7 @@ nodeTypes:
name: Start
position:
x: 100
- y: 200
+ y: 300
data:
label: Start
- id: sf-end
@@ -376,7 +376,7 @@ nodeTypes:
name: End
position:
x: 400
- y: 200
+ y: 300
data:
label: End
connections[1]{id,from,to,fromPort,toPort}:
@@ -429,9 +429,154 @@ nodeTypes:
description: "Skip Git repository trust check. When true, allows execution outside trusted Git repositories. Use with caution."
inputPorts: 1
outputPorts: 1
-connectionRules:
- forbidden[5]: Start node cannot have input connections,End node cannot have output connections,No cycles allowed,No self-connections,One connection per output port
- required[4]: Exactly one Start node per workflow,At least one End node per workflow,All non-Start nodes must have input connection,All non-End nodes must have output connection
+connections:
+ description: "Comprehensive specification for workflow connections. Defines connection object structure, port naming conventions, and completeness rules for AI-generated workflows."
+ overview:
+ description: High-level connection constraints that apply to all workflows
+ forbidden[4]: Start node cannot have input connections,End node cannot have output connections,No cycles allowed,No self-connections
+ required[5]: At least one connection per output port,Exactly one Start node per workflow,At least one End node per workflow,All non-Start nodes must have input connection,All non-End nodes must have output connection
+ format:
+ description: Connection object structure in connections array
+ objectStructure:
+ id:
+ type: string
+ required: true
+ example: c1
+ description: Unique connection identifier. Must be unique across all connections in the workflow.
+ from:
+ type: string
+ required: true
+ example: start-1
+ description: Source node ID. Must reference an existing node in the nodes array.
+ to:
+ type: string
+ required: true
+ example: prompt-1
+ description: Target node ID. Must reference an existing node in the nodes array.
+ fromPort:
+ type: string
+ required: true
+ description: "Source port identifier. See portNamingRules for complete mapping by node type. Examples: 'output', 'branch-0', 'branch-1'"
+ toPort:
+ type: string
+ required: true
+ enum[1]: input
+ description: Target port is always 'input'. Every node has exactly one input port.
+ condition:
+ type: string
+ required: false
+ example: User selected Option A
+ description: Optional condition description for documenting branch semantics.
+ portNamingRules:
+ description: "Port identifier conventions for each node type. Linear nodes: single output. Conditional nodes: multiple outputs (one per branch)."
+ linearNodes:
+ types[8]: start,end,prompt,subAgent,skill,mcp,subAgentFlow,codex
+ inputPortId: input
+ outputPortId: output
+ connectionExample:
+ id: c1
+ from: start-1
+ to: prompt-1
+ fromPort: output
+ toPort: input
+ ifElseNode:
+ type: ifElse
+ inputPortId: input
+ outputPortIds[2]: branch-0,branch-1
+ semantics: "branch-0 = true/if condition, branch-1 = false/else condition"
+ constraint: BOTH branch-0 and branch-1 MUST each have exactly one outgoing connection
+ connectionExample:
+ id: c2
+ from: ifelse-1
+ to: prompt-true
+ fromPort: branch-0
+ toPort: input
+ switchNode:
+ type: switch
+ inputPortId: input
+ outputPortIds: "branch-0, branch-1, ..., branch-N (where N = branchesLength - 1)"
+ constraint: ALL branches (branch-0 through branch-N) MUST each have exactly one outgoing connection
+ note: Last branch is typically the 'default' case
+ connectionExample:
+ id: c3
+ from: switch-1
+ to: handler-case-1
+ fromPort: branch-1
+ toPort: input
+ askUserQuestionNode:
+ type: askUserQuestion
+ inputPortId: input
+ outputPortIds: DYNAMIC - depends on configuration
+ modes:
+ mode_aiSuggestions:
+ condition: "useAiSuggestions: true"
+ outputPortId: output
+ description: AI generates options at runtime. Single output port 'output' used.
+ mode_multiSelect:
+ condition: "multiSelect: true"
+ outputPortId: output
+ description: Multi-select enabled. Single output port 'output' used.
+ mode_singleSelect:
+ condition: "useAiSuggestions: false AND multiSelect: false"
+ outputPortIds: "branch-0, branch-1, ..., branch-N (where N = optionsLength - 1)"
+ constraint: ALL branches (branch-0 through branch-N) MUST each have exactly one outgoing connection
+ description: Single-select with user-defined options. One port per option.
+ connectionExample:
+ id: c4
+ from: ask-1
+ to: handler-option-a
+ fromPort: branch-0
+ toPort: input
+ completenessRules:
+ description: Completeness specification for workflow connections. Distinguishes between AI generation guidelines (what AI should produce) and export validation rules (what must be true at export/execution time). Manual canvas editing allows incomplete states - validation only occurs at export.
+ aiGenerationRules:
+ description: Guidelines for AI-generated and AI-refined workflows. The AI system should ensure these rules are satisfied when generating/refining workflows. These guarantee that generated workflows are complete and ready to export.
+ rules[10]:
+ - id: START_OUTPUT
+ rule: Every Start node's 'output' port MUST have at least one outgoing connection
+ aiGuidance: "When generating/adding a Start node, immediately create a connection from its 'output' port to the next node"
+ - id: NON_END_OUTPUT
+ rule: Every non-End node's output port(s) MUST have at least one outgoing connection per port
+ example: IfElse node with branch-0 and branch-1 requires both branches to be connected
+ - id: NON_START_INPUT
+ rule: Every non-Start node's 'input' port MUST have exactly one incoming connection
+ aiGuidance: "When creating or moving a node, ensure it receives an input connection from a predecessor"
+ - id: END_INPUT
+ rule: Every End node's 'input' port can have one OR MORE incoming connections (merge point allowed)
+ example: Multiple upstream paths can converge to a single End node
+ - id: IFELSE_BOTH_BRANCHES
+ rule: "IfElse node: BOTH branch-0 and branch-1 MUST have outgoing connections"
+ aiGuidance: Create both connections before moving to next node
+ - id: SWITCH_ALL_BRANCHES
+ rule: "Switch node: ALL branch ports (branch-0 through branch-N) MUST have outgoing connections"
+ aiGuidance: "Count branches, create N connections (one per branch)"
+ - id: ASKUSERQUESTION_ALL_OPTIONS
+ rule: "AskUserQuestion (single-select): ALL branch ports (branch-0 through branch-N) MUST have outgoing connections"
+ aiGuidance: "For single-select with N options, create N connections"
+ - id: NODE_ADDITION
+ rule: "When adding a new node, immediately add an input connection to it (except Start)"
+ aiGuidance: Never leave nodes disconnected. Node creation must be paired with connection creation.
+ - id: NODE_REMOVAL
+ rule: "When removing a node, remove ALL connections associated with that node"
+ aiGuidance: "Before deleting a node, remove all edges where node is source or target"
+ - id: NODE_ID_VALIDITY
+ rule: Node IDs referenced in connections must exist in nodes array
+ validation: "For each connection: verify 'from' node exists, verify 'to' node exists"
+ exportValidationRules:
+ description: Mandatory validation rules checked when exporting or executing a workflow. These rules apply to both manually-edited and AI-generated workflows. Workflows must pass these validations before execution.
+ rules[9]{id,rule,failureMessage}:
+ EXACTLY_ONE_START,Workflow MUST have exactly one Start node,Workflow must have exactly one Start node
+ AT_LEAST_ONE_END,Workflow MUST have at least one End node,Workflow must have at least one End node
+ START_OUTPUT_CONNECTED,Start node's 'output' port MUST be connected,Start node output must be connected to downstream node
+ END_INPUT_CONNECTED,At least one End node's 'input' port MUST be connected,At least one End node must have incoming connection
+ ALL_NODES_REACHABLE,All nodes MUST be reachable from Start node,Unreachable nodes found - ensure all nodes form a connected path from Start
+ ALL_PATHS_LEAD_TO_END,All execution paths MUST lead to at least one End node,Dead-end paths found - ensure all branches converge to an End node
+ NO_CYCLES,Workflow MUST NOT contain cycles,Circular reference detected - workflows must be acyclic (DAG)
+ NODE_ID_REFERENCES_VALID,All node IDs in connections must reference existing nodes,Connection references non-existent node
+ CONDITIONAL_BRANCHES_CONNECTED,"All conditional node branches (IfElse, Switch, AskUserQuestion single-select) MUST have outgoing connections",Conditional node has disconnected branch
+ postGenerationChecklist:
+ description: Quick checklist for verifying AI-generated workflows before returning to user
+ items[10]: ✓ Exactly 1 Start node with output connected,✓ At least 1 End node with input(s) connected,✓ All non-Start nodes have input connected,✓ All non-End nodes have output(s) connected,"✓ IfElse nodes: both branches connected","✓ Switch nodes: all branches connected","✓ AskUserQuestion nodes (single-select): all options connected",✓ No dangling nodes,✓ No circular references,✓ All node IDs in connections exist in nodes array
validationRules:
workflow:
maxNodes: 50
@@ -470,9 +615,15 @@ workflowStructure:
nodes:
type: array
required: true
+ nodePositionGuidelines:
+ description: "Each node must have a position: { x, y } in pixels. Follow these spacing rules for readable layouts."
+ horizontalSpacing: 300
+ verticalSpacing: 350
+ rules[4]: Place nodes left-to-right in execution order. Increment x by 300 for each step.,"For linear flows, keep all nodes at the same y value.","For conditional branches (ifElse, switch, askUserQuestion), fan out vertically: center the parent, offset children by ±175 (2 branches) or ±350/0 (3 branches).","After branches merge back, return to the parent's y value."
connections:
type: array
required: true
+ description: "Array of connections between workflow nodes. Each connection defines a directed edge from one node's output port to another node's input port. See 'connections' section for detailed specifications: format, portNamingRules, and completenessRules."
subAgentFlows:
type: array
required: false
@@ -519,15 +670,15 @@ examples[3]:
name: start-node
position:
x: 100
- y: 200
+ y: 300
data:
label: Start
- id: agent-1
type: subAgent
name: analyzer
position:
- x: 350
- y: 200
+ x: 400
+ y: 300
data:
description: Analyze data
prompt: Analyze data and generate insights.
@@ -538,7 +689,7 @@ examples[3]:
name: end-node
position:
x: 700
- y: 200
+ y: 300
data:
label: End
connections[2]{id,from,to,fromPort,toPort}:
@@ -565,7 +716,7 @@ examples[3]:
type: subAgent
name: scanner
position:
- x: 350
+ x: 400
y: 300
data:
description: Scan code
@@ -575,7 +726,7 @@ examples[3]:
type: askUserQuestion
name: priority
position:
- x: 650
+ x: 700
y: 300
data:
questionText: Priority level?
@@ -587,8 +738,8 @@ examples[3]:
type: subAgent
name: critical-fixer
position:
- x: 950
- y: 150
+ x: 1000
+ y: 125
data:
description: Critical fixes
prompt: Generate critical fixes.
@@ -597,8 +748,8 @@ examples[3]:
type: subAgent
name: all-fixer
position:
- x: 950
- y: 450
+ x: 1000
+ y: 475
data:
description: All fixes
prompt: Generate all fixes.
@@ -639,7 +790,7 @@ examples[3]:
type: subAgent
name: validator
position:
- x: 350
+ x: 400
y: 350
data:
description: Validate doc
@@ -649,7 +800,7 @@ examples[3]:
type: ifElse
name: valid-check
position:
- x: 650
+ x: 700
y: 350
data:
branches[2]{label,condition}:
@@ -660,8 +811,8 @@ examples[3]:
type: subAgent
name: analyzer
position:
- x: 950
- y: 200
+ x: 1000
+ y: 175
data:
description: Analyze content
prompt: Analyze content.
@@ -670,8 +821,8 @@ examples[3]:
type: subAgent
name: error-handler
position:
- x: 950
- y: 500
+ x: 1000
+ y: 525
data:
description: Error report
prompt: Generate error report.
@@ -680,8 +831,8 @@ examples[3]:
type: askUserQuestion
name: format
position:
- x: 1250
- y: 200
+ x: 1300
+ y: 175
data:
questionText: Output format?
options[3]{label,description}:
@@ -693,8 +844,8 @@ examples[3]:
type: subAgent
name: pdf-fmt
position:
- x: 1550
- y: 50
+ x: 1600
+ y: -175
data:
description: PDF format
prompt: Format as PDF.
@@ -703,8 +854,8 @@ examples[3]:
type: subAgent
name: md-fmt
position:
- x: 1550
- y: 200
+ x: 1600
+ y: 175
data:
description: MD format
prompt: Format as Markdown.
@@ -713,8 +864,8 @@ examples[3]:
type: subAgent
name: html-fmt
position:
- x: 1550
- y: 350
+ x: 1600
+ y: 525
data:
description: HTML format
prompt: Format as HTML.
@@ -724,7 +875,7 @@ examples[3]:
name: success
position:
x: 1900
- y: 200
+ y: 175
data:
label: Success
- id: end-err
@@ -732,7 +883,7 @@ examples[3]:
name: error
position:
x: 1300
- y: 500
+ y: 525
data:
label: Error
connections[12]{id,from,to,fromPort,toPort}:
diff --git a/scripts/generate-toon-schema.ts b/scripts/generate-toon-schema.ts
index 28b23dfb..28028c36 100644
--- a/scripts/generate-toon-schema.ts
+++ b/scripts/generate-toon-schema.ts
@@ -51,7 +51,21 @@ function createBasicSchema(fullSchema: Record): Record | undefined;
+ if (connections) {
+ const portNamingRules = connections.portNamingRules as Record | undefined;
+ if (portNamingRules) {
+ const linearNodes = portNamingRules.linearNodes as Record | undefined;
+ if (linearNodes?.types && Array.isArray(linearNodes.types)) {
+ linearNodes.types = linearNodes.types.filter(
+ (t: string) => t !== 'subAgent' && t !== 'subAgentFlow'
+ );
+ }
+ }
+ }
+
+ // 6. Remove examples that use subAgent nodes
if (schema.examples && Array.isArray(schema.examples)) {
schema.examples = schema.examples.filter((example: Record) => {
const workflow = example.workflow as Record | undefined;
diff --git a/src/extension/commands/gemini-handlers.ts b/src/extension/commands/gemini-handlers.ts
new file mode 100644
index 00000000..aef4b978
--- /dev/null
+++ b/src/extension/commands/gemini-handlers.ts
@@ -0,0 +1,260 @@
+/**
+ * Claude Code Workflow Studio - Gemini CLI Integration Handlers
+ *
+ * Handles Export/Run for Google Gemini CLI integration
+ *
+ * @beta This is a PoC feature for Google Gemini CLI integration
+ */
+
+import * as vscode from 'vscode';
+import type {
+ ExportForGeminiCliPayload,
+ ExportForGeminiCliSuccessPayload,
+ GeminiOperationFailedPayload,
+ RunForGeminiCliPayload,
+ RunForGeminiCliSuccessPayload,
+} from '../../shared/types/messages';
+import { extractMcpServerIdsFromWorkflow } from '../services/copilot-export-service';
+import type { FileService } from '../services/file-service';
+import {
+ previewMcpSyncForGeminiCli,
+ syncMcpConfigForGeminiCli,
+} from '../services/gemini-mcp-sync-service';
+import {
+ checkExistingGeminiSkill,
+ exportWorkflowAsGeminiSkill,
+} from '../services/gemini-skill-export-service';
+import {
+ hasNonStandardSkills,
+ promptAndNormalizeSkills,
+} from '../services/skill-normalization-service';
+import { executeGeminiCliInTerminal } from '../services/terminal-execution-service';
+
+/**
+ * Handle Export for Gemini CLI request
+ *
+ * Exports workflow to Skills format (.gemini/skills/name/SKILL.md)
+ *
+ * @param fileService - File service instance
+ * @param webview - Webview for sending responses
+ * @param payload - Export payload
+ * @param requestId - Optional request ID for response correlation
+ */
+export async function handleExportForGeminiCli(
+ fileService: FileService,
+ webview: vscode.Webview,
+ payload: ExportForGeminiCliPayload,
+ requestId?: string
+): Promise {
+ try {
+ const { workflow } = payload;
+
+ // Check for existing skill and ask for confirmation
+ const existingSkillPath = await checkExistingGeminiSkill(workflow, fileService);
+ if (existingSkillPath) {
+ const result = await vscode.window.showWarningMessage(
+ `Skill already exists: ${existingSkillPath}\n\nOverwrite?`,
+ { modal: true },
+ 'Overwrite',
+ 'Cancel'
+ );
+ if (result !== 'Overwrite') {
+ webview.postMessage({
+ type: 'EXPORT_FOR_GEMINI_CLI_CANCELLED',
+ requestId,
+ });
+ return;
+ }
+ }
+
+ // Export workflow as skill to .gemini/skills/{name}/SKILL.md
+ const exportResult = await exportWorkflowAsGeminiSkill(workflow, fileService);
+
+ if (!exportResult.success) {
+ const failedPayload: GeminiOperationFailedPayload = {
+ errorCode: 'EXPORT_FAILED',
+ errorMessage: exportResult.errors?.join(', ') || 'Failed to export workflow as skill',
+ timestamp: new Date().toISOString(),
+ };
+ webview.postMessage({
+ type: 'EXPORT_FOR_GEMINI_CLI_FAILED',
+ requestId,
+ payload: failedPayload,
+ });
+ return;
+ }
+
+ // Send success response
+ const successPayload: ExportForGeminiCliSuccessPayload = {
+ skillName: exportResult.skillName,
+ skillPath: exportResult.skillPath,
+ timestamp: new Date().toISOString(),
+ };
+
+ webview.postMessage({
+ type: 'EXPORT_FOR_GEMINI_CLI_SUCCESS',
+ requestId,
+ payload: successPayload,
+ });
+
+ vscode.window.showInformationMessage(
+ `Exported workflow as Gemini skill: ${exportResult.skillPath}`
+ );
+ } catch (error) {
+ const failedPayload: GeminiOperationFailedPayload = {
+ errorCode: 'UNKNOWN_ERROR',
+ errorMessage: error instanceof Error ? error.message : 'Unknown error',
+ timestamp: new Date().toISOString(),
+ };
+ webview.postMessage({
+ type: 'EXPORT_FOR_GEMINI_CLI_FAILED',
+ requestId,
+ payload: failedPayload,
+ });
+ }
+}
+
+/**
+ * Handle Run for Gemini CLI request
+ *
+ * Exports workflow to Skills format and runs it via Gemini CLI
+ *
+ * @param fileService - File service instance
+ * @param webview - Webview for sending responses
+ * @param payload - Run payload
+ * @param requestId - Optional request ID for response correlation
+ */
+export async function handleRunForGeminiCli(
+ fileService: FileService,
+ webview: vscode.Webview,
+ payload: RunForGeminiCliPayload,
+ requestId?: string
+): Promise {
+ try {
+ const { workflow } = payload;
+ const workspacePath = fileService.getWorkspacePath();
+
+ // Step 0.5: Normalize skills (copy non-standard skills to .claude/skills/)
+ // For Gemini CLI, .gemini/skills/ is considered "native" (no copy needed)
+ if (hasNonStandardSkills(workflow, 'gemini')) {
+ const normalizeResult = await promptAndNormalizeSkills(workflow, 'gemini');
+
+ if (!normalizeResult.success) {
+ if (normalizeResult.cancelled) {
+ webview.postMessage({
+ type: 'RUN_FOR_GEMINI_CLI_CANCELLED',
+ requestId,
+ });
+ return;
+ }
+ throw new Error(normalizeResult.error || 'Failed to copy skills to .claude/skills/');
+ }
+
+ // Log normalized skills
+ if (normalizeResult.normalizedSkills && normalizeResult.normalizedSkills.length > 0) {
+ console.log(
+ `[Gemini CLI] Copied ${normalizeResult.normalizedSkills.length} skill(s) to .claude/skills/`
+ );
+ }
+ }
+
+ // Step 1: Check if MCP servers need to be synced to ~/.gemini/settings.json
+ const mcpServerIds = extractMcpServerIdsFromWorkflow(workflow);
+ let mcpSyncConfirmed = false;
+
+ if (mcpServerIds.length > 0) {
+ const mcpSyncPreview = await previewMcpSyncForGeminiCli(mcpServerIds, workspacePath);
+
+ if (mcpSyncPreview.serversToAdd.length > 0) {
+ const serverList = mcpSyncPreview.serversToAdd.map((s) => ` • ${s}`).join('\n');
+ const result = await vscode.window.showInformationMessage(
+ `The following MCP servers will be added to ~/.gemini/settings.json for Gemini CLI:\n\n${serverList}\n\nProceed?`,
+ { modal: true },
+ 'Yes',
+ 'No'
+ );
+ mcpSyncConfirmed = result === 'Yes';
+ }
+ }
+
+ // Step 2: Check for existing skill and ask for confirmation
+ const existingSkillPath = await checkExistingGeminiSkill(workflow, fileService);
+ if (existingSkillPath) {
+ const result = await vscode.window.showWarningMessage(
+ `Skill already exists: ${existingSkillPath}\n\nOverwrite?`,
+ { modal: true },
+ 'Overwrite',
+ 'Cancel'
+ );
+ if (result !== 'Overwrite') {
+ webview.postMessage({
+ type: 'RUN_FOR_GEMINI_CLI_CANCELLED',
+ requestId,
+ });
+ return;
+ }
+ }
+
+ // Step 3: Export workflow as skill to .gemini/skills/{name}/SKILL.md
+ const exportResult = await exportWorkflowAsGeminiSkill(workflow, fileService);
+
+ if (!exportResult.success) {
+ const failedPayload: GeminiOperationFailedPayload = {
+ errorCode: 'EXPORT_FAILED',
+ errorMessage: exportResult.errors?.join(', ') || 'Failed to export workflow as skill',
+ timestamp: new Date().toISOString(),
+ };
+ webview.postMessage({
+ type: 'RUN_FOR_GEMINI_CLI_FAILED',
+ requestId,
+ payload: failedPayload,
+ });
+ return;
+ }
+
+ // Step 4: Sync MCP servers to ~/.gemini/settings.json if confirmed
+ let syncedMcpServers: string[] = [];
+ if (mcpSyncConfirmed) {
+ syncedMcpServers = await syncMcpConfigForGeminiCli(mcpServerIds, workspacePath);
+ }
+
+ // Step 5: Execute in terminal
+ const terminalResult = executeGeminiCliInTerminal({
+ skillName: exportResult.skillName,
+ workingDirectory: workspacePath,
+ });
+
+ // Send success response
+ const successPayload: RunForGeminiCliSuccessPayload = {
+ workflowName: workflow.name,
+ terminalName: terminalResult.terminalName,
+ timestamp: new Date().toISOString(),
+ };
+
+ webview.postMessage({
+ type: 'RUN_FOR_GEMINI_CLI_SUCCESS',
+ requestId,
+ payload: successPayload,
+ });
+
+ // Show notification with config sync info
+ const configInfo =
+ syncedMcpServers.length > 0
+ ? ` (MCP servers: ${syncedMcpServers.join(', ')} added to ~/.gemini/settings.json)`
+ : '';
+ vscode.window.showInformationMessage(
+ `Running workflow via Gemini CLI: ${workflow.name}${configInfo}`
+ );
+ } catch (error) {
+ const failedPayload: GeminiOperationFailedPayload = {
+ errorCode: 'UNKNOWN_ERROR',
+ errorMessage: error instanceof Error ? error.message : 'Unknown error',
+ timestamp: new Date().toISOString(),
+ };
+ webview.postMessage({
+ type: 'RUN_FOR_GEMINI_CLI_FAILED',
+ requestId,
+ payload: failedPayload,
+ });
+ }
+}
diff --git a/src/extension/commands/open-editor.ts b/src/extension/commands/open-editor.ts
index 69dd1835..19d2aae7 100644
--- a/src/extension/commands/open-editor.ts
+++ b/src/extension/commands/open-editor.ts
@@ -41,6 +41,7 @@ import {
handleRunForCopilotCli,
} from './copilot-handlers';
import { handleExportWorkflow, handleExportWorkflowForExecution } from './export-workflow';
+import { handleExportForGeminiCli, handleRunForGeminiCli } from './gemini-handlers';
import { loadWorkflow } from './load-workflow';
import { loadWorkflowList } from './load-workflow-list';
import {
@@ -515,6 +516,50 @@ export function registerOpenEditorCommand(
}
break;
+ case 'EXPORT_FOR_GEMINI_CLI':
+ // Export workflow for Gemini CLI (Skills format)
+ if (message.payload?.workflow) {
+ await handleExportForGeminiCli(
+ fileService,
+ webview,
+ message.payload,
+ message.requestId
+ );
+ } else {
+ webview.postMessage({
+ type: 'EXPORT_FOR_GEMINI_CLI_FAILED',
+ requestId: message.requestId,
+ payload: {
+ errorCode: 'UNKNOWN_ERROR',
+ errorMessage: 'Workflow is required',
+ timestamp: new Date().toISOString(),
+ },
+ });
+ }
+ break;
+
+ case 'RUN_FOR_GEMINI_CLI':
+ // Run workflow for Gemini CLI (via Gemini CLI terminal)
+ if (message.payload?.workflow) {
+ await handleRunForGeminiCli(
+ fileService,
+ webview,
+ message.payload,
+ message.requestId
+ );
+ } else {
+ webview.postMessage({
+ type: 'RUN_FOR_GEMINI_CLI_FAILED',
+ requestId: message.requestId,
+ payload: {
+ errorCode: 'UNKNOWN_ERROR',
+ errorMessage: 'Workflow is required',
+ timestamp: new Date().toISOString(),
+ },
+ });
+ }
+ break;
+
case 'LOAD_WORKFLOW_LIST':
// Load workflow list
await loadWorkflowList(fileService, webview, message.requestId);
diff --git a/src/extension/services/ai-editing-skill-service.ts b/src/extension/services/ai-editing-skill-service.ts
index 6ef8c771..1b33c61f 100644
--- a/src/extension/services/ai-editing-skill-service.ts
+++ b/src/extension/services/ai-editing-skill-service.ts
@@ -17,7 +17,8 @@ export type AiEditingProvider =
| 'copilot-cli'
| 'copilot-vscode'
| 'codex'
- | 'roo-code';
+ | 'roo-code'
+ | 'gemini';
const SKILL_NAME = 'cc-workflow-ai-editor';
@@ -36,6 +37,8 @@ function getSkillDestination(provider: AiEditingProvider, workingDirectory: stri
return path.join(workingDirectory, '.codex', 'skills', SKILL_NAME, 'SKILL.md');
case 'roo-code':
return path.join(workingDirectory, '.roo', 'skills', SKILL_NAME, 'SKILL.md');
+ case 'gemini':
+ return path.join(workingDirectory, '.gemini', 'skills', SKILL_NAME, 'SKILL.md');
}
}
@@ -128,6 +131,17 @@ async function launchProvider(
}
break;
}
+
+ case 'gemini': {
+ const terminalName = `AI Edit: Gemini CLI`;
+ const terminal = vscode.window.createTerminal({
+ name: terminalName,
+ cwd: workingDirectory,
+ });
+ terminal.show(true);
+ terminal.sendText(`gemini -i ":skill ${SKILL_NAME}"`);
+ break;
+ }
}
}
diff --git a/src/extension/services/gemini-cli-path.ts b/src/extension/services/gemini-cli-path.ts
new file mode 100644
index 00000000..4399f149
--- /dev/null
+++ b/src/extension/services/gemini-cli-path.ts
@@ -0,0 +1,108 @@
+/**
+ * Gemini CLI Path Detection Service
+ *
+ * Detects Gemini CLI executable path using the shared CLI path detector.
+ * Uses VSCode's default terminal setting to get the user's shell,
+ * then executes with login shell to get the full PATH environment.
+ *
+ * This handles GUI-launched VSCode scenarios where the Extension Host
+ * doesn't inherit the user's shell PATH settings.
+ *
+ * Based on: codex-cli-path.ts
+ */
+
+import { log } from '../extension';
+import {
+ findExecutableInPath,
+ findExecutableViaDefaultShell,
+ verifyExecutable,
+} from './cli-path-detector';
+
+/**
+ * Cached Gemini CLI path
+ * undefined = not checked yet
+ * null = not found (use npx fallback)
+ * string = path to gemini executable
+ */
+let cachedGeminiPath: string | null | undefined;
+
+/**
+ * Get the path to Gemini CLI executable
+ * Detection order:
+ * 1. VSCode default terminal shell (handles version managers like mise, nvm)
+ * 2. Direct PATH lookup (fallback for terminal-launched VSCode)
+ * 3. npx fallback (handled in getGeminiSpawnCommand)
+ *
+ * @returns Path to gemini executable (full path or 'gemini' for PATH), null for npx fallback
+ */
+export async function getGeminiCliPath(): Promise {
+ // Return cached result if available
+ if (cachedGeminiPath !== undefined) {
+ return cachedGeminiPath;
+ }
+
+ // 1. Try VSCode default terminal (handles GUI-launched VSCode + version managers)
+ const shellPath = await findExecutableViaDefaultShell('gemini');
+ if (shellPath) {
+ const version = await verifyExecutable(shellPath);
+ if (version) {
+ log('INFO', 'Gemini CLI found via default shell', {
+ path: shellPath,
+ version,
+ });
+ cachedGeminiPath = shellPath;
+ return shellPath;
+ }
+ log('WARN', 'Gemini CLI found but not executable', { path: shellPath });
+ }
+
+ // 2. Fall back to direct PATH lookup (terminal-launched VSCode)
+ const pathResult = await findExecutableInPath('gemini');
+ if (pathResult) {
+ cachedGeminiPath = 'gemini';
+ return 'gemini';
+ }
+
+ log('INFO', 'Gemini CLI not found, will use npx fallback');
+ cachedGeminiPath = null;
+ return null;
+}
+
+/**
+ * Clear Gemini CLI path cache
+ * Useful for testing or when user installs Gemini CLI during session
+ */
+export function clearGeminiCliPathCache(): void {
+ cachedGeminiPath = undefined;
+}
+
+/**
+ * Get the command and args for spawning Gemini CLI
+ * Uses gemini directly if available, otherwise falls back to 'npx @google/gemini-cli'
+ * npx detection order:
+ * 1. VSCode default terminal shell (handles version managers)
+ * 2. Direct PATH lookup
+ *
+ * @returns command path with 'npx:' prefix if using npx fallback, or null if not found
+ */
+export async function getGeminiSpawnCommand(): Promise {
+ const geminiPath = await getGeminiCliPath();
+
+ if (geminiPath) {
+ return geminiPath;
+ }
+
+ // Fallback: Try npx @google/gemini-cli
+ // Return a special marker that the caller will handle
+ const npxPath = await findExecutableViaDefaultShell('npx');
+ if (npxPath) {
+ log('INFO', 'Using npx from default shell for Gemini CLI fallback', {
+ path: npxPath,
+ });
+ return `npx:${npxPath}`;
+ }
+
+ // Final fallback to direct PATH lookup
+ log('INFO', 'Using npx from PATH for Gemini CLI fallback');
+ return 'npx:npx';
+}
diff --git a/src/extension/services/gemini-mcp-sync-service.ts b/src/extension/services/gemini-mcp-sync-service.ts
new file mode 100644
index 00000000..62914ddb
--- /dev/null
+++ b/src/extension/services/gemini-mcp-sync-service.ts
@@ -0,0 +1,207 @@
+/**
+ * Claude Code Workflow Studio - Gemini CLI MCP Sync Service
+ *
+ * Handles MCP server configuration sync to ~/.gemini/settings.json
+ * for Google Gemini CLI execution.
+ *
+ * Note: Gemini CLI uses JSON format for configuration:
+ * - Config path: ~/.gemini/settings.json
+ * - MCP servers section: mcpServers key
+ *
+ * @beta This is a PoC feature for Google Gemini CLI integration
+ */
+
+import * as fs from 'node:fs/promises';
+import * as os from 'node:os';
+import * as path from 'node:path';
+import { getMcpServerConfig } from './mcp-config-reader';
+
+/**
+ * Gemini CLI settings.json structure
+ */
+interface GeminiConfig {
+ mcpServers?: Record;
+ [key: string]: unknown;
+}
+
+/**
+ * MCP server configuration entry for Gemini CLI
+ */
+interface GeminiMcpServerEntry {
+ command?: string;
+ args?: string[];
+ env?: Record;
+ url?: string;
+}
+
+/**
+ * Preview result for MCP server sync
+ */
+export interface GeminiMcpSyncPreviewResult {
+ /** Server IDs that would be added to ~/.gemini/settings.json */
+ serversToAdd: string[];
+ /** Server IDs that already exist in ~/.gemini/settings.json */
+ existingServers: string[];
+ /** Server IDs not found in any Claude Code config */
+ missingServers: string[];
+}
+
+/**
+ * Get the Gemini CLI config file path
+ */
+function getGeminiConfigPath(): string {
+ return path.join(os.homedir(), '.gemini', 'settings.json');
+}
+
+/**
+ * Read existing Gemini CLI config
+ */
+async function readGeminiConfig(): Promise {
+ const configPath = getGeminiConfigPath();
+
+ try {
+ const content = await fs.readFile(configPath, 'utf-8');
+ return JSON.parse(content) as GeminiConfig;
+ } catch {
+ // File doesn't exist or invalid JSON
+ return { mcpServers: {} };
+ }
+}
+
+/**
+ * Write Gemini CLI config to file
+ *
+ * @param config - Config to write
+ */
+async function writeGeminiConfig(config: GeminiConfig): Promise {
+ const configPath = getGeminiConfigPath();
+ const configDir = path.dirname(configPath);
+
+ // Ensure ~/.gemini directory exists
+ await fs.mkdir(configDir, { recursive: true });
+
+ // Serialize config to JSON
+ await fs.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`);
+}
+
+/**
+ * Preview which MCP servers would be synced to ~/.gemini/settings.json
+ *
+ * This function checks without actually writing, allowing for confirmation dialogs.
+ *
+ * @param serverIds - Server IDs to sync
+ * @param workspacePath - Workspace path for resolving project-scoped configs
+ * @returns Preview of servers to add, existing, and missing
+ */
+export async function previewMcpSyncForGeminiCli(
+ serverIds: string[],
+ workspacePath: string
+): Promise {
+ if (serverIds.length === 0) {
+ return { serversToAdd: [], existingServers: [], missingServers: [] };
+ }
+
+ const existingConfig = await readGeminiConfig();
+ const existingServersMap = existingConfig.mcpServers || {};
+
+ const serversToAdd: string[] = [];
+ const existingServers: string[] = [];
+ const missingServers: string[] = [];
+
+ for (const serverId of serverIds) {
+ if (existingServersMap[serverId]) {
+ existingServers.push(serverId);
+ } else {
+ // Check if server config exists in Claude Code
+ const serverConfig = getMcpServerConfig(serverId, workspacePath);
+ if (serverConfig) {
+ serversToAdd.push(serverId);
+ } else {
+ missingServers.push(serverId);
+ }
+ }
+ }
+
+ return { serversToAdd, existingServers, missingServers };
+}
+
+/**
+ * Sync MCP server configurations to ~/.gemini/settings.json for Gemini CLI
+ *
+ * Reads MCP server configs from all Claude Code scopes (project, local, user)
+ * and writes them to ~/.gemini/settings.json in JSON format.
+ * Only adds servers that don't already exist in the config file.
+ *
+ * JSON output format:
+ * ```json
+ * {
+ * "mcpServers": {
+ * "my-server": {
+ * "command": "npx",
+ * "args": ["-y", "@my-mcp/server"],
+ * "env": { "API_KEY": "xxx" }
+ * }
+ * }
+ * }
+ * ```
+ *
+ * @param serverIds - Server IDs to sync
+ * @param workspacePath - Workspace path for resolving project-scoped configs
+ * @returns Array of synced server IDs
+ */
+export async function syncMcpConfigForGeminiCli(
+ serverIds: string[],
+ workspacePath: string
+): Promise {
+ if (serverIds.length === 0) {
+ return [];
+ }
+
+ // Read existing config
+ const config = await readGeminiConfig();
+
+ if (!config.mcpServers) {
+ config.mcpServers = {};
+ }
+
+ // Sync servers from all Claude Code scopes (project, local, user)
+ const syncedServers: string[] = [];
+ for (const serverId of serverIds) {
+ // Skip if already exists in config
+ if (config.mcpServers[serverId]) {
+ continue;
+ }
+
+ // Get server config from Claude Code (searches all scopes)
+ const serverConfig = getMcpServerConfig(serverId, workspacePath);
+ if (!serverConfig) {
+ continue;
+ }
+
+ // Convert to Gemini format
+ const geminiEntry: GeminiMcpServerEntry = {};
+
+ if (serverConfig.command) {
+ geminiEntry.command = serverConfig.command;
+ }
+ if (serverConfig.args && serverConfig.args.length > 0) {
+ geminiEntry.args = serverConfig.args;
+ }
+ if (serverConfig.env && Object.keys(serverConfig.env).length > 0) {
+ geminiEntry.env = serverConfig.env;
+ }
+ if (serverConfig.url) {
+ geminiEntry.url = serverConfig.url;
+ }
+
+ config.mcpServers[serverId] = geminiEntry;
+ syncedServers.push(serverId);
+ }
+
+ // Write updated config if any servers were added
+ if (syncedServers.length > 0) {
+ await writeGeminiConfig(config);
+ }
+
+ return syncedServers;
+}
diff --git a/src/extension/services/gemini-skill-export-service.ts b/src/extension/services/gemini-skill-export-service.ts
new file mode 100644
index 00000000..76af879d
--- /dev/null
+++ b/src/extension/services/gemini-skill-export-service.ts
@@ -0,0 +1,131 @@
+/**
+ * Claude Code Workflow Studio - Gemini Skill Export Service
+ *
+ * Handles workflow export to Google Gemini CLI Skills format (.gemini/skills/name/SKILL.md)
+ *
+ * @beta This is a PoC feature for Google Gemini CLI integration
+ */
+
+import * as path from 'node:path';
+import type { Workflow } from '../../shared/types/workflow-definition';
+import { nodeNameToFileName } from './export-service';
+import type { FileService } from './file-service';
+import {
+ generateExecutionInstructions,
+ generateMermaidFlowchart,
+} from './workflow-prompt-generator';
+
+/**
+ * Gemini skill export result
+ */
+export interface GeminiSkillExportResult {
+ success: boolean;
+ skillPath: string;
+ skillName: string;
+ errors?: string[];
+}
+
+/**
+ * Generate SKILL.md content from workflow for Gemini CLI
+ *
+ * @param workflow - Workflow to convert
+ * @returns SKILL.md content as string
+ */
+export function generateGeminiSkillContent(workflow: Workflow): string {
+ const skillName = nodeNameToFileName(workflow.name);
+
+ // Generate description from workflow metadata or create default
+ const description =
+ workflow.metadata?.description ||
+ `Execute the "${workflow.name}" workflow. This skill guides through a structured workflow with defined steps and decision points.`;
+
+ // Generate YAML frontmatter
+ const frontmatter = `---
+name: ${skillName}
+description: ${description}
+---`;
+
+ // Generate Mermaid flowchart
+ const mermaidContent = generateMermaidFlowchart({
+ nodes: workflow.nodes,
+ connections: workflow.connections,
+ });
+
+ // Generate execution instructions
+ const instructions = generateExecutionInstructions(workflow);
+
+ // Compose SKILL.md body
+ const body = `# ${workflow.name}
+
+## Workflow Diagram
+
+${mermaidContent}
+
+## Execution Instructions
+
+${instructions}`;
+
+ return `${frontmatter}\n\n${body}`;
+}
+
+/**
+ * Check if Gemini skill already exists
+ *
+ * @param workflow - Workflow to check
+ * @param fileService - File service instance
+ * @returns Path to existing skill file, or null if not exists
+ */
+export async function checkExistingGeminiSkill(
+ workflow: Workflow,
+ fileService: FileService
+): Promise {
+ const workspacePath = fileService.getWorkspacePath();
+ const skillName = nodeNameToFileName(workflow.name);
+ const skillPath = path.join(workspacePath, '.gemini', 'skills', skillName, 'SKILL.md');
+
+ if (await fileService.fileExists(skillPath)) {
+ return skillPath;
+ }
+ return null;
+}
+
+/**
+ * Export workflow as Gemini Skill
+ *
+ * Exports to .gemini/skills/{name}/SKILL.md
+ *
+ * @param workflow - Workflow to export
+ * @param fileService - File service instance
+ * @returns Export result
+ */
+export async function exportWorkflowAsGeminiSkill(
+ workflow: Workflow,
+ fileService: FileService
+): Promise {
+ try {
+ const workspacePath = fileService.getWorkspacePath();
+ const skillName = nodeNameToFileName(workflow.name);
+ const skillDir = path.join(workspacePath, '.gemini', 'skills', skillName);
+ const skillPath = path.join(skillDir, 'SKILL.md');
+
+ // Ensure directory exists
+ await fileService.createDirectory(skillDir);
+
+ // Generate and write SKILL.md content
+ const content = generateGeminiSkillContent(workflow);
+ await fileService.writeFile(skillPath, content);
+
+ return {
+ success: true,
+ skillPath,
+ skillName,
+ };
+ } catch (error) {
+ return {
+ success: false,
+ skillPath: '',
+ skillName: '',
+ errors: [error instanceof Error ? error.message : 'Unknown error'],
+ };
+ }
+}
diff --git a/src/extension/services/mcp-config-reader.ts b/src/extension/services/mcp-config-reader.ts
index 5d89a5e4..7fbc9a0b 100644
--- a/src/extension/services/mcp-config-reader.ts
+++ b/src/extension/services/mcp-config-reader.ts
@@ -21,6 +21,10 @@
*
* Codex CLI:
* - ~/.codex/config.toml (user-level, TOML format with [mcp_servers.*] sections)
+ *
+ * Gemini CLI:
+ * - ~/.gemini/settings.json (user-level)
+ * - /.gemini/settings.json (project-level)
*/
import * as fs from 'node:fs';
@@ -32,6 +36,8 @@ import { log } from '../extension';
import {
getCodexUserMcpConfigPath,
getCopilotUserMcpConfigPath,
+ getGeminiProjectMcpConfigPath,
+ getGeminiUserMcpConfigPath,
getVSCodeMcpConfigPath,
} from '../utils/path-utils';
@@ -204,6 +210,69 @@ function readCopilotMcpConfig(configPath: string): Record | null {
+ try {
+ const content = fs.readFileSync(configPath, 'utf-8');
+ const parsed = JSON.parse(content);
+ const rawServers = parsed.mcpServers;
+
+ if (!rawServers || typeof rawServers !== 'object') {
+ return null;
+ }
+
+ // Gemini settings.json entries may have url without type field.
+ // normalizeServerConfig cannot infer http vs sse, so we pre-normalize here:
+ // - url present → type 'http'
+ // - command present → type 'stdio'
+ const servers: Record = {};
+
+ for (const [serverId, raw] of Object.entries(
+ rawServers as Record>
+ )) {
+ if (raw.type) {
+ servers[serverId] = raw as McpServerConfig;
+ } else if (raw.command) {
+ servers[serverId] = { ...raw, type: 'stdio' } as McpServerConfig;
+ } else if (raw.url) {
+ servers[serverId] = { ...raw, type: 'http' } as McpServerConfig;
+ } else {
+ log('WARN', 'Invalid Gemini MCP server configuration (no command or url)', {
+ serverId,
+ configPath,
+ });
+ }
+ }
+
+ if (Object.keys(servers).length === 0) {
+ return null;
+ }
+
+ log('INFO', 'Successfully read Gemini settings.json', {
+ configPath,
+ serverCount: Object.keys(servers).length,
+ });
+
+ return servers;
+ } catch (error) {
+ // File not found is expected
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
+ return null;
+ }
+
+ log('WARN', 'Failed to read Gemini settings.json', {
+ configPath,
+ error: error instanceof Error ? error.message : String(error),
+ });
+ return null;
+ }
+}
+
/**
* Read VSCode Copilot MCP config (.vscode/mcp.json)
*
@@ -531,8 +600,46 @@ export function getMcpServerConfig(
}
}
- // Server not found in any configuration (Claude, Copilot, or Codex)
- log('WARN', 'MCP server not found in any configuration (Claude, Copilot, Codex)', {
+ // =========================================================================
+ // Gemini source (Priority 9-10)
+ // =========================================================================
+
+ // Priority 9: Gemini CLI user-scope (~/.gemini/settings.json)
+ const geminiUserConfigPath = getGeminiUserMcpConfigPath();
+ const geminiUserConfig = readGeminiMcpConfig(geminiUserConfigPath);
+ if (geminiUserConfig?.[serverId]) {
+ const serverConfig = normalizeServerConfig(geminiUserConfig[serverId]);
+ if (serverConfig) {
+ log('INFO', 'Retrieved MCP server configuration from Gemini CLI user scope', {
+ serverId,
+ scope: 'gemini-user',
+ configPath: geminiUserConfigPath,
+ type: serverConfig.type,
+ });
+ return { ...serverConfig, source: 'gemini' };
+ }
+ }
+
+ // Priority 10: Gemini CLI project-scope (.gemini/settings.json)
+ const geminiProjectConfigPath = getGeminiProjectMcpConfigPath();
+ if (geminiProjectConfigPath) {
+ const geminiProjectConfig = readGeminiMcpConfig(geminiProjectConfigPath);
+ if (geminiProjectConfig?.[serverId]) {
+ const serverConfig = normalizeServerConfig(geminiProjectConfig[serverId]);
+ if (serverConfig) {
+ log('INFO', 'Retrieved MCP server configuration from Gemini CLI project scope', {
+ serverId,
+ scope: 'gemini-project',
+ configPath: geminiProjectConfigPath,
+ type: serverConfig.type,
+ });
+ return { ...serverConfig, source: 'gemini' };
+ }
+ }
+ }
+
+ // Server not found in any configuration (Claude, Copilot, Codex, or Gemini)
+ log('WARN', 'MCP server not found in any configuration (Claude, Copilot, Codex, Gemini)', {
serverId,
workspacePath,
});
@@ -550,7 +657,7 @@ export function getMcpServerConfig(
}
/**
- * Get all MCP server IDs from all configuration sources (Claude, Copilot, Codex)
+ * Get all MCP server IDs from all configuration sources (Claude, Copilot, Codex, Gemini)
*
* @param workspacePath - Optional workspace path for project-scoped servers
* @returns Array of unique server IDs
@@ -643,6 +750,29 @@ export function getAllMcpServerIds(workspacePath?: string): string[] {
}
}
+ // =========================================================================
+ // Gemini source
+ // =========================================================================
+
+ // Collect from Gemini CLI user-scope (~/.gemini/settings.json)
+ const geminiUserConfig = readGeminiMcpConfig(getGeminiUserMcpConfigPath());
+ if (geminiUserConfig) {
+ for (const id of Object.keys(geminiUserConfig)) {
+ serverIds.add(id);
+ }
+ }
+
+ // Collect from Gemini CLI project-scope (.gemini/settings.json)
+ const geminiProjectConfigPath = getGeminiProjectMcpConfigPath();
+ if (geminiProjectConfigPath) {
+ const geminiProjectConfig = readGeminiMcpConfig(geminiProjectConfigPath);
+ if (geminiProjectConfig) {
+ for (const id of Object.keys(geminiProjectConfig)) {
+ serverIds.add(id);
+ }
+ }
+ }
+
return Array.from(serverIds);
} catch (error) {
log('ERROR', 'Failed to get MCP server list', {
@@ -673,6 +803,7 @@ export interface McpServerWithSource extends McpServerConfig {
* - VSCode Copilot (.vscode/mcp.json)
* - Copilot CLI (.copilot/mcp-config.json)
* - Codex CLI (~/.codex/config.toml)
+ * - Gemini CLI (~/.gemini/settings.json, .gemini/settings.json)
*
* Priority order (first match wins for duplicate server IDs):
* 1. Project-scope Claude Code (/.mcp.json)
@@ -683,6 +814,8 @@ export interface McpServerWithSource extends McpServerConfig {
* 6. Legacy Claude Code user (~/.claude.json → mcpServers)
* 7. User-scope Copilot CLI (~/.copilot/mcp-config.json)
* 8. User-scope Codex CLI (~/.codex/config.toml)
+ * 9. User-scope Gemini CLI (~/.gemini/settings.json)
+ * 10. Project-scope Gemini CLI (/.gemini/settings.json)
*
* @param workspacePath - Optional workspace path for project-scoped servers
* @returns Array of MCP server configurations with source metadata
@@ -792,11 +925,26 @@ export function getAllMcpServersWithSource(workspacePath?: string): McpServerWit
const codexConfig = readCodexMcpConfig(codexConfigPath);
addServers(codexConfig, 'codex', codexConfigPath);
+ // Priority 8: Gemini CLI user-scope (~/.gemini/settings.json)
+ const geminiUserConfigPath = getGeminiUserMcpConfigPath();
+ const geminiUserConfig = readGeminiMcpConfig(geminiUserConfigPath);
+ addServers(geminiUserConfig, 'gemini', geminiUserConfigPath);
+
+ // Priority 9: Gemini CLI project-scope (.gemini/settings.json)
+ if (workspacePath) {
+ const geminiProjectConfigPath = getGeminiProjectMcpConfigPath();
+ if (geminiProjectConfigPath) {
+ const geminiProjectConfig = readGeminiMcpConfig(geminiProjectConfigPath);
+ addServers(geminiProjectConfig, 'gemini', geminiProjectConfigPath);
+ }
+ }
+
log('INFO', 'Scanned all MCP server sources', {
totalServers: servers.length,
claudeCount: servers.filter((s) => s.source === 'claude').length,
copilotCount: servers.filter((s) => s.source === 'copilot').length,
codexCount: servers.filter((s) => s.source === 'codex').length,
+ geminiCount: servers.filter((s) => s.source === 'gemini').length,
});
return servers;
diff --git a/src/extension/services/mcp-server-config-writer.ts b/src/extension/services/mcp-server-config-writer.ts
index 7a4baee0..e9e2f0f5 100644
--- a/src/extension/services/mcp-server-config-writer.ts
+++ b/src/extension/services/mcp-server-config-writer.ts
@@ -47,6 +47,8 @@ function getConfigPath(target: McpConfigTarget, workspacePath: string): string {
return path.join(os.homedir(), '.copilot', 'mcp-config.json');
case 'codex':
return path.join(os.homedir(), '.codex', 'config.toml');
+ case 'gemini':
+ return path.join(os.homedir(), '.gemini', 'settings.json');
}
}
@@ -110,6 +112,15 @@ export async function writeAgentConfig(
}
config.mcp_servers[SERVER_ENTRY_NAME] = { url: serverUrl };
await writeCodexConfig(config);
+ } else if (target === 'gemini') {
+ // Gemini CLI uses JSON format with "mcpServers" key
+ const filePath = getConfigPath(target, workspacePath);
+ const config = await readJsonConfig(filePath);
+ if (!config.mcpServers) {
+ config.mcpServers = {};
+ }
+ config.mcpServers[SERVER_ENTRY_NAME] = { url: serverUrl };
+ await writeJsonConfig(filePath, config);
} else if (target === 'copilot-chat') {
// VSCode Copilot uses "servers" key with type "http"
const filePath = getConfigPath(target, workspacePath);
@@ -171,6 +182,14 @@ export async function removeAgentConfig(
delete config.mcp_servers[SERVER_ENTRY_NAME];
await writeCodexConfig(config);
}
+ } else if (target === 'gemini') {
+ // Gemini CLI uses JSON format with "mcpServers" key
+ const filePath = getConfigPath(target, workspacePath);
+ const config = await readJsonConfig(filePath);
+ if (config.mcpServers?.[SERVER_ENTRY_NAME]) {
+ delete config.mcpServers[SERVER_ENTRY_NAME];
+ await writeJsonConfig(filePath, config);
+ }
} else if (target === 'copilot-chat') {
const filePath = getConfigPath(target, workspacePath);
const config = await readJsonConfig(filePath);
@@ -234,6 +253,8 @@ export function getConfigTargetsForProvider(provider: AiEditingProvider): McpCon
return ['codex'];
case 'roo-code':
return ['roo-code'];
+ case 'gemini':
+ return ['gemini'];
}
}
@@ -247,6 +268,7 @@ export async function removeAllAgentConfigs(workspacePath: string): Promise {
try {
@@ -106,9 +106,9 @@ export function registerMcpTools(server: McpServer, manager: McpServerManager):
const variant = getSchemaVariantForProvider(manager.getCurrentProvider());
const schemaPath = getDefaultSchemaPath(extensionPath, variant);
- const result = await loadWorkflowSchema(schemaPath, variant);
+ const result = await loadWorkflowSchemaToon(schemaPath, variant);
- if (!result.success || !result.schema) {
+ if (!result.success || !result.schemaString) {
return {
content: [
{
@@ -127,10 +127,7 @@ export function registerMcpTools(server: McpServer, manager: McpServerManager):
content: [
{
type: 'text' as const,
- text: JSON.stringify({
- success: true,
- schema: result.schema,
- }),
+ text: result.schemaString,
},
],
};
diff --git a/src/extension/services/skill-normalization-service.ts b/src/extension/services/skill-normalization-service.ts
index 46d4cc90..5c87139b 100644
--- a/src/extension/services/skill-normalization-service.ts
+++ b/src/extension/services/skill-normalization-service.ts
@@ -32,13 +32,13 @@ const NON_STANDARD_SKILL_PATTERNS = [
'.copilot/skills/', // GitHub Copilot CLI (alternative)
'.codex/skills/', // OpenAI Codex CLI
'.roo/skills/', // Roo Code
- // Future: '.gemini/skills/', '.cursor/skills/', etc.
+ '.gemini/skills/', // Google Gemini CLI
] as const;
/**
* Source type for skill directories
*/
-export type SkillSourceType = 'github' | 'copilot' | 'codex' | 'roo-code' | 'other';
+export type SkillSourceType = 'github' | 'copilot' | 'codex' | 'roo-code' | 'gemini' | 'other';
/**
* Target CLI for workflow execution
@@ -48,7 +48,7 @@ export type SkillSourceType = 'github' | 'copilot' | 'codex' | 'roo-code' | 'oth
* - 'copilot': .claude/skills/, .github/skills/, AND .copilot/skills/ are standard
* - 'codex': .claude/skills/ AND .codex/skills/ are standard
*/
-export type TargetCli = 'claude' | 'copilot' | 'codex' | 'roo-code';
+export type TargetCli = 'claude' | 'copilot' | 'codex' | 'roo-code' | 'gemini';
/**
* Get the list of skill directory patterns that are considered "standard" for a given CLI
@@ -73,6 +73,10 @@ function getStandardSkillPatterns(targetCli: TargetCli): string[] {
// Roo Code considers .roo/skills/ as native
patterns.push('.roo/skills/');
break;
+ case 'gemini':
+ // Gemini CLI considers .gemini/skills/ as native
+ patterns.push('.gemini/skills/');
+ break;
// case 'claude' falls through to default
// Claude Code only uses .claude/skills/
}
@@ -198,6 +202,9 @@ function getSourceType(skillPath: string): SkillSourceType {
if (normalizedPath.includes('.roo/skills/')) {
return 'roo-code';
}
+ if (normalizedPath.includes('.gemini/skills/')) {
+ return 'gemini';
+ }
return 'other';
}
@@ -226,6 +233,8 @@ function _getSourceSkillsDir(sourceType: SkillSourceType): string | null {
return path.join(workspaceRoot, '.codex', 'skills');
case 'roo-code':
return path.join(workspaceRoot, '.roo', 'skills');
+ case 'gemini':
+ return path.join(workspaceRoot, '.gemini', 'skills');
default:
return null;
}
@@ -366,6 +375,9 @@ function getSourceDescription(skills: SkillToNormalize[]): string {
if (sources.has('roo-code')) {
descriptions.push('.roo/skills/');
}
+ if (sources.has('gemini')) {
+ descriptions.push('.gemini/skills/');
+ }
if (sources.has('other')) {
descriptions.push('non-standard directories');
}
diff --git a/src/extension/services/skill-service.ts b/src/extension/services/skill-service.ts
index f4d72729..49ea88e6 100644
--- a/src/extension/services/skill-service.ts
+++ b/src/extension/services/skill-service.ts
@@ -14,6 +14,8 @@ import {
getCodexProjectSkillsDir,
getCodexUserSkillsDir,
getCopilotUserSkillsDir,
+ getGeminiProjectSkillsDir,
+ getGeminiUserSkillsDir,
getGithubSkillsDir,
getInstalledPluginsJsonPath,
getKnownMarketplacesJsonPath,
@@ -44,7 +46,7 @@ import { parseSkillFrontmatter, type SkillMetadata } from './yaml-parser';
export async function scanSkills(
baseDir: string,
scope: 'user' | 'project' | 'local',
- source?: 'claude' | 'copilot' | 'codex' | 'roo'
+ source?: 'claude' | 'copilot' | 'codex' | 'roo' | 'gemini'
): Promise {
const skills: SkillReference[] = [];
@@ -394,22 +396,26 @@ export async function scanAllSkills(): Promise<{
const copilotUserDir = getCopilotUserSkillsDir();
const codexUserDir = getCodexUserSkillsDir();
const rooUserDir = getRooUserSkillsDir();
+ const geminiUserDir = getGeminiUserSkillsDir();
// Project directories
const claudeProjectDir = getProjectSkillsDir();
const githubProjectDir = getGithubSkillsDir();
const codexProjectDir = getCodexProjectSkillsDir();
const rooProjectDir = getRooProjectSkillsDir();
+ const geminiProjectDir = getGeminiProjectSkillsDir();
const [
claudeUserSkills,
copilotUserSkills,
codexUserSkills,
rooUserSkills,
+ geminiUserSkills,
claudeProjectSkills,
githubProjectSkills,
codexProjectSkills,
rooProjectSkills,
+ geminiProjectSkills,
pluginSkills,
] = await Promise.all([
// User-scope scans
@@ -417,11 +423,13 @@ export async function scanAllSkills(): Promise<{
scanSkills(copilotUserDir, 'user', 'copilot'),
scanSkills(codexUserDir, 'user', 'codex'),
scanSkills(rooUserDir, 'user', 'roo'),
+ scanSkills(geminiUserDir, 'user', 'gemini'),
// Project-scope scans
claudeProjectDir ? scanSkills(claudeProjectDir, 'project', 'claude') : Promise.resolve([]),
githubProjectDir ? scanSkills(githubProjectDir, 'project', 'copilot') : Promise.resolve([]),
codexProjectDir ? scanSkills(codexProjectDir, 'project', 'codex') : Promise.resolve([]),
rooProjectDir ? scanSkills(rooProjectDir, 'project', 'roo') : Promise.resolve([]),
+ geminiProjectDir ? scanSkills(geminiProjectDir, 'project', 'gemini') : Promise.resolve([]),
// Plugin skills
scanPluginSkills(),
]);
@@ -432,6 +440,7 @@ export async function scanAllSkills(): Promise<{
...copilotUserSkills,
...codexUserSkills,
...rooUserSkills,
+ ...geminiUserSkills,
];
// Merge project skills: include all sources (no deduplication - show all available skills)
@@ -440,6 +449,7 @@ export async function scanAllSkills(): Promise<{
...githubProjectSkills,
...codexProjectSkills,
...rooProjectSkills,
+ ...geminiProjectSkills,
];
// Separate plugin skills by their scope
diff --git a/src/extension/services/terminal-execution-service.ts b/src/extension/services/terminal-execution-service.ts
index 8c7410c8..43a95032 100644
--- a/src/extension/services/terminal-execution-service.ts
+++ b/src/extension/services/terminal-execution-service.ts
@@ -145,3 +145,45 @@ export function executeCodexCliInTerminal(
terminal,
};
}
+
+/**
+ * Options for executing Gemini CLI skill command
+ */
+export interface GeminiCliExecutionOptions {
+ /** Skill name (the workflow name as .gemini/skills/{name}/SKILL.md) */
+ skillName: string;
+ /** Working directory for the terminal */
+ workingDirectory: string;
+}
+
+/**
+ * Execute Gemini CLI with skill in a new VSCode integrated terminal
+ *
+ * Creates a new terminal and executes:
+ * gemini -i ":skill {skillName}"
+ *
+ * @param options - Gemini CLI execution options
+ * @returns Terminal execution result
+ */
+export function executeGeminiCliInTerminal(
+ options: GeminiCliExecutionOptions
+): TerminalExecutionResult {
+ const terminalName = `Gemini: ${options.skillName}`;
+
+ // Create a new terminal
+ const terminal = vscode.window.createTerminal({
+ name: terminalName,
+ cwd: options.workingDirectory,
+ });
+
+ // Show the terminal and focus on it
+ terminal.show(true);
+
+ // Execute: gemini with :skill prompt to invoke the exported skill
+ terminal.sendText(`gemini -i ":skill ${options.skillName}"`);
+
+ return {
+ terminalName,
+ terminal,
+ };
+}
diff --git a/src/extension/utils/path-utils.ts b/src/extension/utils/path-utils.ts
index 2842e666..8c7be719 100644
--- a/src/extension/utils/path-utils.ts
+++ b/src/extension/utils/path-utils.ts
@@ -151,6 +151,36 @@ export function getRooProjectSkillsDir(): string | null {
return path.join(workspaceRoot, '.roo', 'skills');
}
+/**
+ * Get the Gemini CLI user-scope Skills directory path
+ *
+ * @returns Absolute path to ~/.gemini/skills/
+ *
+ * @example
+ * // Unix: /Users/username/.gemini/skills
+ * // Windows: C:\Users\username\.gemini\skills
+ */
+export function getGeminiUserSkillsDir(): string {
+ return path.join(os.homedir(), '.gemini', 'skills');
+}
+
+/**
+ * Get the Gemini CLI project-scope Skills directory path
+ *
+ * @returns Absolute path to .gemini/skills/ in workspace root, or null if no workspace
+ *
+ * @example
+ * // Unix: /workspace/myproject/.gemini/skills
+ * // Windows: C:\workspace\myproject\.gemini\skills
+ */
+export function getGeminiProjectSkillsDir(): string | null {
+ const workspaceRoot = getWorkspaceRoot();
+ if (!workspaceRoot) {
+ return null;
+ }
+ return path.join(workspaceRoot, '.gemini', 'skills');
+}
+
// =====================================================================
// MCP Configuration Paths
// =====================================================================
@@ -201,6 +231,36 @@ export function getCodexUserMcpConfigPath(): string {
return path.join(os.homedir(), '.codex', 'config.toml');
}
+/**
+ * Get the Gemini CLI user-scope MCP config path (~/.gemini/settings.json)
+ *
+ * @returns Absolute path to user MCP config
+ *
+ * @example
+ * // Unix: /Users/username/.gemini/settings.json
+ * // Windows: C:\Users\username\.gemini\settings.json
+ */
+export function getGeminiUserMcpConfigPath(): string {
+ return path.join(os.homedir(), '.gemini', 'settings.json');
+}
+
+/**
+ * Get the Gemini CLI project-scope MCP config path (.gemini/settings.json)
+ *
+ * @returns Absolute path to project MCP config, or null if no workspace
+ *
+ * @example
+ * // Unix: /workspace/myproject/.gemini/settings.json
+ * // Windows: C:\workspace\myproject\.gemini\settings.json
+ */
+export function getGeminiProjectMcpConfigPath(): string | null {
+ const workspaceRoot = getWorkspaceRoot();
+ if (!workspaceRoot) {
+ return null;
+ }
+ return path.join(workspaceRoot, '.gemini', 'settings.json');
+}
+
/**
* Get the installed plugins JSON path
*
diff --git a/src/shared/types/mcp-node.ts b/src/shared/types/mcp-node.ts
index 6308500d..a67eadb3 100644
--- a/src/shared/types/mcp-node.ts
+++ b/src/shared/types/mcp-node.ts
@@ -9,7 +9,7 @@
/**
* MCP configuration source provider
*/
-export type McpConfigSource = 'claude' | 'copilot' | 'codex';
+export type McpConfigSource = 'claude' | 'copilot' | 'codex' | 'gemini';
/**
* MCP server reference information (from 'claude mcp list')
diff --git a/src/shared/types/messages.ts b/src/shared/types/messages.ts
index 3108b948..13da1b2d 100644
--- a/src/shared/types/messages.ts
+++ b/src/shared/types/messages.ts
@@ -188,9 +188,10 @@ export interface SkillReference {
* - 'copilot': from ~/.copilot/skills/ (user) or .github/skills/ (project)
* - 'codex': from ~/.codex/skills/ (user) or .codex/skills/ (project)
* - 'roo': from ~/.roo/skills/ (user) or .roo/skills/ (project)
+ * - 'gemini': from ~/.gemini/skills/ (user) or .gemini/skills/ (project)
* - undefined: for local scope or legacy data
*/
- source?: 'claude' | 'copilot' | 'codex' | 'roo';
+ source?: 'claude' | 'copilot' | 'codex' | 'roo' | 'gemini';
}
export interface CreateSkillPayload {
@@ -796,6 +797,12 @@ export type ExtensionMessage =
| Message
| Message
| Message
+ | Message
+ | Message
+ | Message
+ | Message
+ | Message
+ | Message
| Message
| Message
| Message
@@ -1412,6 +1419,63 @@ export interface RooCodeOperationFailedPayload {
timestamp: string; // ISO 8601
}
+// ============================================================================
+// Gemini CLI Integration Payloads (Beta)
+// ============================================================================
+
+/**
+ * Export workflow for Gemini CLI payload (Skills format)
+ * Exports to .gemini/skills/{name}/SKILL.md
+ */
+export interface ExportForGeminiCliPayload {
+ /** Workflow to export */
+ workflow: Workflow;
+}
+
+/**
+ * Export for Gemini CLI success payload
+ */
+export interface ExportForGeminiCliSuccessPayload {
+ /** Skill name */
+ skillName: string;
+ /** Skill file path */
+ skillPath: string;
+ /** Timestamp */
+ timestamp: string; // ISO 8601
+}
+
+/**
+ * Run workflow for Gemini CLI payload
+ */
+export interface RunForGeminiCliPayload {
+ /** Workflow to run */
+ workflow: Workflow;
+}
+
+/**
+ * Run for Gemini CLI success payload
+ */
+export interface RunForGeminiCliSuccessPayload {
+ /** Workflow name */
+ workflowName: string;
+ /** Terminal name where command is running */
+ terminalName: string;
+ /** Timestamp */
+ timestamp: string; // ISO 8601
+}
+
+/**
+ * Gemini CLI operation failed payload
+ */
+export interface GeminiOperationFailedPayload {
+ /** Error code */
+ errorCode: 'GEMINI_NOT_INSTALLED' | 'EXPORT_FAILED' | 'UNKNOWN_ERROR';
+ /** Error message */
+ errorMessage: string;
+ /** Timestamp */
+ timestamp: string; // ISO 8601
+}
+
// ============================================================================
// AI Editing Skill Payloads (MCP-based AI editing)
// ============================================================================
@@ -1424,7 +1488,8 @@ export type AiEditingProvider =
| 'copilot-cli'
| 'copilot-vscode'
| 'codex'
- | 'roo-code';
+ | 'roo-code'
+ | 'gemini';
/**
* Run AI editing skill request payload (Webview → Extension)
@@ -1490,7 +1555,13 @@ export interface LaunchAiAgentFailedPayload {
/**
* AI agent config target for MCP server registration
*/
-export type McpConfigTarget = 'claude-code' | 'roo-code' | 'copilot-chat' | 'copilot-cli' | 'codex';
+export type McpConfigTarget =
+ | 'claude-code'
+ | 'roo-code'
+ | 'copilot-chat'
+ | 'copilot-cli'
+ | 'codex'
+ | 'gemini';
/**
* Start MCP Server request payload (Webview → Extension)
@@ -1664,6 +1735,8 @@ export type WebviewMessage =
| Message
| Message
| Message
+ | Message
+ | Message
| Message
| Message
| Message
diff --git a/src/shared/types/workflow-definition.ts b/src/shared/types/workflow-definition.ts
index 443cf1be..c89ed9e2 100644
--- a/src/shared/types/workflow-definition.ts
+++ b/src/shared/types/workflow-definition.ts
@@ -309,8 +309,8 @@ export interface SubAgentFlowNodeData {
export interface McpNodeData {
/** MCP server identifier (from 'claude mcp list') */
serverId: string;
- /** Source provider of the MCP server (claude, copilot, codex) */
- source?: 'claude' | 'copilot' | 'codex';
+ /** Source provider of the MCP server (claude, copilot, codex, gemini) */
+ source?: 'claude' | 'copilot' | 'codex' | 'gemini';
/** Tool function name from the MCP server (optional for aiToolSelection mode) */
toolName?: string;
/** Human-readable description of the tool's functionality (optional for aiToolSelection mode) */
diff --git a/src/webview/package-lock.json b/src/webview/package-lock.json
index fdbda663..0887f235 100644
--- a/src/webview/package-lock.json
+++ b/src/webview/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "cc-wf-studio-webview",
- "version": "3.21.0",
+ "version": "3.22.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "cc-wf-studio-webview",
- "version": "3.21.0",
+ "version": "3.22.0",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@radix-ui/react-collapsible": "^1.1.12",
diff --git a/src/webview/package.json b/src/webview/package.json
index 4f56aa74..58729285 100644
--- a/src/webview/package.json
+++ b/src/webview/package.json
@@ -1,6 +1,6 @@
{
"name": "cc-wf-studio-webview",
- "version": "3.21.0",
+ "version": "3.22.0",
"private": true,
"license": "AGPL-3.0-or-later",
"type": "module",
diff --git a/src/webview/src/components/Toolbar.tsx b/src/webview/src/components/Toolbar.tsx
index ab3718d9..e6530d6a 100644
--- a/src/webview/src/components/Toolbar.tsx
+++ b/src/webview/src/components/Toolbar.tsx
@@ -19,11 +19,13 @@ import {
exportForCodexCli,
exportForCopilot,
exportForCopilotCli,
+ exportForGeminiCli,
exportForRooCode,
runAsSlashCommand,
runForCodexCli,
runForCopilot,
runForCopilotCli,
+ runForGeminiCli,
runForRooCode,
saveWorkflow,
} from '../services/vscode-bridge';
@@ -102,6 +104,8 @@ export const Toolbar: React.FC = ({
toggleCodexEnabled,
isRooCodeEnabled,
toggleRooCodeEnabled,
+ isGeminiEnabled,
+ toggleGeminiEnabled,
} = useRefinementStore();
const [isSaving, setIsSaving] = useState(false);
const [isExporting, setIsExporting] = useState(false);
@@ -119,6 +123,9 @@ export const Toolbar: React.FC = ({
// Roo Code integration (Beta)
const [isRooCodeExporting, setIsRooCodeExporting] = useState(false);
const [isRooCodeRunning, setIsRooCodeRunning] = useState(false);
+ // Gemini CLI integration (Beta)
+ const [isGeminiExporting, setIsGeminiExporting] = useState(false);
+ const [isGeminiRunning, setIsGeminiRunning] = useState(false);
// Copilot Beta feature toggle is now managed by refinement-store
// Copilot execution mode (persisted in localStorage, default: 'cli')
const [copilotExecutionMode, setCopilotExecutionMode] = useState(() => {
@@ -723,6 +730,104 @@ export const Toolbar: React.FC = ({
}
};
+ // ============================================================================
+ // Gemini CLI Integration Handlers (Beta)
+ // ============================================================================
+
+ const handleGeminiExport = async () => {
+ if (!workflowName.trim()) {
+ onError({
+ code: 'VALIDATION_ERROR',
+ message: t('toolbar.error.workflowNameRequiredForExport'),
+ });
+ return;
+ }
+
+ if (!WORKFLOW_NAME_PATTERN.test(workflowName)) {
+ onError({
+ code: 'VALIDATION_ERROR',
+ message: t('toolbar.error.workflowNameInvalid'),
+ });
+ return;
+ }
+
+ setIsGeminiExporting(true);
+ try {
+ const { subAgentFlows, workflowDescription, slashCommandOptions } =
+ useWorkflowStore.getState();
+
+ const workflow = serializeWorkflow(
+ nodes,
+ edges,
+ workflowName,
+ workflowDescription || undefined,
+ undefined,
+ subAgentFlows,
+ slashCommandOptions
+ );
+
+ validateWorkflow(workflow);
+
+ const result = await exportForGeminiCli(workflow);
+ console.log('Workflow exported as skill for Gemini CLI:', result.skillPath);
+ } catch (error) {
+ onError({
+ code: 'EXPORT_FAILED',
+ message: error instanceof Error ? error.message : 'Failed to export for Gemini CLI',
+ details: error,
+ });
+ } finally {
+ setIsGeminiExporting(false);
+ }
+ };
+
+ const handleGeminiRun = async () => {
+ if (!workflowName.trim()) {
+ onError({
+ code: 'VALIDATION_ERROR',
+ message: t('toolbar.error.workflowNameRequiredForExport'),
+ });
+ return;
+ }
+
+ if (!WORKFLOW_NAME_PATTERN.test(workflowName)) {
+ onError({
+ code: 'VALIDATION_ERROR',
+ message: t('toolbar.error.workflowNameInvalid'),
+ });
+ return;
+ }
+
+ setIsGeminiRunning(true);
+ try {
+ const { subAgentFlows, workflowDescription, slashCommandOptions } =
+ useWorkflowStore.getState();
+
+ const workflow = serializeWorkflow(
+ nodes,
+ edges,
+ workflowName,
+ workflowDescription || undefined,
+ undefined,
+ subAgentFlows,
+ slashCommandOptions
+ );
+
+ validateWorkflow(workflow);
+
+ const result = await runForGeminiCli(workflow);
+ console.log('Workflow run for Gemini CLI:', result.workflowName);
+ } catch (error) {
+ onError({
+ code: 'RUN_FAILED',
+ message: error instanceof Error ? error.message : 'Failed to run for Gemini CLI',
+ details: error,
+ });
+ } finally {
+ setIsGeminiRunning(false);
+ }
+ };
+
// Handle AI workflow name generation
const handleGenerateWorkflowName = useCallback(async () => {
const currentRequestId = `gen-name-${Date.now()}`;
@@ -973,7 +1078,7 @@ export const Toolbar: React.FC = ({
/>
{/* Slash Command Section - Layout changes based on Copilot/Codex Beta enabled */}
- {isCopilotEnabled || isCodexEnabled || isRooCodeEnabled ? (
+ {isCopilotEnabled || isCodexEnabled || isRooCodeEnabled || isGeminiEnabled ? (
/* Combined layout when Copilot Beta is enabled */
= ({
)}
+
+ {/* Vertical Divider - shown when Gemini CLI is enabled */}
+ {isGeminiEnabled && (
+
+ )}
+
+ {/* Gemini CLI Column - shown when Gemini CLI is enabled */}
+ {isGeminiEnabled && (
+
+
+ Gemini CLI
+
+
+
+
+
+
+
+
+ )}
) : (
@@ -1559,6 +1756,8 @@ export const Toolbar: React.FC = ({
onToggleCodexBeta={toggleCodexEnabled}
isRooCodeEnabled={isRooCodeEnabled}
onToggleRooCodeBeta={toggleRooCodeEnabled}
+ isGeminiEnabled={isGeminiEnabled}
+ onToggleGeminiBeta={toggleGeminiEnabled}
open={moreActionsOpen}
onOpenChange={onMoreActionsOpenChange}
/>
diff --git a/src/webview/src/components/chat/McpServerSection.tsx b/src/webview/src/components/chat/McpServerSection.tsx
index 7bfc0acc..b5e46a16 100644
--- a/src/webview/src/components/chat/McpServerSection.tsx
+++ b/src/webview/src/components/chat/McpServerSection.tsx
@@ -30,6 +30,7 @@ const AI_EDIT_BUTTONS: AiEditButton[] = [
{ provider: 'copilot-vscode', label: 'VSCode Copilot' },
{ provider: 'codex', label: 'Codex CLI' },
{ provider: 'roo-code', label: 'Roo Code' },
+ { provider: 'gemini', label: 'Gemini CLI' },
];
interface McpServerSectionProps {
@@ -43,7 +44,8 @@ export function McpServerSection({ isCollapsed, onToggleCollapse }: McpServerSec
const [port, setPort] = useState(null);
const [launchingProvider, setLaunchingProvider] = useState(null);
- const { isCopilotEnabled, isCodexEnabled, isRooCodeEnabled } = useRefinementStore();
+ const { isCopilotEnabled, isCodexEnabled, isRooCodeEnabled, isGeminiEnabled } =
+ useRefinementStore();
const visibleButtons = useMemo(() => {
return AI_EDIT_BUTTONS.filter((button) => {
@@ -57,11 +59,13 @@ export function McpServerSection({ isCollapsed, onToggleCollapse }: McpServerSec
return isCodexEnabled;
case 'roo-code':
return isRooCodeEnabled;
+ case 'gemini':
+ return isGeminiEnabled;
default:
return false;
}
});
- }, [isCopilotEnabled, isCodexEnabled, isRooCodeEnabled]);
+ }, [isCopilotEnabled, isCodexEnabled, isRooCodeEnabled, isGeminiEnabled]);
// Listen for MCP server status updates
useEffect(() => {
diff --git a/src/webview/src/components/common/AIProviderBadge.tsx b/src/webview/src/components/common/AIProviderBadge.tsx
index 4d28e467..7a07b459 100644
--- a/src/webview/src/components/common/AIProviderBadge.tsx
+++ b/src/webview/src/components/common/AIProviderBadge.tsx
@@ -13,7 +13,7 @@ import { useVSCodeTheme } from '../../hooks/useVSCodeTheme';
* Supported AI provider types
* Add new providers here as needed
*/
-export type AIProviderType = 'copilot' | 'claude' | 'codex' | 'roo';
+export type AIProviderType = 'copilot' | 'claude' | 'codex' | 'roo' | 'gemini';
/**
* Configuration for each AI provider
@@ -58,6 +58,13 @@ const PROVIDER_CONFIG: Record<
dark: '#FFFFFF', // White text (dark theme)
},
},
+ gemini: {
+ label: 'Gemini',
+ colors: {
+ light: '#4285F4', // Google Blue
+ dark: '#1A73E8', // Darker Google Blue
+ },
+ },
};
/**
diff --git a/src/webview/src/components/dialogs/SkillBrowserDialog.tsx b/src/webview/src/components/dialogs/SkillBrowserDialog.tsx
index f94d5b3c..fa3953d4 100644
--- a/src/webview/src/components/dialogs/SkillBrowserDialog.tsx
+++ b/src/webview/src/components/dialogs/SkillBrowserDialog.tsx
@@ -17,7 +17,7 @@ import { useWorkflowStore } from '../../stores/workflow-store';
import { AIProviderBadge, type AIProviderType } from '../common/AIProviderBadge';
import { type CreateSkillFormData, SkillCreationDialog } from './SkillCreationDialog';
-type SourceType = 'claude' | 'copilot' | 'codex' | 'roo';
+type SourceType = 'claude' | 'copilot' | 'codex' | 'roo' | 'gemini';
interface GroupedSkills {
source: SourceType;
@@ -29,7 +29,7 @@ interface GroupedSkills {
* Skills without a source are treated as 'claude' for backward compatibility.
*/
function groupSkillsBySource(skills: SkillReference[]): GroupedSkills[] {
- const sourceOrder: SourceType[] = ['claude', 'copilot', 'codex', 'roo'];
+ const sourceOrder: SourceType[] = ['claude', 'copilot', 'codex', 'roo', 'gemini'];
const groups = new Map();
// Initialize all groups
diff --git a/src/webview/src/components/mcp/McpServerList.tsx b/src/webview/src/components/mcp/McpServerList.tsx
index 99a4a6da..5abe9bd5 100644
--- a/src/webview/src/components/mcp/McpServerList.tsx
+++ b/src/webview/src/components/mcp/McpServerList.tsx
@@ -15,7 +15,7 @@ import { listMcpServers, refreshMcpCache } from '../../services/mcp-service';
import { AIProviderBadge, type AIProviderType } from '../common/AIProviderBadge';
import { IndeterminateProgressBar } from '../common/IndeterminateProgressBar';
-type SourceType = 'claude' | 'copilot' | 'codex';
+type SourceType = 'claude' | 'copilot' | 'codex' | 'gemini';
interface GroupedServers {
source: SourceType;
@@ -27,7 +27,7 @@ interface GroupedServers {
* Servers without a source are treated as 'claude' for backward compatibility.
*/
function groupServersBySource(servers: McpServerReference[]): GroupedServers[] {
- const sourceOrder: SourceType[] = ['claude', 'copilot', 'codex'];
+ const sourceOrder: SourceType[] = ['claude', 'copilot', 'codex', 'gemini'];
const groups = new Map();
// Initialize all groups
diff --git a/src/webview/src/components/toolbar/MoreActionsDropdown.tsx b/src/webview/src/components/toolbar/MoreActionsDropdown.tsx
index 67d8c9e2..a9973ff6 100644
--- a/src/webview/src/components/toolbar/MoreActionsDropdown.tsx
+++ b/src/webview/src/components/toolbar/MoreActionsDropdown.tsx
@@ -39,6 +39,8 @@ interface MoreActionsDropdownProps {
onToggleCodexBeta: () => void;
isRooCodeEnabled: boolean;
onToggleRooCodeBeta: () => void;
+ isGeminiEnabled: boolean;
+ onToggleGeminiBeta: () => void;
open?: boolean;
onOpenChange?: (open: boolean) => void;
}
@@ -55,6 +57,8 @@ export function MoreActionsDropdown({
onToggleCodexBeta,
isRooCodeEnabled,
onToggleRooCodeBeta,
+ isGeminiEnabled,
+ onToggleGeminiBeta,
open,
onOpenChange,
}: MoreActionsDropdownProps) {
@@ -227,6 +231,29 @@ export function MoreActionsDropdown({
{isRooCodeEnabled && }
+ {/* Gemini CLI Beta Toggle */}
+
+
+
+ Gemini CLI
+
+
+ {isGeminiEnabled && }
+
+
{
+ return new Promise((resolve, reject) => {
+ const requestId = `req-${Date.now()}-${Math.random()}`;
+
+ const handler = (event: MessageEvent) => {
+ const message: ExtensionMessage = event.data;
+
+ if (message.requestId === requestId) {
+ window.removeEventListener('message', handler);
+
+ if (message.type === 'EXPORT_FOR_GEMINI_CLI_SUCCESS') {
+ resolve(message.payload as ExportForGeminiCliSuccessPayload);
+ } else if (message.type === 'EXPORT_FOR_GEMINI_CLI_CANCELLED') {
+ // User cancelled - resolve with empty result
+ resolve({
+ skillName: '',
+ skillPath: '',
+ timestamp: new Date().toISOString(),
+ });
+ } else if (message.type === 'EXPORT_FOR_GEMINI_CLI_FAILED') {
+ reject(new Error(message.payload?.errorMessage || 'Failed to export for Gemini CLI'));
+ }
+ }
+ };
+
+ window.addEventListener('message', handler);
+
+ const payload: ExportForGeminiCliPayload = { workflow };
+ vscode.postMessage({
+ type: 'EXPORT_FOR_GEMINI_CLI',
+ requestId,
+ payload,
+ });
+
+ // Timeout after 30 seconds
+ setTimeout(() => {
+ window.removeEventListener('message', handler);
+ reject(new Error('Request timed out'));
+ }, 30000);
+ });
+}
+
+/**
+ * Run workflow for Gemini CLI (Beta)
+ *
+ * Exports the workflow to Gemini Skills format and runs it via
+ * Gemini CLI terminal
+ *
+ * @param workflow - Workflow to run
+ * @returns Promise that resolves with run result
+ */
+export function runForGeminiCli(workflow: Workflow): Promise {
+ return new Promise((resolve, reject) => {
+ const requestId = `req-${Date.now()}-${Math.random()}`;
+
+ const handler = (event: MessageEvent) => {
+ const message: ExtensionMessage = event.data;
+
+ if (message.requestId === requestId) {
+ window.removeEventListener('message', handler);
+
+ if (message.type === 'RUN_FOR_GEMINI_CLI_SUCCESS') {
+ resolve(message.payload as RunForGeminiCliSuccessPayload);
+ } else if (message.type === 'RUN_FOR_GEMINI_CLI_CANCELLED') {
+ // User cancelled - resolve with empty result
+ resolve({
+ workflowName: '',
+ terminalName: '',
+ timestamp: new Date().toISOString(),
+ });
+ } else if (message.type === 'RUN_FOR_GEMINI_CLI_FAILED') {
+ reject(new Error(message.payload?.errorMessage || 'Failed to run for Gemini CLI'));
+ }
+ }
+ };
+
+ window.addEventListener('message', handler);
+
+ const payload: RunForGeminiCliPayload = { workflow };
+ vscode.postMessage({
+ type: 'RUN_FOR_GEMINI_CLI',
+ requestId,
+ payload,
+ });
+
+ // Timeout after 30 seconds
+ setTimeout(() => {
+ window.removeEventListener('message', handler);
+ reject(new Error('Request timed out'));
+ }, 30000);
+ });
+}
+
// ============================================================================
// One-Click AI Agent Launch
// ============================================================================
diff --git a/src/webview/src/stores/refinement-store.ts b/src/webview/src/stores/refinement-store.ts
index 064037b1..e520ec50 100644
--- a/src/webview/src/stores/refinement-store.ts
+++ b/src/webview/src/stores/refinement-store.ts
@@ -30,6 +30,8 @@ const COPILOT_ENABLED_STORAGE_KEY = 'cc-wf-studio:copilot-beta-enabled';
const CODEX_ENABLED_STORAGE_KEY = 'cc-wf-studio:codex-beta-enabled';
// Note: This key is shared with Toolbar.tsx for the "Roo Code (Beta)" toggle
const ROO_CODE_ENABLED_STORAGE_KEY = 'cc-wf-studio:roo-code-beta-enabled';
+// Note: This key is shared with Toolbar.tsx for the "Gemini CLI (Beta)" toggle
+const GEMINI_ENABLED_STORAGE_KEY = 'cc-wf-studio:gemini-beta-enabled';
// Available tools for Claude Code CLI (used in AI editing allowed tools)
export const AVAILABLE_TOOLS = [
@@ -337,6 +339,29 @@ function saveRooCodeEnabledToStorage(enabled: boolean): void {
}
}
+/**
+ * Load Gemini enabled state from localStorage
+ */
+function loadGeminiEnabledFromStorage(): boolean {
+ try {
+ const saved = localStorage.getItem(GEMINI_ENABLED_STORAGE_KEY);
+ return saved === 'true';
+ } catch {
+ return false;
+ }
+}
+
+/**
+ * Save Gemini enabled state to localStorage
+ */
+function saveGeminiEnabledToStorage(enabled: boolean): void {
+ try {
+ localStorage.setItem(GEMINI_ENABLED_STORAGE_KEY, String(enabled));
+ } catch {
+ // localStorage may not be available in some contexts
+ }
+}
+
// ============================================================================
// Session Status Type
// ============================================================================
@@ -372,6 +397,7 @@ interface RefinementStore {
isCopilotEnabled: boolean;
isCodexEnabled: boolean;
isRooCodeEnabled: boolean;
+ isGeminiEnabled: boolean;
// Dynamic Copilot Models State
availableCopilotModels: CopilotModelInfo[];
@@ -403,6 +429,7 @@ interface RefinementStore {
toggleCopilotEnabled: () => void;
toggleCodexEnabled: () => void;
toggleRooCodeEnabled: () => void;
+ toggleGeminiEnabled: () => void;
fetchCopilotModels: () => Promise;
initConversation: () => void;
loadConversationHistory: (history: ConversationHistory | undefined) => void;
@@ -499,6 +526,7 @@ export const useRefinementStore = create((set, get) => ({
isCopilotEnabled: loadCopilotEnabledFromStorage(), // Load from localStorage, default: false
isCodexEnabled: loadCodexEnabledFromStorage(), // Load from localStorage, default: false
isRooCodeEnabled: loadRooCodeEnabledFromStorage(), // Load from localStorage, default: false
+ isGeminiEnabled: loadGeminiEnabledFromStorage(), // Load from localStorage, default: false
// Dynamic Copilot Models Initial State
availableCopilotModels: [],
@@ -616,6 +644,13 @@ export const useRefinementStore = create((set, get) => ({
set({ isRooCodeEnabled: newEnabled });
},
+ toggleGeminiEnabled: () => {
+ const currentEnabled = get().isGeminiEnabled;
+ const newEnabled = !currentEnabled;
+ saveGeminiEnabledToStorage(newEnabled);
+ set({ isGeminiEnabled: newEnabled });
+ },
+
fetchCopilotModels: async () => {
// Avoid fetching if already in progress
if (get().isFetchingCopilotModels) {