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) {