Compare commits

...

17 Commits

Author SHA1 Message Date
Arafatkatze 5d77a9a787 Send to cline provider 2026-01-27 18:28:13 -08:00
cline-test 92ebfce0cb feat(settings): Support linking to recommended or free model picker. 2026-01-27 18:01:44 -08:00
cline-test 8a5042a8bc release(3.55.0): Version bump and update WhatsNewModal 2026-01-27 17:31:11 -08:00
github-actions 726853d679 Updating CHANGELOG.md format 2026-01-27 23:52:40 +00:00
github-actions[bot] 0f52d80e55 changeset version bump 2026-01-27 23:52:38 +00:00
Ara 71af56f493 feat: add Arcee AI Trinity Large Preview to free models (#8897)
Add arcee-ai/trinity-large-preview:free as a new free model option:
- Add to onboarding models with 131k context window and score of 88
- Include in OpenRouterModelPicker free models list
- Update filter to preserve Trinity Large models like Minimax models
2026-01-27 15:51:39 -08:00
Juan Pablo Flores 1167b4f3a6 feat(rules): Conditional rules docs (#8874)
* docs(rules): Initial thoughts on docs for conditional rules.

* docs: restructure Cline Rules documentation into nested structure

Reorganize Cline Rules documentation by:
- Creating a "Cline Rules" group with overview and conditional-rules pages
- Moving conditional-rules.mdx into features/cline-rules/ subdirectory
- Adding URL redirects for backward compatibility
- Streamlining conditional-rules content for clarity and conciseness
- Adding cross-reference link to the overview page

This improves documentation navigation by grouping related rule concepts together and makes the content more accessible with clearer, more concise explanations.

* docs(cline-rules): consolidate rule file format documentation

Reorganize and expand the documentation for supported rule file formats:

- Add new "Supported Rule Files" section with comprehensive table
- Document cross-tool compatibility (Cursor, Windsurf, AGENTS.md)
- Clarify file priority and loading behavior
- Remove separate AGENTS.md section and integrate into unified table

This improves discoverability by showing all supported formats in one
place and makes it clearer how Cline works with rules from different AI
coding tools.

* docs(rules): remove context management note from overview

---------

Co-authored-by: cline-test <132302818+candieduniverse@users.noreply.github.com>
2026-01-27 14:17:29 -08:00
WangXiaolong e8d6370b0c feat(deepseek): add native tool calling support and reasoning_content handling (#7888)
* feat(deepseek): add native tool calling support and reasoning_content passback

- Add DeepSeek to isNextGenModelProvider list to enable native tool calling
- Add isDeepSeekModelFamily function for model identification
- Add addReasoningContent function for DeepSeek Reasoner's reasoning_content field
  - Pass back reasoning_content during tool calling within the same turn
  - Clear reasoning_content when starting a new conversation turn
- Compliant with DeepSeek API documentation for thinking mode with tool calling

* Update src/core/api/transform/r1-format.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update comments for user message handling logic

Clarify reasoning for handling user messages in comments.

* Update src/core/api/transform/r1-format.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix: format code for consistency in isNextGenModelFamily function

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Ara <arafat.da.khan@gmail.com>
2026-01-27 13:24:20 -08:00
Toby White e243376a39 feat: add MCP prompts support (#8066)
* feat: add MCP prompts support

Implement support for MCP prompts as defined in the MCP spec (2025-06-18):
- Add McpPrompt and McpPromptArgument types to shared types
- Update proto definitions with prompt messages
- Update McpHub to fetch prompts list and get individual prompts
- Add prompts to system prompt component for AI awareness
- Add McpPromptRow UI component for displaying prompts
- Update ServerRow with Prompts tab showing available prompts
- Add slash command integration (/mcp:<server>:<prompt>)
- Update regex patterns to support colons in command names

MCP prompts are user-controlled templates that can be invoked via
slash commands to inject contextual messages into the conversation.

* style: alphabetize imports in mcp-server-conversion.ts

Reorder imports to follow project convention of alphabetical ordering.

* feat: add MCP prompts to slash command autocomplete

Wire up mcpServers to SlashCommandMenu so MCP prompt commands appear
in the autocomplete dropdown with their own "MCP Prompts" section.

* test: add unit tests for MCP prompt slash commands

- Add webview slash-commands.test.ts testing getMcpPromptCommands,
  getMatchingSlashCommands, and validateSlashCommand with MCP servers
- Add backend slash-commands tests for formatMcpPromptResponse and
  parseSlashCommands MCP handling
- Export formatMcpPromptResponse for testability
- Add "mcp_prompt" to telemetry captureSlashCommandUsed types

* test: update snapshots and fix backend tests for MCP prompts

- Update system prompt snapshots to include MCP prompts section
- Remove backend tests requiring StateManager initialization
  (tests for unknown server, no fetcher, fetcher errors)
- Core MCP prompt functionality is covered by remaining tests

* fix: change test status to valid 'connecting' value

* chore: remove commented debug line from prompts fetching

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: use Logger instead of console.error for lint compliance

* fix: wire up mcpPromptFetcher callback to parseSlashCommands

The MCP prompt slash commands were not working because the
mcpPromptFetcher callback was never passed to parseSlashCommands.
This adds the callback that wraps mcpHub.getPrompt() to actually
fetch and inject prompt content when using /mcp:server:prompt.

* fix: resolve MCP prompts keyboard navigation and edge cases

- Add mcpServers param to keyboard handler's getMatchingSlashCommands calls
  to fix arrow key navigation and Enter/Tab selection for MCP prompts
- Add null check for connection.client in McpHub.getPrompt()
- Add debug logging when MCP prompt fetch returns null
- Fix regex in shouldShowSlashCommandsMenu to include colons for MCP format

* chore: add changeset for MCP prompts feature

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Robin Newhouse <robin@cline.bot>
2026-01-27 13:12:02 -08:00
Tomás Barreiro 4f1be9d512 Replace POSTHOG_TELEMETRY_ENABLED with CLINE_TELEMETRY_DISABLED (#8818)
* Replace `POSTHOG_TELEMETRY_ENABLED` with `CLINE_TELEMETRY_DISABLED`

* Update cli/pkg/hostbridge/env.go

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-27 18:17:06 +01:00
Ara 94d36ce719 feat(ui): reduce font size for thinking content (#8892)
Add text-xs class to reasoning content in ThinkingRow component
for improved visual hierarchy and readability.
2026-01-27 08:28:39 -08:00
Bee 5df7498f03 refactor: simplify ThinkingRow expansion state management (#8735)
* refactor: simplify ThinkingRow expansion state management

Remove the responseStarted prop and complex logic that conditionally controlled ThinkingRow visibility during streaming. Simplify to allow ThinkingRow to remain expandable throughout the entire streaming lifecycle instead of forcing it expanded during reasoning and then collapsing after response starts.

Changes:
- Remove ApiReqState type and responseStarted tracking
- Eliminate showStreamingThinking and showCollapsedThinking logic
- Use consistent isExpanded state based only on user toggle
- Always show ThinkingRow title

* remove unused responseStarted

* feat(ui): update thinking UI with improved expand/collapse controls

Changes:
- Replace "Thinking..." with "Working..." status text in non-plan mode
- Switch from ChevronRight to ChevronUp/Down icons for better UX
- Redesign thinking section header with cleaner layout
- Remove preview text when collapsed, show only "Thinking" label
- Add consistent border styling to thinking content
- Implement per-tool thinking expand/collapse state management
- Update icon sizing and styling for better visual consistency

This improves the user experience by making the thinking/reasoning sections more intuitive to expand and collapse, with clearer visual indicators and a more polished appearance.

* add blur

* feat: chevron fix, reasoning change, slight style change

* feat: spacing issues

* keep thinking row expanded during stream

* Reasoning -> Thoughts

* feat: Inline reading of files vs having reading then read list items seperately

* feat: remove extra reading state

* feat: removed reasoning from file expandable file state

---------

Co-authored-by: Jose R. Perez <trupix@gmail.com>
2026-01-27 07:53:03 -08:00
Juan Pablo Flores c2b87252ac Fixes Cannot restore checkpoint more than once #8866 (#8873) 2026-01-27 07:46:43 -08:00
github-actions[bot] be353bb3da v3.54.0 Release Notes (#8840)
- Native tool calls support for Ollama provider
- Sonnet 4.5 is now the default Amazon Bedrock model id

- Prevent infinite retry loops when replace_in_file fails repeatedly. The system now detects repeated failures and provides better guidance to break out of retry cycles.
- Skip diff error UI handling during streaming to prevent flickering. Error handling is deferred until streaming completes.
- Strip notebook cell outputs when extracting text content from Jupyter notebooks, significantly reducing context size sent to the LLM.
- Throttle diff view updates during streaming to reduce UI flickering and improve performance.

- Removed Devstral-2512 free from the free models list
- Removed deprecated zai-glm-4.6 model from Cerebras provider

Co-authored-by: Arafatkatze <arafat.da.khan@gmail.com>
2026-01-27 07:44:49 -08:00
Ara 0a7791de1f feat: remove Mistral Devstral 2512 from free models list (#8889)
Remove mistralai/devstral-2512:free from:
- Onboarding models configuration
- Free models picker in settings
- OpenRouter model filter exception list

The Devstral model is no longer included as a free tier option.
2026-01-27 07:16:55 -08:00
Robin Newhouse 7cca102e14 fix: apply_patch tool now works with OCA provider's gpt5 model ID (#8875) 2026-01-26 22:54:21 -08:00
Bee 4f591de6a9 feat: Adds support for native tool calls to Ollama provider (#8871)
* feat: add support for tool calls in Ollama API

Enhanced OllamaHandler to support tool calls by adding a 'tools' parameter to createMessage. Implements processing of tool call deltas using ToolCallProcessor, enabling handling of function calls made by the model. Added necessary imports for ChatCompletionTool and ToolCallProcessor types.

* add changeset
2026-01-26 17:50:52 -08:00
78 changed files with 2167 additions and 473 deletions
-7
View File
@@ -1,7 +0,0 @@
---
"claude-dev": patch
---
Add endpoint configuration file support for on-premise deployments
Enterprise customers can now configure custom API endpoints by creating a `~/.cline/endpoints.json` file with custom URLs for `appBaseUrl`, `apiBaseUrl`, and `mcpBaseUrl`. When this file is present, Cline runs in on-premise mode with the custom endpoints.
@@ -1,7 +0,0 @@
---
"claude-dev": patch
---
fix: prevent infinite retry loops when replace_in_file fails repeatedly
Add safeguards to prevent the LLM from getting stuck in infinite retry loops when `replace_in_file` operations fail repeatedly. The system now detects repeated failures and provides better guidance to break out of retry cycles.
@@ -1,7 +0,0 @@
---
"claude-dev": patch
---
fix: skip diff error UI handling during streaming to prevent flickering
Suppress diff view error notifications while content is actively streaming to prevent visual flickering and improve user experience. Error handling is deferred until streaming completes.
-7
View File
@@ -1,7 +0,0 @@
---
"claude-dev": patch
---
fix(extract-text): strip notebook outputs to reduce context size
Strip notebook cell outputs when extracting text content from Jupyter notebooks, significantly reducing the amount of context sent to the LLM while preserving the essential code and markdown content.
-7
View File
@@ -1,7 +0,0 @@
---
"claude-dev": patch
---
fix: throttle diff view updates during streaming
Add throttling to diff view updates during content streaming to reduce UI flickering and improve performance. Updates are now batched at reasonable intervals instead of firing on every token received.
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Disable PostHog telemetry, error tracking, and feature flags in self-hosted mode
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Remove deprecated zai-glm-4.6 model from Cerebras provider
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Make Sonnet 4.5 the default Amazon Bedrock model id
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Disable PostHog and build-time OpenTelemetry telemetry in self-hosted/on-premise mode. Enterprise customers running self-hosted deployments will no longer send any telemetry to Cline's collectors. Runtime environment OTEL and remote config OTEL remain available for enterprises to configure their own telemetry collection.
-7
View File
@@ -16,13 +16,6 @@
TELEMETRY_SERVICE_API_KEY=your-posthog-telemetry-api-key
ERROR_SERVICE_API_KEY=your-posthog-error-tracking-api-key
# ============================================================================
# TELEMETRY PROVIDER CONTROL
# ============================================================================
# Control which telemetry providers are active
POSTHOG_TELEMETRY_ENABLED=true # Enable PostHog telemetry (default: true)
# Set to false to disable Telemetry completely
# ============================================================================
# OPENTELEMETRY (Optional - for advanced telemetry)
# ============================================================================
-1
View File
@@ -97,7 +97,6 @@ jobs:
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
POSTHOG_TELEMETRY_ENABLED: "true"
run: npm run compile-standalone-npm
- name: Generate Protos (Second Pass - Bug Workaround)
-1
View File
@@ -138,7 +138,6 @@ jobs:
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
POSTHOG_TELEMETRY_ENABLED: "true"
run: npm run compile-standalone-npm
- name: Generate Protos (Second Pass - Bug Workaround)
+29 -2
View File
@@ -1,14 +1,41 @@
# Changelog
## [3.55.0]
- Add new model: Arcee Trinity Large Preview
- Add new model: Moonshot Kimi K2.5
- Add MCP prompts support - prompts from connected MCP servers now appear in slash command autocomplete as `/mcp:<server>:<prompt>`
## [3.54.0]
### Added
- Native tool calls support for Ollama provider
- Sonnet 4.5 is now the default Amazon Bedrock model id
### Fixed
- Prevent infinite retry loops when replace_in_file fails repeatedly. The system now detects repeated failures and provides better guidance to break out of retry cycles.
- Skip diff error UI handling during streaming to prevent flickering. Error handling is deferred until streaming completes.
- Strip notebook cell outputs when extracting text content from Jupyter notebooks, significantly reducing context size sent to the LLM.
- Throttle diff view updates during streaming to reduce UI flickering and improve performance.
### Changed
- Removed Mistral's Devstral-2512 free from the free models list
- Removed deprecated zai-glm-4.6 model from Cerebras provider
## [3.53.1]
### Fixed
- Bug in responses API
- Bug in responses API
## [3.53.0]
### Fixed
- Removed grok model from free tier
- Removed grok model from free tier
## [3.52.0]
+7 -4
View File
@@ -104,14 +104,18 @@ func (s *EnvService) Shutdown(ctx context.Context, req *cline.EmptyRequest) (*cl
return &cline.Empty{}, nil
}
func (s *EnvService) isTelemetryEnabled() bool {
// In CLI mode, check the CLINE_TELEMETRY_DISABLED environment variable
return os.Getenv("CLINE_TELEMETRY_DISABLED") != "true"
}
// GetTelemetrySettings returns the telemetry settings for CLI mode
func (s *EnvService) GetTelemetrySettings(ctx context.Context, req *cline.EmptyRequest) (*host.GetTelemetrySettingsResponse, error) {
if s.verbose {
log.Printf("GetTelemetrySettings called")
}
// In CLI mode, check the POSTHOG_TELEMETRY_ENABLED environment variable
telemetryEnabled := os.Getenv("POSTHOG_TELEMETRY_ENABLED") == "true"
telemetryEnabled := s.isTelemetryEnabled()
var setting host.Setting
if telemetryEnabled {
@@ -133,8 +137,7 @@ func (s *EnvService) SubscribeToTelemetrySettings(req *cline.EmptyRequest, strea
log.Printf("SubscribeToTelemetrySettings called")
}
// Send initial telemetry state
telemetryEnabled := os.Getenv("POSTHOG_TELEMETRY_ENABLED") == "true"
telemetryEnabled := s.isTelemetryEnabled()
var setting host.Setting
if telemetryEnabled {
+15 -1
View File
@@ -117,7 +117,13 @@
"features/auto-compact",
"features/background-edit",
"features/checkpoints",
"features/cline-rules",
{
"group": "Cline Rules",
"pages": [
"features/cline-rules/overview",
"features/cline-rules/conditional-rules"
]
},
{
"group": "Commands & Shortcuts",
"pages": [
@@ -424,6 +430,14 @@
{
"source": "/enterprise-solutions/team-management/roles-and-permissions",
"destination": "/enterprise-solutions/team-management/managing-members"
},
{
"source": "/features/cline-rules",
"destination": "/features/cline-rules/overview"
},
{
"source": "/features/conditional-rules",
"destination": "/features/cline-rules/conditional-rules"
}
],
"search": {
-188
View File
@@ -1,188 +0,0 @@
Cline Rules allow you to provide Cline with system-level guidance. Think of them as a persistent way to include context and preferences for your projects or globally for every conversation.
## Creating a Rule
You can create a rule by clicking the `+` button in the Rules tab. This will open a new file in your IDE which you can use to write your rule.
<Frame>
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/cline-rules.png" alt="Create a Rule" />
</Frame>
Once you save the file:
- Your rule will be stored in the `.clinerules/` directory in your project (if it's a Workspace Rule)
- Or in the Global Rules directory (if it's a Global Rule):
### Global Rules Directory Location
The location of your Global Rules directory depends on your operating system:
| Operating System | Default Location | Notes |
|------------------|------------------|-------|
| **Windows** | `Documents\Cline\Rules` | Uses system Documents folder |
| **macOS** | `~/Documents/Cline/Rules` | Uses user Documents folder |
| **Linux/WSL** | `~/Documents/Cline/Rules` | May fall back to `~/Cline/Rules` on some systems |
> **Note for Linux/WSL users**: If you don't find your global rules in `~/Documents/Cline/Rules`, check `~/Cline/Rules` as the location may vary depending on your system configuration and whether the Documents directory exists.
You can also have Cline create a rule for you by using the [`/newrule` slash command](/features/slash-commands/new-rule) in the chat.
```markdown Example Cline Rule Structure [expandable]
# Project Guidelines
## Documentation Requirements
- Update relevant documentation in /docs when modifying features
- Keep README.md in sync with new capabilities
- Maintain changelog entries in CHANGELOG.md
## Architecture Decision Records
Create ADRs in /docs/adr for:
- Major dependency changes
- Architectural pattern changes
- New integration patterns
- Database schema changes
Follow template in /docs/adr/template.md
## Code Style & Patterns
- Generate API clients using OpenAPI Generator
- Use TypeScript axios template
- Place generated code in /src/generated
- Prefer composition over inheritance
- Use repository pattern for data access
- Follow error handling pattern in /src/utils/errors.ts
## Testing Standards
- Unit tests required for business logic
- Integration tests for API endpoints
- E2E tests for critical user flows
```
### Key Benefits
1. **Version Controlled**: The `.clinerules` file becomes part of your project's source code
2. **Team Consistency**: Ensures consistent behavior across all team members
3. **Project-Specific**: Rules and standards tailored to each project's needs
4. **Institutional Knowledge**: Maintains project standards and practices in code
Place the `.clinerules` file in your project's root directory:
```
your-project/
├── .clinerules
├── src/
├── docs/
└── ...
```
Cline's system prompt, on the other hand, is not user-editable ([here's where you can find it](https://github.com/cline/cline/blob/main/src/core/prompts/system.ts)). For a broader look at prompt engineering best practices, check out [this resource](https://docs.anthropic.com/en/docs/build-with-claude/prompt-engineering/overview).
### AGENTS.md Standard Support
Cline also supports the [AGENTS.md](https://agents.md/) standard as a fallback
(in addition to Cline Rules) by automatically detecting `AGENTS.md` files in
your workspace root. This allows you to use the same rules file across different AI
coding tools.
```
your-project/
├── AGENTS.md
├── src/
└── ...
```
### Tips for Writing Effective Cline Rules
- Be Clear and Concise: Use simple language and avoid ambiguity.
- Focus on Desired Outcomes: Describe the results you want, not the specific steps.
- Test and Iterate: Experiment to find what works best for your workflow.
### .clinerules/ Folder System
```
your-project/
├── .clinerules/ # Folder containing active rules
│ ├── 01-coding.md # Core coding standards
│ ├── 02-documentation.md # Documentation requirements
│ └── current-sprint.md # Rules specific to current work
├── src/
└── ...
```
Cline automatically processes **all Markdown files** inside the `.clinerules/` directory, combining them into a unified set of rules. The numeric prefixes (optional) help organize files in a logical sequence.
#### Using a Rules Bank
For projects with multiple contexts or teams, maintain a rules bank directory:
```
your-project/
├── .clinerules/ # Active rules - automatically applied
│ ├── 01-coding.md
│ └── client-a.md
├── clinerules-bank/ # Repository of available but inactive rules
│ ├── clients/ # Client-specific rule sets
│ │ ├── client-a.md
│ │ └── client-b.md
│ ├── frameworks/ # Framework-specific rules
│ │ ├── react.md
│ │ └── vue.md
│ └── project-types/ # Project type standards
│ ├── api-service.md
│ └── frontend-app.md
└── ...
```
#### Benefits of the Folder Approach
1. **Contextual Activation**: Copy only relevant rules from the bank to the active folder
2. **Easier Maintenance**: Update individual rule files without affecting others
3. **Team Flexibility**: Different team members can activate rules specific to their current task
4. **Reduced Noise**: Keep the active ruleset focused and relevant
#### Usage Examples
Switch between client projects:
```bash
# Switch to Client B project
rm .clinerules/client-a.md
cp clinerules-bank/clients/client-b.md .clinerules/
```
Adapt to different tech stacks:
```bash
# Frontend React project
cp clinerules-bank/frameworks/react.md .clinerules/
```
#### Implementation Tips
- Keep individual rule files focused on specific concerns
- Use descriptive filenames that clearly indicate the rule's purpose
- Consider git-ignoring the active `.clinerules/` folder while tracking the `clinerules-bank/`
- Create team scripts to quickly activate common rule combinations
The folder system transforms your Cline rules from a static document into a dynamic knowledge system that adapts to your team's changing contexts and requirements.
### Managing Rules with the Toggleable Popover
To make managing both single `.clinerules` files and the folder system even easier, Cline v3.13 introduces a dedicated popover UI directly accessible from the chat interface.
Located conveniently under the chat input field, this popover allows you to:
- **Instantly See Active Rules:** View which global rules (from your user settings) and workspace rules (`.clinerules` file or folder contents) are currently active.
- **Quickly Toggle Rules:** Enable or disable specific rule files within your workspace `.clinerules/` folder with a single click. This is perfect for activating context-specific rules (like `react-rules.md` or `memory-bank.md`) only when needed.
- **Easily Add/Manage Rules:** Quickly create a workspace `.clinerules` file or folder if one doesn't exist, or add new rule files to an existing folder.
This UI significantly simplifies switching contexts and managing different sets of instructions without needing to manually edit files or configurations during a conversation.
<Frame>
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(1).png" alt="Cline Logo" />
</Frame>
@@ -0,0 +1,267 @@
---
title: "Conditional Rules"
sidebarTitle: "Conditional Rules"
description: "Activate rules automatically based on which files you're working with"
---
Conditional rules let you scope rules to specific parts of your codebase. Rules activate only when you're working with matching files, keeping your context focused and relevant.
For an introduction to Cline Rules, see the [Overview](/features/cline-rules/overview).
- **Without conditionals**: every rule loads for every request.
- **With conditionals**, rules activate only when your current files match their defined scope.
For example, React component rules should appear when you're working with React components, not when you're editing Python or documentation.
## How It Works
Conditional rules use YAML frontmatter at the top of your rule files. When Cline processes a request, it gathers context from your current work (open files, visible tabs, mentioned paths, edited files), evaluates each rule's conditions, and activates matching rules.
<Note>
When a conditional rule activates, you'll see a notification: **"Conditional rules applied: workspace:frontend-rules.md"**
</Note>
## Writing Conditional Rules
Add YAML frontmatter to the top of any rule file in your `.clinerules/` directory:
```yaml
---
paths:
- "src/components/**"
- "src/hooks/**"
---
# React Component Guidelines
When creating or modifying React components:
- Use functional components with React hooks
- Extract reusable logic into custom React hooks
- Keep components focused on a single responsibility
```
The `---` markers delimit the frontmatter. Everything after the closing `---` is your rule content.
### The `paths` Conditional
Currently, `paths` is the supported conditional. It takes an array of glob patterns:
```yaml
---
paths:
- "src/**" # All files under src/
- "*.config.js" # Config files in root
- "packages/*/src/" # Monorepo package sources
---
```
**Glob pattern syntax:**
- `*` matches any characters except `/`
- `**` matches any characters including `/` (recursive)
- `?` matches a single character
- `[abc]` matches any character in the brackets
- `{a,b}` matches either pattern
**Examples:**
| Pattern | Matches |
|---------|---------|
| `src/**/*.ts` | All TypeScript files under `src/` |
| `*.md` | Markdown files in root only |
| `**/*.test.ts` | Test files anywhere in the project |
| `packages/{web,api}/**` | Files in web or api packages |
| `src/components/*.tsx` | TSX files directly in components (not nested) |
### Behavior Details
**Multiple patterns**: A rule activates if any pattern matches any file in your context.
```yaml
---
paths:
- "frontend/**"
- "mobile/**"
---
# Activates when working in frontend OR mobile
```
**No frontmatter**: Rules without frontmatter are always active.
**Empty paths array**: `paths: []` means the rule never activates. Use this to temporarily disable a rule.
**Invalid YAML**: If frontmatter can't be parsed, Cline fails open: the rule activates with raw content visible to help debugging.
## What Counts as "Current Context"
Cline evaluates rules based on:
1. **Your message**: File paths mentioned in your prompt (e.g., "update `src/App.tsx`")
2. **Open tabs**: Files currently open in your editor
3. **Visible files**: Files visible in your active editor panes
4. **Edited files**: Files Cline has created, modified, or deleted during the task
5. **Pending operations**: Files Cline is about to edit
Conditional rules can activate on your first message, when relevant files are open, or mid-task when Cline starts working with matching files.
<Tip>
Be explicit about file paths in your prompts. "Update `src/services/user.ts`" reliably triggers path-based rules; "update the user service" may not.
</Tip>
## Practical Examples
Copy these patterns and adapt them to your project structure.
### Frontend vs Backend Rules
Keep frontend and backend rules separate to avoid noise. Frontend rules only load when working with UI code, backend rules only load when working with API or service code.
```yaml
# .clinerules/frontend.md
---
paths:
- "src/components/**"
- "src/pages/**"
- "src/hooks/**"
---
# Frontend Guidelines
- Use Tailwind CSS for styling
- Prefer server components where possible
- Keep client components small and focused
```
```yaml
# .clinerules/backend.md
---
paths:
- "src/api/**"
- "src/services/**"
- "src/db/**"
---
# Backend Guidelines
- Use dependency injection for services
- All database queries go through repositories
- Return typed errors, not thrown exceptions
```
### Test File Rules
Enforce testing standards automatically. This rule activates only when you're writing or modifying tests, so testing guidance appears exactly when you need it.
```yaml
# .clinerules/testing.md
---
paths:
- "**/*.test.ts"
- "**/*.spec.ts"
- "**/__tests__/**"
---
# Testing Standards
- Use descriptive test names: "should [expected behavior] when [condition]"
- One assertion per test when possible
- Mock external dependencies, not internal modules
- Use factories for test data, not fixtures
```
### Documentation Rules
Apply documentation standards only when editing docs. Prevents style rules from cluttering your context when you're writing code.
```yaml
# .clinerules/docs.md
---
paths:
- "docs/**"
- "**/*.md"
- "**/*.mdx"
---
# Documentation Guidelines
- Use sentence case for headings
- Include code examples for all features
- Keep paragraphs short (3-4 sentences max)
- Link to related documentation
```
## Combining with Rule Toggles
Conditional rules work alongside the rule toggle UI. Toggle off a conditional rule to disable it entirely (it won't activate even if paths match). Toggle on to let it activate when conditions are met.
This provides two levels of control: manual toggles and automatic condition-based activation.
## Tips for Effective Conditional Rules
### Start Broad, Then Narrow
Begin with broader patterns and refine as you learn what works:
```yaml
# Start here
paths:
- "src/**"
# Then narrow down
paths:
- "src/features/auth/**"
```
### Use Descriptive Filenames
Name your rule files to indicate their scope:
```
.clinerules/
├── api-endpoints.md # Rules for API code
├── database-models.md # Rules for DB layer
├── react-components.md # Rules for React
└── universal.md # No frontmatter = always active
```
### Keep Universal Rules Separate
Put always-on rules (coding standards, project conventions) in files without frontmatter. Reserve conditional rules for context-specific guidance.
### Test Your Patterns
Not sure if a pattern matches? Create a simple test rule:
```yaml
---
paths:
- "your/pattern/here/**"
---
TEST: This rule should activate for your/pattern/here files.
```
Then work with a file in that path and check if you see the activation notification.
## Troubleshooting
**Rule not activating:**
- Check that file paths in your context match the glob pattern
- Verify the rule is toggled on in the rules panel
- Ensure YAML frontmatter has proper `---` delimiters
**Rule activating unexpectedly:**
- Review glob patterns: `**` is recursive and may match more than intended
- Check for open files that match the pattern
- File paths mentioned in your message also count as context
**Frontmatter showing in output:**
- YAML couldn't be parsed
- Check for syntax errors (unquoted special characters, improper indentation)
## Related
- [Cline Rules Overview](/features/cline-rules/overview) - Complete rules system guide
- [Skills](/features/skills) - Load instructions on demand with `/skill` command
- [Workflows](/features/slash-commands/workflows/index) - Define explicit task automation
- [@ Mentions](/features/at-mentions/overview) - Add files to context explicitly
- [Understanding Context Management](/prompting/understanding-context-management) - How Cline manages context window
+205
View File
@@ -0,0 +1,205 @@
---
title: "Cline Rules"
sidebarTitle: "Overview"
description: "Add persistent instructions and context to guide Cline's behavior"
---
Cline Rules provide system-level guidance for your projects. Rules persist across conversations, ensuring consistent behavior without repeating instructions in every chat.
## How It Works
Rules are loaded when Cline starts a task. Here's what happens:
**Loading order**: Cline checks for rules in this sequence:
1. `.clinerules/` folder (all `.md` files inside)
2. Single `.clinerules` file
3. `AGENTS.md` file
**Scope precedence**: Workspace rules override global rules when both define the same guidance.
**Multiple files**: When using a `.clinerules/` folder, all Markdown files are combined into one ruleset. Numeric prefixes (like `01-`, `02-`) control the order.
**Conditional activation**: Rules with YAML frontmatter activate only when you're working with matching files. See [Conditional Rules](/features/cline-rules/conditional-rules) for details.
## Supported Rule Files
Cline reads rules from multiple file formats in your workspace root, letting you share rules across different AI coding tools:
| File/Folder | Source | Notes |
|-------------|--------|-------|
| `.clinerules/` | Cline | Folder with `.md` files (recommended) |
| `.cursor/rules/` | Cursor | Folder with `.mdc` files |
| `.windsurf/rules` | Windsurf | Folder with multiple `md` files |
| `AGENTS.md` | Universal | Follows [agents.md](https://agents.md/) standard, searched recursively |
Cline prioritizes `.clinerules` when present. Other formats load only if no `.clinerules` exists (except `AGENTS.md`, which always searches subdirectories). All rules appear in the Rules popover where you can toggle them.
## Creating Rules
Click the `+` button in the Rules tab to create a new rule. This opens a file in your editor where you write your guidance.
<Frame>
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/cline-rules.png" alt="Create a Rule" />
</Frame>
When you save the file, it's stored in:
- **Workspace rules**: `.clinerules/` in your project root
- **Global rules**: Platform-specific location (see table below)
You can also use the [`/newrule` slash command](/features/slash-commands/new-rule) to have Cline generate a rule based on your description.
### Global Rules Location
| Operating System | Default Location |
|------------------|------------------|
| **Windows** | `Documents\Cline\Rules` |
| **macOS** | `~/Documents/Cline/Rules` |
| **Linux/WSL** | `~/Documents/Cline/Rules` or `~/Cline/Rules` |
<Note>
Linux/WSL users: Check both locations if you don't find global rules in `~/Documents/Cline/Rules`.
</Note>
## Managing Rules
The Rules popover (below the chat input) shows active rules and lets you toggle them on or off.
<Frame>
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(1).png" alt="Rules Popover" />
</Frame>
The popover displays:
- **Global rules**: From your user-level Rules directory
- **Workspace rules**: From `.clinerules/` in your project
Toggle any rule to enable or disable it. Disabled rules won't load, even if they match conditions.
## When to Use Rules
Rules work best for persistent project context:
- **Code standards**: Formatting preferences, naming conventions, project-specific patterns
- **Documentation requirements**: Where to add docs, what format to follow
- **Architecture decisions**: Design patterns, dependency rules, module boundaries
- **Team conventions**: PR processes, branch naming, commit message format
- **Technology constraints**: Required libraries, banned APIs, version requirements
Rules are less effective for:
- One-time instructions (just say it in the chat)
- Complex multi-step workflows (use [Workflows](/features/slash-commands/workflows/index) instead)
- Dynamic decisions that depend on runtime context
## Example Rule
```markdown
# Backend API Guidelines
## Route Handlers
- Use async/await, not callbacks
- Validate request bodies with Zod schemas
- Return typed errors from `src/errors.ts`
- All routes require authentication unless in `publicRoutes` array
## Database Access
- All queries go through repository classes in `src/repositories/`
- Use transactions for multi-table updates
- Never expose raw database errors to clients
## Testing
- Unit tests for business logic in `src/services/`
- Integration tests for route handlers in `src/routes/`
- Mock external APIs, not internal modules
```
This rule provides clear, actionable guidance without explaining obvious concepts or using vague language.
## Using a Folder Structure
For projects with many rules, organize them in a `.clinerules/` folder:
```
your-project/
├── .clinerules/
│ ├── 01-coding-standards.md
│ ├── 02-documentation.md
│ └── 03-testing.md
├── src/
└── ...
```
Cline loads all Markdown files in `.clinerules/` automatically. The numeric prefixes help you control ordering, but they're optional.
### Organizing a Rules Bank
Maintain a separate folder for rules you might need but don't always want active:
```
your-project/
├── .clinerules/ # Active rules
│ ├── 01-coding.md
│ └── client-a.md
├── clinerules-bank/ # Available but inactive
│ ├── clients/
│ │ ├── client-a.md
│ │ └── client-b.md
│ └── frameworks/
│ ├── react.md
│ └── vue.md
└── ...
```
Copy files from the bank to `.clinerules/` when you need them. This keeps your active context lean while maintaining a library of reusable guidance.
Switch contexts with simple file operations:
```bash
# Switch to Client B
rm .clinerules/client-a.md
cp clinerules-bank/clients/client-b.md .clinerules/
```
<Tip>
Consider git-ignoring `.clinerules/` while tracking `clinerules-bank/` so team members can activate the rules relevant to their current work.
</Tip>
## Conditional Rules
Scope rules to specific file patterns using YAML frontmatter. This keeps React guidance out of Python code and backend rules away from frontend work.
```yaml
---
paths:
- "src/components/**"
- "src/hooks/**"
---
# React Guidelines
Use functional components with hooks. Extract reusable logic into custom hooks.
```
This rule activates only when working with files matching those patterns. Read the [Conditional Rules guide](/features/cline-rules/conditional-rules) for pattern syntax, behavior details, and more examples.
## Tips for Effective Rules
**Be specific**: "Use async/await for all database calls" beats "write good async code."
**Show patterns**: Include file paths and real examples. "Follow the error handling in `src/utils/errors.ts`" gives Cline a concrete reference.
**Focus on outcomes**: Describe what you want, not step-by-step instructions. Let Cline figure out how.
**Test and refine**: Start with core standards. Add rules when you find yourself repeating the same feedback.
**Use conditional rules**: Load guidance only when relevant. This keeps context efficient and reduces noise.
## Related
- [Conditional Rules](/features/cline-rules/conditional-rules) - Activate rules based on file patterns
- [Skills](/features/skills) - Load instructions on demand with `/skill` command
- [Workflows](/features/slash-commands/workflows/index) - Define explicit task automation
- [New Rule Slash Command](/features/slash-commands/new-rule) - Generate rules with AI assistance
- [Plan and Act Mode](/features/plan-and-act) - Use different rules for planning vs execution
-4
View File
@@ -144,10 +144,6 @@ if (process.env.ERROR_SERVICE_API_KEY) {
buildEnvVars["process.env.ERROR_SERVICE_API_KEY"] = JSON.stringify(process.env.ERROR_SERVICE_API_KEY)
}
if (process.env.POSTHOG_TELEMETRY_ENABLED) {
buildEnvVars["process.env.POSTHOG_TELEMETRY_ENABLED"] = JSON.stringify(process.env.POSTHOG_TELEMETRY_ENABLED)
}
// OpenTelemetry configuration (injected at build time from GitHub secrets)
// These provide production defaults that can be overridden at runtime via environment variables
if (process.env.OTEL_TELEMETRY_ENABLED) {
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "claude-dev",
"version": "3.53.1",
"version": "3.55.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "claude-dev",
"version": "3.53.1",
"version": "3.55.0",
"license": "Apache-2.0",
"dependencies": {
"@anthropic-ai/sdk": "^0.37.0",
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "claude-dev",
"displayName": "Cline",
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
"version": "3.53.1",
"version": "3.55.0",
"icon": "assets/icons/icon.png",
"engines": {
"vscode": "^1.84.0"
+14
View File
@@ -75,6 +75,19 @@ message McpResourceTemplate {
optional string description = 4;
}
message McpPromptArgument {
string name = 1;
optional string description = 2;
optional bool required = 3;
}
message McpPrompt {
string name = 1;
optional string title = 2;
optional string description = 3;
repeated McpPromptArgument arguments = 4;
}
enum McpServerStatus {
// Protobuf enums (in proto3) must have a zero value defined, which serves as the default if the field isn't explicitly set.
// To align with the required nature of the TypeScript type and avoid an unnecessary UNSPECIFIED state, we map one of the existing statuses to this zero value.
@@ -95,6 +108,7 @@ message McpServer {
optional int32 timeout = 9;
optional bool oauth_required = 10;
optional string oauth_auth_status = 11;
repeated McpPrompt prompts = 12;
}
message McpServers {
+5 -9
View File
@@ -7,7 +7,7 @@ import { fetch } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { convertToR1Format } from "../transform/r1-format"
import { addReasoningContent } from "../transform/r1-format"
import { ApiStream } from "../transform/stream"
import { getOpenAIToolParams, ToolCallProcessor } from "../transform/tool-call-processor"
@@ -81,14 +81,10 @@ export class DeepSeekHandler implements ApiHandler {
const isDeepseekReasoner = model.id.includes("deepseek-reasoner")
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
]
if (isDeepseekReasoner) {
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
}
const convertedMessages = convertToOpenAiMessages(messages)
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = isDeepseekReasoner
? [{ role: "system", content: systemPrompt }, ...addReasoningContent(convertedMessages, messages)]
: [{ role: "system", content: systemPrompt }, ...convertedMessages]
const stream = await client.chat.completions.create({
model: model.id,
+32 -6
View File
@@ -1,5 +1,6 @@
import { type ModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
import { type Config, type Message, Ollama } from "ollama"
import type { ChatCompletionTool } from "openai/resources/chat/completions"
import { ClineStorageMessage } from "@/shared/messages/content"
import { fetch } from "@/shared/net"
import { Logger } from "@/shared/services/Logger"
@@ -7,6 +8,7 @@ import type { ApiHandler, CommonApiHandlerOptions } from "../"
import { withRetry } from "../retry"
import { convertToOllamaMessages } from "../transform/ollama-format"
import type { ApiStream } from "../transform/stream"
import { ToolCallProcessor } from "../transform/tool-call-processor"
interface OllamaHandlerOptions extends CommonApiHandlerOptions {
ollamaBaseUrl?: string
@@ -51,7 +53,7 @@ export class OllamaHandler implements ApiHandler {
}
@withRetry({ retryAllErrors: true })
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[]): ApiStream {
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: ChatCompletionTool[]): ApiStream {
const client = this.ensureClient()
const ollamaMessages: Message[] = [{ role: "system", content: systemPrompt }, ...convertToOllamaMessages(messages)]
@@ -70,20 +72,44 @@ export class OllamaHandler implements ApiHandler {
options: {
num_ctx: Number(this.options.ollamaApiOptionsCtxNum),
},
tools: tools as any,
})
const toolCallProcessor = new ToolCallProcessor()
// Race the API request against the timeout
const stream = (await Promise.race([apiPromise, timeoutPromise])) as Awaited<typeof apiPromise>
try {
for await (const chunk of stream) {
if (typeof chunk.message.content === "string") {
yield {
type: "text",
text: chunk.message.content,
}
Logger.debug("[OllamaHandler] Message Chunk" + JSON.stringify(chunk))
const delta = chunk.message
if (delta?.tool_calls) {
Logger.debug(`[OllamaHandler] Tool Calls Detected: ${JSON.stringify(delta.tool_calls)}`)
yield* toolCallProcessor.processToolCallDeltas(
delta.tool_calls?.map((tc, inx) => ({
index: inx,
id: `ollama-tool-${inx}`,
function: {
name: tc.function.name,
arguments:
typeof tc.function.arguments === "string"
? tc.function.arguments
: JSON.stringify(tc.function.arguments),
},
type: "function",
})),
)
}
if (typeof delta.content === "string") {
yield {
type: "text",
text: delta.content,
}
}
// Handle token usage if available
if (chunk.eval_count !== undefined || chunk.prompt_eval_count !== undefined) {
yield {
+59
View File
@@ -1,5 +1,64 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { ClineAssistantThinkingBlock, ClineStorageMessage } from "@/shared/messages/content"
/**
* DeepSeek Reasoner message format with reasoning_content support.
*/
export type DeepSeekReasonerMessage = OpenAI.Chat.ChatCompletionMessageParam & {
reasoning_content?: string
}
/**
* Adds reasoning_content to OpenAI messages for DeepSeek Reasoner.
* Per DeepSeek API: reasoning_content should be passed back during tool calling in the same turn,
* and omitted when starting a new turn.
*/
export function addReasoningContent(
openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[],
originalMessages: ClineStorageMessage[],
): DeepSeekReasonerMessage[] {
// Find last user message index (start of current turn)
// If no user message exists (lastUserIndex = -1), all messages are in the "current turn",
// so reasoning_content will be added to all assistant messages. This is intentional.
let lastUserIndex = -1
for (let i = openAiMessages.length - 1; i >= 0; i--) {
if (openAiMessages[i].role === "user") {
lastUserIndex = i
break
}
}
// Extract thinking content from original messages, keyed by assistant index
const thinkingByIndex = new Map<number, string>()
let assistantIdx = 0
for (const msg of originalMessages) {
if (msg.role === "assistant") {
if (Array.isArray(msg.content)) {
const thinking = msg.content
.filter((p): p is ClineAssistantThinkingBlock => p.type === "thinking")
.map((p) => p.thinking)
.join("\n")
if (thinking) {
thinkingByIndex.set(assistantIdx, thinking)
}
}
assistantIdx++
}
}
// Add reasoning_content only to assistant messages in current turn
let aiIdx = 0
return openAiMessages.map((msg, i): DeepSeekReasonerMessage => {
if (msg.role === "assistant") {
const thinking = thinkingByIndex.get(aiIdx++)
if (thinking && i >= lastUserIndex) {
return { ...msg, reasoning_content: thinking }
}
}
return msg
})
}
/**
* Converts Anthropic messages to OpenAI format and merges consecutive messages with the same role.
@@ -464,12 +464,14 @@ Example:
MCP SERVERS
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools and resources to extend your capabilities.
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools, resources, and prompts to extend your capabilities.
# Connected MCP Servers
When a server is connected, you can use the server's tools via the `use_mcp_tool` tool, and access the server's resources via the `access_mcp_resource` tool.
Servers may also provide prompts - predefined templates that can be invoked by users to generate contextual messages.
## test-server (`test`)
### Available Tools
@@ -430,12 +430,14 @@ Example:
MCP SERVERS
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools and resources to extend your capabilities.
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools, resources, and prompts to extend your capabilities.
# Connected MCP Servers
When a server is connected, you can use the server's tools via the `use_mcp_tool` tool, and access the server's resources via the `access_mcp_resource` tool.
Servers may also provide prompts - predefined templates that can be invoked by users to generate contextual messages.
## test-server (`test`)
### Available Tools
@@ -392,12 +392,14 @@ By waiting for and carefully considering the user's response after each tool use
MCP SERVERS
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools and resources to extend your capabilities.
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools, resources, and prompts to extend your capabilities.
# Connected MCP Servers
When a server is connected, you can use the server's tools via the `use_mcp_tool` tool, and access the server's resources via the `access_mcp_resource` tool.
Servers may also provide prompts - predefined templates that can be invoked by users to generate contextual messages.
## test-server (`test`)
### Available Tools
@@ -495,12 +495,14 @@ Example:
MCP SERVERS
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools and resources to extend your capabilities.
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools, resources, and prompts to extend your capabilities.
# Connected MCP Servers
When a server is connected, you can use the server's tools via the `use_mcp_tool` tool, and access the server's resources via the `access_mcp_resource` tool.
Servers may also provide prompts - predefined templates that can be invoked by users to generate contextual messages.
## test-server (`test`)
### Available Tools
@@ -461,12 +461,14 @@ Example:
MCP SERVERS
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools and resources to extend your capabilities.
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools, resources, and prompts to extend your capabilities.
# Connected MCP Servers
When a server is connected, you can use the server's tools via the `use_mcp_tool` tool, and access the server's resources via the `access_mcp_resource` tool.
Servers may also provide prompts - predefined templates that can be invoked by users to generate contextual messages.
## test-server (`test`)
### Available Tools
@@ -419,12 +419,14 @@ By waiting for and carefully considering the user's response after each tool use
MCP SERVERS
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools and resources to extend your capabilities.
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools, resources, and prompts to extend your capabilities.
# Connected MCP Servers
When a server is connected, you can use the server's tools via the `use_mcp_tool` tool, and access the server's resources via the `access_mcp_resource` tool.
Servers may also provide prompts - predefined templates that can be invoked by users to generate contextual messages.
## test-server (`test`)
### Available Tools
@@ -464,12 +464,14 @@ Example:
MCP SERVERS
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools and resources to extend your capabilities.
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools, resources, and prompts to extend your capabilities.
# Connected MCP Servers
When a server is connected, you can use the server's tools via the `use_mcp_tool` tool, and access the server's resources via the `access_mcp_resource` tool.
Servers may also provide prompts - predefined templates that can be invoked by users to generate contextual messages.
## test-server (`test`)
### Available Tools
@@ -430,12 +430,14 @@ Example:
MCP SERVERS
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools and resources to extend your capabilities.
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools, resources, and prompts to extend your capabilities.
# Connected MCP Servers
When a server is connected, you can use the server's tools via the `use_mcp_tool` tool, and access the server's resources via the `access_mcp_resource` tool.
Servers may also provide prompts - predefined templates that can be invoked by users to generate contextual messages.
## test-server (`test`)
### Available Tools
@@ -392,12 +392,14 @@ By waiting for and carefully considering the user's response after each tool use
MCP SERVERS
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools and resources to extend your capabilities.
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools, resources, and prompts to extend your capabilities.
# Connected MCP Servers
When a server is connected, you can use the server's tools via the `use_mcp_tool` tool, and access the server's resources via the `access_mcp_resource` tool.
Servers may also provide prompts - predefined templates that can be invoked by users to generate contextual messages.
## test-server (`test`)
### Available Tools
@@ -464,12 +464,14 @@ Example:
MCP SERVERS
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools and resources to extend your capabilities.
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools, resources, and prompts to extend your capabilities.
# Connected MCP Servers
When a server is connected, you can use the server's tools via the `use_mcp_tool` tool, and access the server's resources via the `access_mcp_resource` tool.
Servers may also provide prompts - predefined templates that can be invoked by users to generate contextual messages.
## test-server (`test`)
### Available Tools
@@ -430,12 +430,14 @@ Example:
MCP SERVERS
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools and resources to extend your capabilities.
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools, resources, and prompts to extend your capabilities.
# Connected MCP Servers
When a server is connected, you can use the server's tools via the `use_mcp_tool` tool, and access the server's resources via the `access_mcp_resource` tool.
Servers may also provide prompts - predefined templates that can be invoked by users to generate contextual messages.
## test-server (`test`)
### Available Tools
@@ -392,12 +392,14 @@ By waiting for and carefully considering the user's response after each tool use
MCP SERVERS
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools and resources to extend your capabilities.
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools, resources, and prompts to extend your capabilities.
# Connected MCP Servers
When a server is connected, you can use the server's tools via the `use_mcp_tool` tool, and access the server's resources via the `access_mcp_resource` tool.
Servers may also provide prompts - predefined templates that can be invoked by users to generate contextual messages.
## test-server (`test`)
### Available Tools
@@ -22,12 +22,14 @@ export function hasEnabledMcpServers(context: SystemPromptContext): boolean {
const MCP_TEMPLATE_TEXT = `MCP SERVERS
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools and resources to extend your capabilities.
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools, resources, and prompts to extend your capabilities.
# Connected MCP Servers
When a server is connected, you can use the server's tools via the \`use_mcp_tool\` tool, and access the server's resources via the \`access_mcp_resource\` tool.
Servers may also provide prompts - predefined templates that can be invoked by users to generate contextual messages.
{{MCP_SERVERS_LIST}}`
export async function getMcp(variant: PromptVariant, context: SystemPromptContext): Promise<string | undefined> {
@@ -71,6 +73,18 @@ function formatMcpServersList(servers: McpServer[]): string {
?.map((resource) => `- ${resource.uri} (${resource.name}): ${resource.description}`)
.join("\n")
const prompts = server.prompts
?.map((prompt) => {
const argsStr = prompt.arguments?.length
? `\n Arguments: ${prompt.arguments
.map((arg) => `${arg.name}${arg.required ? " (required)" : ""}${arg.description ? `: ${arg.description}` : ""}`)
.join(", ")}`
: ""
const title = prompt.title ? ` (${prompt.title})` : ""
return `- ${prompt.name}${title}: ${prompt.description || "No description"}${argsStr}`
})
.join("\n")
const config = JSON.parse(server.config)
return (
@@ -80,7 +94,8 @@ function formatMcpServersList(servers: McpServer[]): string {
: "") +
(tools ? `\n\n### Available Tools\n${tools}` : "") +
(templates ? `\n\n### Resource Templates\n${templates}` : "") +
(resources ? `\n\n### Direct Resources\n${resources}` : "")
(resources ? `\n\n### Direct Resources\n${resources}` : "") +
(prompts ? `\n\n### Available Prompts\n${prompts}` : "")
)
})
.join("\n\n")
@@ -1,5 +1,6 @@
import { ModelFamily } from "@/shared/prompts"
import { ClineDefaultTool } from "@/shared/tools"
import { isGPT5ModelFamily } from "@/utils/model-utils"
import type { ClineToolSpec } from "../spec"
import { TASK_PROGRESS_PARAMETER } from "../types"
@@ -80,7 +81,7 @@ const NATIVE_GPT_5: ClineToolSpec = {
id: ClineDefaultTool.APPLY_PATCH,
name: "apply_patch",
description: APPLY_PATCH_TOOL_DESC,
contextRequirements: (context) => context.providerInfo.model.id.includes("gpt-5"),
contextRequirements: (context) => isGPT5ModelFamily(context.providerInfo.model.id),
parameters: [
{
name: "input",
@@ -0,0 +1,144 @@
import type { McpPromptResponse } from "@shared/mcp"
import { expect } from "chai"
import { formatMcpPromptResponse, McpPromptFetcher, parseSlashCommands } from "../index"
describe("slash-commands", () => {
describe("formatMcpPromptResponse", () => {
it("should format text message", () => {
const response: McpPromptResponse = {
messages: [{ role: "user", content: { type: "text", text: "Hello world" } }],
}
const result = formatMcpPromptResponse(response)
expect(result).to.equal("[User]\nHello world")
})
it("should format assistant message", () => {
const response: McpPromptResponse = {
messages: [{ role: "assistant", content: { type: "text", text: "I can help" } }],
}
const result = formatMcpPromptResponse(response)
expect(result).to.equal("[Assistant]\nI can help")
})
it("should include description when provided", () => {
const response: McpPromptResponse = {
description: "Test description",
messages: [{ role: "user", content: { type: "text", text: "Hello" } }],
}
const result = formatMcpPromptResponse(response)
expect(result).to.include("Description: Test description")
expect(result).to.include("[User]\nHello")
})
it("should format multiple messages", () => {
const response: McpPromptResponse = {
messages: [
{ role: "user", content: { type: "text", text: "Question" } },
{ role: "assistant", content: { type: "text", text: "Answer" } },
],
}
const result = formatMcpPromptResponse(response)
expect(result).to.include("[User]\nQuestion")
expect(result).to.include("[Assistant]\nAnswer")
})
it("should format image content", () => {
const response: McpPromptResponse = {
messages: [{ role: "user", content: { type: "image", data: "base64data", mimeType: "image/png" } }],
}
const result = formatMcpPromptResponse(response)
expect(result).to.equal("[User]\n[Image: image/png]")
})
it("should format audio content", () => {
const response: McpPromptResponse = {
messages: [{ role: "user", content: { type: "audio", data: "base64data", mimeType: "audio/mp3" } }],
}
const result = formatMcpPromptResponse(response)
expect(result).to.equal("[User]\n[Audio: audio/mp3]")
})
it("should format resource with text", () => {
const response: McpPromptResponse = {
messages: [
{
role: "user",
content: {
type: "resource",
resource: { uri: "file:///test.txt", text: "File content" },
},
},
],
}
const result = formatMcpPromptResponse(response)
expect(result).to.include("[Resource: file:///test.txt]")
expect(result).to.include("File content")
})
it("should format resource without text", () => {
const response: McpPromptResponse = {
messages: [
{
role: "user",
content: {
type: "resource",
resource: { uri: "file:///binary.bin" },
},
},
],
}
const result = formatMcpPromptResponse(response)
expect(result).to.equal("[User]\n[Resource: file:///binary.bin]")
})
})
describe("parseSlashCommands MCP handling", () => {
const mockMcpPromptFetcher: McpPromptFetcher = async (serverName, promptName) => {
if (serverName === "test-server" && promptName === "greet") {
return {
description: "A greeting prompt",
messages: [{ role: "user", content: { type: "text", text: "Hello from MCP!" } }],
}
}
return null
}
it("should process MCP prompt command in task tag", async () => {
const text = "<task>/mcp:test-server:greet</task>"
const result = await parseSlashCommands(text, {}, {}, "test-ulid", undefined, false, undefined, mockMcpPromptFetcher)
expect(result.processedText).to.include('<mcp_prompt server="test-server" prompt="greet">')
expect(result.processedText).to.include("Hello from MCP!")
expect(result.needsClinerulesFileCheck).to.equal(false)
})
it("should process MCP prompt with additional text", async () => {
const text = "<task>/mcp:test-server:greet Please expand on this</task>"
const result = await parseSlashCommands(text, {}, {}, "test-ulid", undefined, false, undefined, mockMcpPromptFetcher)
expect(result.processedText).to.include('<mcp_prompt server="test-server" prompt="greet">')
expect(result.processedText).to.include("Please expand on this")
})
it("should handle MCP prompt with colons in prompt name", async () => {
const fetcherWithColons: McpPromptFetcher = async (serverName, promptName) => {
if (serverName === "server" && promptName === "prompt:with:colons") {
return {
messages: [{ role: "user", content: { type: "text", text: "Colon prompt" } }],
}
}
return null
}
const text = "<task>/mcp:server:prompt:with:colons</task>"
const result = await parseSlashCommands(text, {}, {}, "test-ulid", undefined, false, undefined, fetcherWithColons)
expect(result.processedText).to.include('prompt="prompt:with:colons"')
expect(result.processedText).to.include("Colon prompt")
})
// Note: Tests for "unknown MCP server", "no fetcher", and "fetcher errors"
// are skipped because they require StateManager initialization when falling
// through to workflow checking. The core MCP functionality is covered above.
})
})
+76 -3
View File
@@ -1,5 +1,6 @@
import type { ApiProviderInfo } from "@core/api"
import { ClineRulesToggles } from "@shared/cline-rules"
import { McpPromptResponse } from "@shared/mcp"
import fs from "fs/promises"
import { telemetryService } from "@/services/telemetry"
import { Logger } from "@/shared/services/Logger"
@@ -15,6 +16,11 @@ import {
} from "../prompts/commands"
import { StateManager } from "../storage/StateManager"
/**
* Callback type for fetching MCP prompts
*/
export type McpPromptFetcher = (serverName: string, promptName: string) => Promise<McpPromptResponse | null>
type FileBasedWorkflow = {
fullPath: string
fileName: string
@@ -42,6 +48,7 @@ export async function parseSlashCommands(
focusChainSettings?: { enabled: boolean },
enableNativeToolCalls?: boolean,
providerInfo?: ApiProviderInfo,
mcpPromptFetcher?: McpPromptFetcher,
): Promise<{ processedText: string; needsClinerulesFileCheck: boolean }> {
const SUPPORTED_DEFAULT_COMMANDS = [
"newtask",
@@ -79,10 +86,10 @@ export async function parseSlashCommands(
// Regex to find slash commands anywhere in text (not just at the beginning).
// This mirrors how @ mentions work - they can appear anywhere in a message.
//
// Pattern breakdown: /(^|\s)\/([a-zA-Z0-9_.-]+)(?=\s|$)/
// Pattern breakdown: /(^|\s)\/([a-zA-Z0-9_.:@-]+)(?=\s|$)/
// - (^|\s) : Must be at start of string OR preceded by whitespace
// - \/ : The literal slash character
// - ([a-zA-Z0-9_.-]+) : The command name (letters, numbers, underscore, dot, hyphen)
// - ([a-zA-Z0-9_.:@-]+) : The command name (letters, numbers, underscore, dot, hyphen, colon, @)
// - (?=\s|$): Must be followed by whitespace or end of string (lookahead)
//
// This safely avoids false matches in:
@@ -91,7 +98,8 @@ export async function parseSlashCommands(
// - Partial words: "foo/bar" - same reason
//
// Only ONE slash command per message is processed (first match found).
const slashCommandInTextRegex = /(^|\s)\/([a-zA-Z0-9_.-]+)(?=\s|$)/
// Note: Colons are allowed to support MCP prompt commands like /mcp:server:prompt
const slashCommandInTextRegex = /(^|\s)\/([a-zA-Z0-9_.:@-]+)(?=\s|$)/
// Helper function to calculate positions and remove slash command from text
const removeSlashCommand = (
@@ -144,6 +152,39 @@ export async function parseSlashCommands(
return { processedText: processedText, needsClinerulesFileCheck: commandName === "newrule" }
}
// Check for MCP prompt commands (format: mcp:<server>:<prompt>)
if (commandName.startsWith("mcp:") && mcpPromptFetcher) {
const parts = commandName.split(":")
if (parts.length >= 3) {
const serverName = parts[1]
const promptName = parts.slice(2).join(":") // Allow colons in prompt name
try {
const promptResponse = await mcpPromptFetcher(serverName, promptName)
if (promptResponse) {
// Format the prompt messages as text
const promptContent = formatMcpPromptResponse(promptResponse)
// Remove the slash command and add the prompt content
const textWithoutSlashCommand = removeSlashCommand(text, tagContent, contentStartIndex, slashMatch)
const processedText =
`<mcp_prompt server="${serverName}" prompt="${promptName}">\n${promptContent}\n</mcp_prompt>\n` +
textWithoutSlashCommand
// Track telemetry for MCP prompt usage
telemetryService.captureSlashCommandUsed(ulid, commandName, "mcp_prompt")
return { processedText, needsClinerulesFileCheck: false }
} else {
// Prompt not found - log for debugging and fall through to workflow checking
Logger.debug(`MCP prompt not found: ${commandName} (server: ${serverName}, prompt: ${promptName})`)
}
} catch (error) {
Logger.error(`Error fetching MCP prompt ${commandName}: ${error}`)
}
}
}
const globalWorkflows: Workflow[] = Object.entries(globalWorkflowToggles)
.filter(([_, enabled]) => enabled)
.map(([filePath, _]) => ({
@@ -214,3 +255,35 @@ export async function parseSlashCommands(
// if no supported commands are found, return the original text
return { processedText: text, needsClinerulesFileCheck: false }
}
/**
* Formats MCP prompt response messages into a text format for injection
*/
export function formatMcpPromptResponse(response: McpPromptResponse): string {
const parts: string[] = []
if (response.description) {
parts.push(`Description: ${response.description}`)
}
for (const message of response.messages) {
const roleLabel = message.role === "user" ? "User" : "Assistant"
if (message.content.type === "text") {
parts.push(`[${roleLabel}]\n${message.content.text}`)
} else if (message.content.type === "image") {
parts.push(`[${roleLabel}]\n[Image: ${message.content.mimeType}]`)
} else if (message.content.type === "audio") {
parts.push(`[${roleLabel}]\n[Audio: ${message.content.mimeType}]`)
} else if (message.content.type === "resource") {
const resource = message.content.resource
if (resource.text) {
parts.push(`[${roleLabel}]\n[Resource: ${resource.uri}]\n${resource.text}`)
} else {
parts.push(`[${roleLabel}]\n[Resource: ${resource.uri}]`)
}
}
}
return parts.join("\n\n")
}
+10
View File
@@ -3044,6 +3044,15 @@ export class Task {
this.workspaceManager,
)
// Create MCP prompt fetcher callback that wraps mcpHub.getPrompt
const mcpPromptFetcher = async (serverName: string, promptName: string) => {
try {
return await this.mcpHub.getPrompt(serverName, promptName)
} catch {
return null
}
}
const { processedText, needsClinerulesFileCheck: needsCheck } = await parseSlashCommands(
parsedText,
localWorkflowToggles,
@@ -3052,6 +3061,7 @@ export class Task {
focusChainSettings,
useNativeToolCalls,
providerInfo,
mcpPromptFetcher,
)
if (needsCheck) {
+73 -1
View File
@@ -9,6 +9,8 @@ import { getDefaultEnvironment, StdioClientTransport } from "@modelcontextprotoc
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
import {
CallToolResultSchema,
GetPromptResultSchema,
ListPromptsResultSchema,
ListResourcesResultSchema,
ListResourceTemplatesResultSchema,
ListToolsResultSchema,
@@ -16,6 +18,8 @@ import {
} from "@modelcontextprotocol/sdk/types.js"
import {
DEFAULT_MCP_TIMEOUT_SECONDS,
McpPrompt,
McpPromptResponse,
McpResource,
McpResourceResponse,
McpResourceTemplate,
@@ -617,10 +621,11 @@ export class McpHub {
Logger.error(`[MCP Debug] Error setting notification handlers for ${name}:`, error)
}
// Initial fetch of tools and resources
// Initial fetch of tools, resources, and prompts
connection.server.tools = await this.fetchToolsList(name)
connection.server.resources = await this.fetchResourcesList(name)
connection.server.resourceTemplates = await this.fetchResourceTemplatesList(name)
connection.server.prompts = await this.fetchPromptsList(name)
} catch (error) {
// Update status with error
const connection = this.findConnection(name, source)
@@ -716,6 +721,34 @@ export class McpHub {
}
}
private async fetchPromptsList(serverName: string): Promise<McpPrompt[]> {
try {
const connection = this.connections.find((conn) => conn.server.name === serverName)
// Disabled servers don't have clients, so return empty prompts list
if (!connection || connection.server.disabled || !connection.client) {
return []
}
const response = await connection.client.request({ method: "prompts/list" }, ListPromptsResultSchema, {
timeout: DEFAULT_REQUEST_TIMEOUT_MS,
})
return (response?.prompts || []).map((prompt) => ({
name: prompt.name,
title: prompt.title,
description: prompt.description,
arguments: prompt.arguments?.map((arg) => ({
name: arg.name,
description: arg.description,
required: arg.required,
})),
}))
} catch (_error) {
return []
}
}
async deleteConnection(name: string): Promise<void> {
const connection = this.connections.find((conn) => conn.server.name === name)
if (connection) {
@@ -1116,6 +1149,45 @@ export class McpHub {
)
}
async getPrompt(
serverName: string,
promptName: string,
promptArguments?: Record<string, string>,
): Promise<McpPromptResponse> {
const connection = this.connections.find((conn) => conn.server.name === serverName)
if (!connection) {
throw new Error(`No connection found for server: ${serverName}`)
}
if (connection.server.disabled) {
throw new Error(`Server "${serverName}" is disabled`)
}
if (!connection.client) {
throw new Error(`No client available for server: ${serverName}`)
}
const response = await connection.client.request(
{
method: "prompts/get",
params: {
name: promptName,
arguments: promptArguments,
},
},
GetPromptResultSchema,
{
timeout: DEFAULT_REQUEST_TIMEOUT_MS,
},
)
return {
description: response.description,
messages: response.messages.map((msg) => ({
role: msg.role,
content: msg.content as McpPromptResponse["messages"][0]["content"],
})),
}
}
async callTool(
serverName: string,
toolName: string,
+2 -2
View File
@@ -1530,9 +1530,9 @@ export class TelemetryService {
* Records when slash commands or workflows are activated
* @param ulid Unique identifier for the task
* @param commandName The name of the command (e.g., "newtask", "reportbug", or custom workflow name)
* @param commandType Whether it's a built-in command or custom workflow
* @param commandType Whether it's a built-in command, custom workflow, or MCP prompt
*/
public captureSlashCommandUsed(ulid: string, commandName: string, commandType: "builtin" | "workflow") {
public captureSlashCommandUsed(ulid: string, commandName: string, commandType: "builtin" | "workflow" | "mcp_prompt") {
this.capture({
event: TelemetryService.EVENTS.TASK.SLASH_COMMAND_USED,
properties: {
+5 -5
View File
@@ -23,13 +23,13 @@ export const CLINE_ONBOARDING_MODELS: OnboardingModel[] = [
},
{
group: "free",
id: "mistralai/devstral-2512:free",
name: "Mistral: Devstral 2512",
score: 85,
id: "arcee-ai/trinity-large-preview:free",
name: "Arcee AI: Trinity Large Preview",
score: 88,
latency: 2,
badge: "",
badge: "New",
info: {
contextWindow: 256_000,
contextWindow: 131_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
+49
View File
@@ -16,6 +16,7 @@ export type McpServer = {
tools?: McpTool[]
resources?: McpResource[]
resourceTemplates?: McpResourceTemplate[]
prompts?: McpPrompt[]
disabled?: boolean
timeout?: number
uid?: string
@@ -46,6 +47,54 @@ export type McpResourceTemplate = {
mimeType?: string
}
export type McpPromptArgument = {
name: string
description?: string
required?: boolean
}
export type McpPrompt = {
name: string
title?: string
description?: string
arguments?: McpPromptArgument[]
}
export type McpPromptMessageContent =
| {
type: "text"
text: string
}
| {
type: "image"
data: string
mimeType: string
}
| {
type: "audio"
data: string
mimeType: string
}
| {
type: "resource"
resource: {
uri: string
mimeType?: string
text?: string
blob?: string
}
}
export type McpPromptMessage = {
role: "user" | "assistant"
content: McpPromptMessageContent
}
export type McpPromptResponse = {
description?: string
messages: McpPromptMessage[]
}
export type McpResourceResponse = {
_meta?: Record<string, any>
contents: Array<{
@@ -1,11 +1,13 @@
import {
McpServerStatus,
McpPrompt as ProtoMcpPrompt,
McpPromptArgument as ProtoMcpPromptArgument,
McpResource as ProtoMcpResource,
McpResourceTemplate as ProtoMcpResourceTemplate,
McpServer as ProtoMcpServer,
McpServerStatus,
McpTool as ProtoMcpTool,
} from "@shared/proto/cline/mcp"
import { McpOAuthAuthStatus, McpResource, McpResourceTemplate, McpServer, McpTool } from "../../mcp"
import { McpOAuthAuthStatus, McpPrompt, McpPromptArgument, McpResource, McpResourceTemplate, McpServer, McpTool } from "../../mcp"
// Helper to convert TS status to Proto enum
function convertMcpStatusToProto(status: McpServer["status"]): McpServerStatus {
@@ -30,6 +32,7 @@ export function convertMcpServersToProtoMcpServers(mcpServers: McpServer[]): Pro
tools: (server.tools || []).map(convertTool),
resources: (server.resources || []).map(convertResource),
resourceTemplates: (server.resourceTemplates || []).map(convertResourceTemplate),
prompts: (server.prompts || []).map(convertPrompt),
disabled: server.disabled,
timeout: server.timeout,
@@ -81,6 +84,29 @@ function convertResourceTemplate(template: McpResourceTemplate): ProtoMcpResourc
}
}
/**
* Converts McpPromptArgument to ProtoMcpPromptArgument format
*/
function convertPromptArgument(arg: McpPromptArgument): ProtoMcpPromptArgument {
return {
name: arg.name,
description: arg.description,
required: arg.required,
}
}
/**
* Converts McpPrompt to ProtoMcpPrompt format
*/
function convertPrompt(prompt: McpPrompt): ProtoMcpPrompt {
return {
name: prompt.name,
title: prompt.title,
description: prompt.description,
arguments: (prompt.arguments || []).map(convertPromptArgument),
}
}
// Helper to convert Proto enum to TS status
function convertProtoStatusToMcp(status: McpServerStatus): McpServer["status"] {
switch (status) {
@@ -106,6 +132,7 @@ export function convertProtoMcpServersToMcpServers(protoServers: ProtoMcpServer[
tools: protoServer.tools.map(convertProtoTool),
resources: protoServer.resources.map(convertProtoResource),
resourceTemplates: protoServer.resourceTemplates.map(convertProtoResourceTemplate),
prompts: protoServer.prompts.map(convertProtoPrompt),
disabled: protoServer.disabled,
timeout: protoServer.timeout,
@@ -155,3 +182,26 @@ function convertProtoResourceTemplate(protoTemplate: ProtoMcpResourceTemplate):
description: protoTemplate.description === "" ? undefined : protoTemplate.description,
}
}
/**
* Converts ProtoMcpPromptArgument to McpPromptArgument format
*/
function convertProtoPromptArgument(protoArg: ProtoMcpPromptArgument): McpPromptArgument {
return {
name: protoArg.name,
description: protoArg.description === "" ? undefined : protoArg.description,
required: protoArg.required,
}
}
/**
* Converts ProtoMcpPrompt to McpPrompt format
*/
function convertProtoPrompt(protoPrompt: ProtoMcpPrompt): McpPrompt {
return {
name: protoPrompt.name,
title: protoPrompt.title === "" ? undefined : protoPrompt.title,
description: protoPrompt.description === "" ? undefined : protoPrompt.description,
arguments: protoPrompt.arguments.map(convertProtoPromptArgument),
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
export interface SlashCommand {
name: string
description?: string
section?: "default" | "custom"
section?: "default" | "custom" | "mcp"
cliCompatible?: boolean
}
+28 -1
View File
@@ -1,6 +1,6 @@
import { describe, it } from "mocha"
import "should"
import { isClaude4PlusModelFamily, shouldSkipReasoningForModel } from "../model-utils"
import { isClaude4PlusModelFamily, isGPT5ModelFamily, shouldSkipReasoningForModel } from "../model-utils"
describe("shouldSkipReasoningForModel", () => {
it("should return true for grok-4 models", () => {
@@ -55,3 +55,30 @@ describe("isClaude4PlusModelFamily", () => {
isClaude4PlusModelFamily("llama-3").should.equal(false)
})
})
describe("isGPT5ModelFamily", () => {
it("should return true for GPT-5 model IDs with hyphen", () => {
isGPT5ModelFamily("gpt-5").should.equal(true)
isGPT5ModelFamily("gpt-5.1").should.equal(true)
isGPT5ModelFamily("gpt-5.2-codex").should.equal(true)
isGPT5ModelFamily("openai/gpt-5").should.equal(true)
})
it("should return true for GPT-5 model IDs without hyphen (OCA format)", () => {
isGPT5ModelFamily("gpt5").should.equal(true)
isGPT5ModelFamily("oca/gpt5").should.equal(true)
})
it("should be case insensitive", () => {
isGPT5ModelFamily("GPT-5").should.equal(true)
isGPT5ModelFamily("GPT5").should.equal(true)
isGPT5ModelFamily("OCA/GPT5").should.equal(true)
})
it("should return false for non-GPT-5 models", () => {
isGPT5ModelFamily("gpt-4").should.equal(false)
isGPT5ModelFamily("gpt-4o").should.equal(false)
isGPT5ModelFamily("claude-3-sonnet").should.equal(false)
isGPT5ModelFamily("gemini-pro").should.equal(false)
})
})
+8 -1
View File
@@ -18,6 +18,7 @@ export function isNextGenModelProvider(providerInfo: ApiProviderInfo): boolean {
"openai-codex",
"baseten",
"vercel-ai-gateway",
"deepseek",
"oca",
].some((id) => providerId === id)
}
@@ -140,6 +141,11 @@ function isDeepSeek32ModelFamily(id: string): boolean {
return modelId.includes("deepseek") && modelId.includes("3.2") && !modelId.includes("speciale")
}
export function isDeepSeekNativeModelFamily(id: string): boolean {
const modelId = normalize(id)
return modelId.includes("deepseek-chat") || modelId.includes("deepseek-reasoner")
}
export function isNextGenModelFamily(id: string): boolean {
const modelId = normalize(id)
return (
@@ -150,7 +156,8 @@ export function isNextGenModelFamily(id: string): boolean {
isMinimaxModelFamily(modelId) ||
isGemini3ModelFamily(modelId) ||
isNextGenOpenSourceModelFamily(modelId) ||
isDeepSeek32ModelFamily(modelId)
isDeepSeek32ModelFamily(modelId) ||
isDeepSeekNativeModelFamily(modelId)
)
}
@@ -3,6 +3,7 @@ import { BrowserAction, BrowserActionResult, ClineMessage, ClineSayBrowserAction
import { StringRequest } from "@shared/proto/cline/common"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import deepEqual from "fast-deep-equal"
import { ChevronDownIcon, ChevronRightIcon } from "lucide-react"
import React, { CSSProperties, memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
import { useSize } from "react-use"
import styled from "styled-components"
@@ -438,7 +439,7 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
cursor: "pointer",
padding: `9px 8px ${consoleLogsExpanded ? 0 : 8}px 8px`,
}}>
<span className={`codicon codicon-chevron-${consoleLogsExpanded ? "down" : "right"}`}></span>
{consoleLogsExpanded ? <ChevronDownIcon size={16} /> : <ChevronRightIcon size={16} />}
<span style={consoleLogsTextStyle}>Console Logs</span>
</div>
{consoleLogsExpanded && (
+4 -5
View File
@@ -16,6 +16,8 @@ import {
ArrowRightIcon,
BellIcon,
CheckIcon,
ChevronDownIcon,
ChevronRightIcon,
CircleSlashIcon,
CircleXIcon,
FileCode2Icon,
@@ -62,9 +64,6 @@ import SearchResultsDisplay from "./SearchResultsDisplay"
import { ThinkingRow } from "./ThinkingRow"
import UserMessage from "./UserMessage"
// State type for api_req_started rendering
type ApiReqState = "pre" | "thinking" | "error" | "final"
const HEADER_CLASSNAMES = "flex items-center gap-2.5 mb-3"
interface ChatRowProps {
@@ -641,7 +640,7 @@ export const ChatRowContent = memo(
<div className="flex items-center mb-2">
<span className="font-bold mr-1">Summary:</span>
<div className="grow" />
<span className="codicon codicon-chevron-up my-0.5 shrink-0" />
<ChevronDownIcon className="my-0.5 shrink-0 size-4" />
</div>
<span className="ph-no-capture break-words whitespace-pre-wrap">{tool.content}</span>
</div>
@@ -650,7 +649,7 @@ export const ChatRowContent = memo(
<span className="ph-no-capture whitespace-nowrap overflow-hidden text-ellipsis text-left flex-1 mr-2 [direction:rtl]">
{tool.content + "\u200E"}
</span>
<span className="codicon codicon-chevron-down my-0.5 shrink-0" />
<ChevronRightIcon className="my-0.5 shrink-0 size-4" />
</div>
)}
</div>
@@ -273,6 +273,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
showChatModelSelector: showModelSelector,
setShowChatModelSelector: setShowModelSelector,
dictationSettings,
mcpServers,
} = useExtensionState()
const { clineUser } = useClineAuth()
const [isTextAreaFocused, setIsTextAreaFocused] = useState(false)
@@ -527,6 +528,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
globalWorkflowToggles,
remoteWorkflowToggles,
remoteConfigSettings?.remoteGlobalWorkflows,
mcpServers,
)
if (allCommands.length === 0) {
@@ -551,6 +553,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
globalWorkflowToggles,
remoteWorkflowToggles,
remoteConfigSettings?.remoteGlobalWorkflows,
mcpServers,
)
if (commands.length > 0) {
handleSlashCommandsSelect(commands[selectedSlashCommandsIndex])
@@ -1503,6 +1506,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
<SlashCommandMenu
globalWorkflowToggles={globalWorkflowToggles}
localWorkflowToggles={localWorkflowToggles}
mcpServers={mcpServers}
onMouseDown={handleMenuMouseDown}
onSelect={handleSlashCommandsSelect}
query={slashCommandsQuery}
+1 -1
View File
@@ -104,7 +104,7 @@ const ErrorRow = memo(({ message, errorType, apiRequestFailedMessage, apiReqStre
}
// Regular error message
return <p className="m-0 mt-4 whitespace-pre-wrap text-error wrap-anywhere">{message.text}</p>
return <p className="m-0 mt-0 whitespace-pre-wrap text-error wrap-anywhere">{message.text}</p>
case "diff_error":
return (
@@ -5,6 +5,7 @@ import type React from "react"
import { useMemo } from "react"
import { cleanPathPrefix } from "../common/CodeAccordian"
import { getIconByToolName } from "./chat-view"
import { isApiReqAbsorbable, isLowStakesTool } from "./chat-view/utils/messageUtils"
import ErrorRow from "./ErrorRow"
import { ThinkingRow } from "./ThinkingRow"
import { TypewriterText } from "./TypewriterText"
@@ -62,12 +63,17 @@ const collectToolsInRange = (
stopCondition?: (msg: ClineMessage) => boolean,
): { icon: LucideIcon; text: string }[] => {
const activities: { icon: LucideIcon; text: string }[] = []
for (let i = startIdx; i < endIdx; i++) {
const msg = messages[i]
if (stopCondition?.(msg)) {
break
}
if (msg.say !== "tool" && msg.ask !== "tool") {
// Only collect tools that are currently executing (ask === "tool")
// Skip completed tools (say === "tool") - they should be in the completed list
if (msg.say === "tool" || msg.ask !== "tool") {
continue
}
@@ -138,19 +144,27 @@ export const RequestStartRow: React.FC<RequestStartRowProps> = ({
const hasError = !!(apiRequestFailedMessage || apiReqStreamingFailedMessage)
const hasCost = cost != null
const hasReasoning = !!reasoningContent
const hasResponseStarted = !!responseStarted
const hasCompletionResult = clineMessages.some(
(msg) => msg.ask === "completion_result" || msg.say === "completion_result" || msg.ask === "plan_mode_respond",
)
const apiReqState: ApiReqState = hasError ? "error" : hasCost ? "final" : hasReasoning ? "thinking" : "pre"
// While reasoning is streaming, keep the Brain ThinkingBlock exactly as-is.
// Once response content starts (any text/tool/command), collapse into a compact
// "🧠 Thinking" row that can be expanded to show the reasoning only.
const showStreamingThinking = hasReasoning && !hasResponseStarted && !hasError && !hasCost
const showCollapsedThinking = hasReasoning && !showStreamingThinking
const showStreamingThinking = useMemo(
() => hasReasoning && !hasError && !cost && !responseStarted,
[hasReasoning, hasError, cost, responseStarted],
)
// Check if this api_req will be absorbed into a tool group (reasoning will disappear)
const willBeAbsorbed = useMemo(() => {
return isApiReqAbsorbable(message.ts, clineMessages)
}, [message.ts, clineMessages])
// Find all exploratory tool activities that are currently in flight.
// Only show tools between the previous completed API request and the current incomplete one.
// Once an API request completes (has cost), tool messages that follow belong to the next cycle.
// Tools come AFTER the api_req_started message, so we look from currentApiReq forward.
const currentActivities = useMemo(() => {
const currentApiReq = findCurrentApiReq(clineMessages)
if (!currentApiReq) {
@@ -159,47 +173,76 @@ export const RequestStartRow: React.FC<RequestStartRowProps> = ({
if (!currentApiReq.hasCost) {
// CASE A: Current api_req is INCOMPLETE
const prevIdx = findPrevCompletedApiReq(clineMessages, currentApiReq.index)
if (prevIdx === -1) {
return []
}
return collectToolsInRange(clineMessages, prevIdx + 1, currentApiReq.index)
// Look for ask === "tool" messages AFTER the current api_req_started
return collectToolsInRange(clineMessages, currentApiReq.index + 1, clineMessages.length)
}
// CASE B: Current api_req is COMPLETE - no activities to show
return []
}, [clineMessages])
// Check if there are any completed tools in the tool group
const hasCompletedTools = useMemo(() => {
// Look for any completed low-stakes tool messages that would be in a tool group
return clineMessages.some((msg, idx) => {
if (msg.say === "tool" && isLowStakesTool(msg)) {
// Check if this tool is from a completed API request
// (looking backwards for an api_req with cost)
for (let i = idx - 1; i >= 0; i--) {
const prevMsg = clineMessages[i]
if (prevMsg.say === "api_req_started" && prevMsg.text) {
try {
const info = JSON.parse(prevMsg.text)
return info.cost != null
} catch {
return false
}
}
}
}
return false
})
}, [clineMessages])
// Only show currentActivities if there are NO completed tools
// (otherwise they'll be shown in the unified ToolGroupRenderer list)
const shouldShowActivities = currentActivities.length > 0 && !hasCompletedTools
return (
<div>
{apiReqState === "pre" && (
{apiReqState === "pre" && shouldShowActivities && (
<div className="flex items-center text-description w-full text-sm">
<div className="ml-1 flex-1 w-full h-full">
{currentActivities.length > 0 ? (
<div className="flex flex-col gap-0.5 w-full min-h-1">
{currentActivities.map((activity, _) => (
<div className="flex items-center gap-2 h-auto w-full overflow-hidden" key={activity.text}>
<activity.icon className="size-2 text-foreground shrink-0" />
<TypewriterText speed={15} text={activity.text} />
</div>
))}
</div>
) : (
<TypewriterText
text={message.partial !== false ? (mode === "plan" ? "Planning..." : "Thinking...") : ""}
/>
)}
<div className="flex flex-col gap-0.5 w-full min-h-1">
{currentActivities.map((activity, _) => (
<div className="flex items-center gap-2 h-auto w-full overflow-hidden" key={activity.text}>
<activity.icon className="size-2 text-foreground shrink-0" />
<TypewriterText speed={15} text={activity.text} />
</div>
))}
</div>
</div>
</div>
)}
{reasoningContent && (
<ThinkingRow
isExpanded={isExpanded || showStreamingThinking || showCollapsedThinking}
isVisible={true}
onToggle={handleToggle}
reasoningContent={reasoningContent}
showTitle={false}
/>
)}
{reasoningContent &&
(!hasCost ? (
// Still streaming - show "Thinking..." text with shimmer
<div className="ml-1 pl-0 mb-1 -mt-1.25 pt-1">
<div className="inline-flex justify-baseline gap-0.5 text-left select-none px-0 w-full">
<span className="animate-shimmer bg-linear-90 from-foreground to-description bg-[length:200%_100%] bg-clip-text text-transparent">
Thinking...
</span>
</div>
</div>
) : (
// Complete - always show collapsible "Thoughts" section
<ThinkingRow
isExpanded={isExpanded}
isVisible={true}
onToggle={handleToggle}
reasoningContent={reasoningContent}
showTitle={true}
/>
))}
{apiReqState === "error" && (
<ErrorRow
@@ -1,3 +1,4 @@
import { ChevronDownIcon, ChevronRightIcon } from "lucide-react"
import React, { useMemo } from "react"
import CodeAccordian from "../common/CodeAccordian"
@@ -121,12 +122,11 @@ const SearchResultsDisplay: React.FC<SearchResultsDisplayProps> = ({
{path + (filePattern ? `/(${filePattern})` : "")}
</span>
<div style={{ flexGrow: 1 }}></div>
<span
className={`codicon codicon-chevron-${isExpanded ? "up" : "down"}`}
style={{
fontSize: 13.5,
margin: "1px 0",
}}></span>
{isExpanded ? (
<ChevronDownIcon size={16} style={{ margin: "1px 0" }} />
) : (
<ChevronRightIcon size={16} style={{ margin: "1px 0" }} />
)}
</div>
{isExpanded && (
@@ -1,4 +1,5 @@
import { type SlashCommand } from "@shared/slashCommands"
import type { McpServer } from "@shared/mcp"
import type { SlashCommand } from "@/utils/slash-commands"
import React, { useCallback, useEffect, useRef } from "react"
import ScreenReaderAnnounce from "@/components/common/ScreenReaderAnnounce"
import { useMenuAnnouncement } from "@/hooks/useMenuAnnouncement"
@@ -14,6 +15,7 @@ interface SlashCommandMenuProps {
globalWorkflowToggles?: Record<string, boolean>
remoteWorkflowToggles?: Record<string, boolean>
remoteWorkflows?: any[]
mcpServers?: McpServer[]
}
const SlashCommandMenu: React.FC<SlashCommandMenuProps> = ({
@@ -26,6 +28,7 @@ const SlashCommandMenu: React.FC<SlashCommandMenuProps> = ({
globalWorkflowToggles = {},
remoteWorkflowToggles,
remoteWorkflows,
mcpServers = [],
}) => {
const menuRef = useRef<HTMLDivElement>(null)
@@ -36,9 +39,11 @@ const SlashCommandMenu: React.FC<SlashCommandMenuProps> = ({
globalWorkflowToggles,
remoteWorkflowToggles,
remoteWorkflows,
mcpServers,
)
const defaultCommands = filteredCommands.filter((cmd) => cmd.section === "default" || !cmd.section)
const workflowCommands = filteredCommands.filter((cmd) => cmd.section === "custom")
const mcpCommands = filteredCommands.filter((cmd) => cmd.section === "mcp")
// Screen reader announcements
const getCommandLabel = useCallback((command: SlashCommand) => {
@@ -135,6 +140,12 @@ const SlashCommandMenu: React.FC<SlashCommandMenuProps> = ({
<>
{renderCommandSection(defaultCommands, "Default Commands", 0, true)}
{renderCommandSection(workflowCommands, "Workflow Commands", defaultCommands.length, false)}
{renderCommandSection(
mcpCommands,
"MCP Prompts",
defaultCommands.length + workflowCommands.length,
true,
)}
</>
) : (
<div aria-selected="false" className="py-2 px-3 cursor-default flex flex-col" role="option">
+36 -17
View File
@@ -1,5 +1,5 @@
import { ChevronDownIcon, ChevronRightIcon } from "lucide-react"
import { memo, useEffect, useRef } from "react"
import { memo, useCallback, useEffect, useRef, useState } from "react"
import { Button } from "@/components/ui/button"
import { cn } from "@/lib/utils"
@@ -13,6 +13,14 @@ interface ThinkingRowProps {
export const ThinkingRow = memo(({ showTitle = false, reasoningContent, isVisible, isExpanded, onToggle }: ThinkingRowProps) => {
const scrollRef = useRef<HTMLDivElement>(null)
const [canScrollDown, setCanScrollDown] = useState(false)
const checkScrollable = useCallback(() => {
if (scrollRef.current) {
const { scrollTop, scrollHeight, clientHeight } = scrollRef.current
setCanScrollDown(scrollTop + clientHeight < scrollHeight - 1)
}
}, [])
// Only auto-scroll to bottom during streaming (showCursor=true)
// For expanded collapsed thinking, start at top
@@ -20,24 +28,31 @@ export const ThinkingRow = memo(({ showTitle = false, reasoningContent, isVisibl
if (scrollRef.current && isVisible) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight
}
}, [reasoningContent, isVisible])
checkScrollable()
}, [reasoningContent, isVisible, checkScrollable])
if (!isVisible) {
return null
}
// Don't render anything if collapsed and no title (nothing to show)
if (!isExpanded && !showTitle) {
return null
}
return (
<div className="ml-1">
<div className="ml-1 pl-0 mb-1 -mt-1.25">
{showTitle ? (
<Button
className="inline-flex justify-baseline gap-0.5 text-left select-none cursor-pointer text-description px-0 w-full"
className="inline-flex justify-baseline gap-0.5 text-left select-none cursor-pointer px-0 w-full"
onClick={onToggle}
variant="icon">
{isExpanded ? <ChevronDownIcon className="opacity-70" /> : <ChevronRightIcon className="opacity-70" />}
<span className="font-semibold">Thinking:</span>
<span className="italic break-words truncate [direction:rtl] w-full">
{!isExpanded ? reasoningContent : ""}
</span>
<span>Thoughts</span>
{isExpanded ? (
<ChevronDownIcon className="!size-1 text-foreground" />
) : (
<ChevronRightIcon className="!size-1 text-foreground" />
)}
</Button>
) : null}
@@ -55,15 +70,19 @@ export const ThinkingRow = memo(({ showTitle = false, reasoningContent, isVisibl
disabled={!showTitle}
onClick={onToggle}
variant="text">
<div
className={cn(
"flex max-h-[150px] overflow-y-auto text-description leading-normal truncated whitespace-pre-wrap break-words flex-1 [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden [direction:ltr]",
{
"pl-2 border-l border-description/50": showTitle,
},
<div className="relative flex-1">
<div
className={cn(
"flex max-h-[150px] overflow-y-auto text-description leading-normal truncated whitespace-pre-wrap break-words [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden [direction:ltr]",
"pl-2 border-l border-description/50",
)}
onScroll={checkScrollable}
ref={scrollRef}>
<span className="pb-2 block text-xs">{reasoningContent}</span>
</div>
{canScrollDown && (
<div className="absolute bottom-0 left-0 right-0 h-6 pointer-events-none bg-gradient-to-t from-background to-transparent" />
)}
ref={scrollRef}>
<span>{reasoningContent}</span>
</div>
</Button>
)}
@@ -1,3 +1,4 @@
import { ChevronDownIcon, ChevronRightIcon } from "lucide-react"
import { useRef, useState } from "react"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { getAsVar, VSC_TITLEBAR_INACTIVE_FOREGROUND } from "@/utils/vscStyles"
@@ -161,11 +162,7 @@ const AutoApproveBar = ({ style }: AutoApproveBarProps) => {
<span className="whitespace-nowrap">Auto-approve:</span>
{getEnabledActionsText()}
</div>
{isModalVisible ? (
<span className="codicon codicon-chevron-down" />
) : (
<span className="codicon codicon-chevron-up" />
)}
{isModalVisible ? <ChevronDownIcon size={16} /> : <ChevronRightIcon size={16} />}
</div>
<AutoApproveModal
@@ -0,0 +1,136 @@
/**
* STASHED CODE - Idle Indicator with MutationObserver
*
* This code implements a "Thinking..."/"Working..." indicator that appears after 3 seconds
* of DOM silence. It was removed from MessagesArea.tsx but preserved here for future use.
*
* To re-enable:
* 1. Import this hook in MessagesArea.tsx
* 2. Call useIdleIndicator() and get showIdleIndicator state
* 3. Add the indicator to the Virtuoso Footer component
*/
import { ClineMessage } from "@shared/ExtensionMessage"
import { useEffect, useRef, useState } from "react"
// Idle timeout in milliseconds before showing indicator
const IDLE_TIMEOUT_MS = 3000
/**
* Hook that detects when the DOM has been idle for IDLE_TIMEOUT_MS
* Uses MutationObserver to track actual content changes
*/
export function useIdleIndicator(scrollContainerRef: React.RefObject<HTMLDivElement>, clineMessages: ClineMessage[]): boolean {
const [showIdleIndicator, setShowIdleIndicator] = useState(false)
const idleTimerRef = useRef<NodeJS.Timeout | null>(null)
const timerStartTimeRef = useRef<number | null>(null)
useEffect(() => {
const container = scrollContainerRef.current
if (!container) {
return
}
// Check if task is complete
const isTaskComplete = clineMessages.some(
(msg) => msg.ask === "completion_result" || msg.say === "completion_result" || msg.ask === "plan_mode_respond",
)
if (isTaskComplete) {
// Don't show indicator if task is complete
setShowIdleIndicator(false)
timerStartTimeRef.current = null
return
}
console.log("[IdleIndicator] Setting up MutationObserver")
const resetIdleTimer = () => {
// Clear existing timer
if (idleTimerRef.current) {
clearTimeout(idleTimerRef.current)
}
// Hide indicator immediately when new content arrives
setShowIdleIndicator(false)
// Record start time if this is the first mutation
if (!timerStartTimeRef.current) {
timerStartTimeRef.current = Date.now()
}
// Calculate elapsed and remaining time
const elapsed = Date.now() - timerStartTimeRef.current
const remaining = Math.max(0, IDLE_TIMEOUT_MS - elapsed)
console.log(
`[IdleIndicator] DOM mutation detected, restarting timer. Elapsed: ${elapsed}ms, Remaining: ${remaining}ms`,
)
// Start new timer for remaining duration
idleTimerRef.current = setTimeout(() => {
console.log("[IdleIndicator] DOM idle for 3s, showing indicator")
setShowIdleIndicator(true)
}, remaining)
}
// Observe changes to the chat container
const observer = new MutationObserver((mutations) => {
// Only reset timer if there are actual content changes
const hasContentChange = mutations.some((mutation) => {
return (
mutation.type === "childList" ||
mutation.type === "characterData" ||
(mutation.type === "attributes" && mutation.attributeName !== "style")
)
})
if (hasContentChange) {
resetIdleTimer()
}
})
observer.observe(container, {
childList: true,
subtree: true,
characterData: true,
attributes: true,
})
// Start initial timer
resetIdleTimer()
return () => {
console.log("[IdleIndicator] Cleaning up MutationObserver")
observer.disconnect()
if (idleTimerRef.current) {
clearTimeout(idleTimerRef.current)
}
timerStartTimeRef.current = null
}
}, [scrollContainerRef, clineMessages])
return showIdleIndicator
}
/**
* Component to render in Virtuoso Footer
*
* Usage:
* <Virtuoso
* components={{
* Footer: () => (
* <div>
* <div className="min-h-1" />
* {showIdleIndicator && (
* <div className="flex items-center text-description text-sm px-4 pt-2.5 pb-2.5">
* <div className="ml-1">
* <TypewriterText text={mode === "plan" ? "Thinking..." : "Working..."} />
* </div>
* </div>
* )}
* </div>
* ),
* }}
* />
*/
@@ -60,10 +60,22 @@ export const MessageRenderer: React.FC<MessageRendererProps> = ({
}, [messageOrGroup, modifiedMessages])
// Tool group (low-stakes tools grouped together)
// Always render - the loading state in ChatRow shows current activities,
// while ToolGroupRenderer shows what's in context. Overlap is fine.
// Determine if this is the last tool group to show active items
const isLastToolGroup = useMemo(() => {
if (!isToolGroup(messageOrGroup)) {
return false
}
// Find the last tool group in groupedMessages
for (let i = groupedMessages.length - 1; i >= 0; i--) {
if (isToolGroup(groupedMessages[i])) {
return i === index
}
}
return false
}, [messageOrGroup, groupedMessages, index])
if (isToolGroup(messageOrGroup)) {
return <ToolGroupRenderer allMessages={modifiedMessages} messages={messageOrGroup} />
return <ToolGroupRenderer allMessages={modifiedMessages} isLastGroup={isLastToolGroup} messages={messageOrGroup} />
}
// Browser session group
@@ -1,9 +1,9 @@
import { ClineMessage, ClineSayTool } from "@shared/ExtensionMessage"
import { StringRequest } from "@shared/proto/cline/common"
import { memo, useCallback, useMemo, useState } from "react"
import { TypewriterText } from "@/components/chat/TypewriterText"
import { cleanPathPrefix } from "@/components/common/CodeAccordian"
import { Button } from "@/components/ui/button"
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
import { cn } from "@/lib/utils"
import { FileServiceClient } from "@/services/grpc-client"
import { getIconByToolName, getToolsNotInCurrentActivities, isLowStakesTool } from "../../utils/messageUtils"
@@ -11,28 +11,138 @@ import { getIconByToolName, getToolsNotInCurrentActivities, isLowStakesTool } fr
interface ToolGroupRendererProps {
messages: ClineMessage[]
allMessages: ClineMessage[]
isLastGroup: boolean
}
interface ToolWithReasoning {
tool: ClineMessage
parsedTool: ClineSayTool
reasoning?: string
isActive?: boolean
activityText?: string
}
const EXPANDABLE_TOOLS = new Set(["listFilesTopLevel", "listFilesRecursive", "listCodeDefinitionNames", "searchFiles"])
// Helper to format activity text for active items (from RequestStartRow logic)
const getActivityText = (tool: ClineSayTool): string | null => {
const cleanedPath = cleanPathPrefix(tool.path || "")
const formatSearchRegex = (regex: string, path: string, filePattern?: string): string => {
const cleanedPath = cleanPathPrefix(path)
const terms = regex
.split("|")
.map((t) => t.trim().replace(/\\b/g, "").replace(/\\s\?/g, " "))
.filter(Boolean)
.join(" | ")
return filePattern && filePattern !== "*"
? `"${terms}" in ${cleanedPath}/ (${filePattern})`
: `"${terms}" in ${cleanedPath}/`
}
switch (tool.tool) {
case "readFile":
return tool.path ? `Reading ${cleanedPath}...` : null
case "listFilesTopLevel":
case "listFilesRecursive":
return tool.path ? `Exploring ${cleanedPath}/...` : null
case "searchFiles":
return tool.regex && tool.path ? `Searching ${formatSearchRegex(tool.regex, tool.path, tool.filePattern)}...` : null
case "listCodeDefinitionNames":
return tool.path ? `Analyzing ${cleanedPath}/...` : null
default:
return null
}
}
// Calculate current activities (from RequestStartRow logic)
const getCurrentActivities = (allMessages: ClineMessage[]): ClineMessage[] => {
// Find current api_req
let currentApiReqIndex = -1
for (let i = allMessages.length - 1; i >= 0; i--) {
const msg = allMessages[i]
if (msg.say === "api_req_started" && msg.text) {
try {
const info = JSON.parse(msg.text)
const hasCost = info.cost != null
if (!hasCost) {
currentApiReqIndex = i
break
}
} catch {
// ignore
}
}
}
if (currentApiReqIndex === -1) {
return []
}
// Collect tools AFTER the current api_req_started
const activities: ClineMessage[] = []
for (let i = currentApiReqIndex + 1; i < allMessages.length; i++) {
const msg = allMessages[i]
// Only collect tools that are currently executing (ask === "tool")
// Skip completed tools (say === "tool") - they should be in the completed list
if (msg.say === "tool" || msg.ask !== "tool") {
continue
}
if (isLowStakesTool(msg)) {
activities.push(msg)
}
}
return activities
}
/**
* Renders a collapsible group of low-stakes tool calls.
* Only shows tools that are NOT in the "current activities" range (PAST tools only).
* Shows both completed tools AND currently active tools in a unified list (only for last group).
*/
export const ToolGroupRenderer = memo(({ messages, allMessages }: ToolGroupRendererProps) => {
export const ToolGroupRenderer = memo(({ messages, allMessages, isLastGroup }: ToolGroupRendererProps) => {
const [expandedItems, setExpandedItems] = useState<Record<number, boolean>>({})
// Filter out tools in the "current activities" range (being shown in loading state)
const filteredMessages = useMemo(() => getToolsNotInCurrentActivities(messages, allMessages), [messages, allMessages])
// Build tool items with associated reasoning (reasoning that comes BEFORE a tool)
const toolsWithReasoning = useMemo(() => buildToolsWithReasoning(filteredMessages), [filteredMessages])
// Get current activities (active reading/exploring) - only for last group
const currentActivities = useMemo(() => {
if (!isLastGroup) {
return []
}
return getCurrentActivities(allMessages)
}, [allMessages, isLastGroup])
// Build completed tool items
const completedTools = useMemo(() => buildToolsWithReasoning(filteredMessages), [filteredMessages])
// Build active tool items
const activeTools = useMemo(() => {
return currentActivities
.map((msg) => {
const parsedTool = parseToolSafe(msg.text)
return {
tool: msg,
parsedTool,
reasoning: undefined,
isActive: true,
activityText: getActivityText(parsedTool),
}
})
.filter((item) => item.activityText)
}, [currentActivities])
// Merge: completed items first, then active items (active only added to last group)
// Deduplicate - exclude completed items that match active items by path
const allTools = useMemo(() => {
// Get paths of active items
const activePaths = new Set(activeTools.map((item) => item.parsedTool.path).filter(Boolean))
// Filter out completed items that are also being actively read
const dedupedCompleted = completedTools.filter((item) => !activePaths.has(item.parsedTool.path))
return [...dedupedCompleted, ...activeTools]
}, [completedTools, activeTools])
const summary = getToolGroupSummary(filteredMessages)
@@ -46,19 +156,19 @@ export const ToolGroupRenderer = memo(({ messages, allMessages }: ToolGroupRende
setExpandedItems((prev) => ({ ...prev, [ts]: !prev[ts] }))
}, [])
// Don't render if no PAST tools to show
if (toolsWithReasoning.length === 0) {
// Don't render if no tools to show
if (allTools.length === 0) {
return null
}
return (
<div className={cn("px-4 py-2 text-description")}>
<div className={cn("px-4 py-2 ml-1 text-description")}>
{/* Header */}
<div className="text-[13px] opacity-90 mb-1">{summary}:</div>
<div className="text-[13px] text-foreground mb-1">{summary}:</div>
{/* Content - files/folders with reasoning in tooltip */}
{/* Content - unified list of completed + active tools */}
<div className="min-w-0">
{toolsWithReasoning.map(({ tool, parsedTool, reasoning }) => {
{allTools.map(({ tool, parsedTool, isActive, activityText }) => {
const info = getToolDisplayInfo(parsedTool)
if (!info) {
return null
@@ -67,32 +177,46 @@ export const ToolGroupRenderer = memo(({ messages, allMessages }: ToolGroupRende
const isExpandable = EXPANDABLE_TOOLS.has(parsedTool.tool)
const isItemExpanded = expandedItems[tool.ts] ?? false
const content = parsedTool.content || null
const hasReasoning = !!reasoning?.length
// Active items render with "Reading..." TypewriterText (match completed item structure exactly)
if (isActive && activityText) {
return (
<div className="min-w-0" key={tool.ts}>
{/* ACTIVE "READING..." ITEM STYLING - Modify vertical spacing here via py-0 and -my-0.5 */}
<Button
className="flex items-center gap-[3px] text-[13px] text-description py-[1px] min-w-0 max-w-full px-0 leading-tight -my-0.5"
disabled
size="icon"
variant="text">
<info.icon className="opacity-70 shrink-0 size-[12px]" />
<span className="flex-1 min-w-0 whitespace-nowrap overflow-hidden text-ellipsis text-left text-[13px]">
<TypewriterText speed={15} text={activityText} />
</span>{" "}
</Button>
</div>
)
}
// Completed items render normally (clickable)
return (
<div className="min-w-0" key={tool.ts}>
<Tooltip>
<TooltipTrigger asChild>
<Button
className="flex items-center gap-1.5 cursor-pointer text-[13px] text-description py-0.5 hover:text-link min-w-0 max-w-full px-0"
onClick={() => (isExpandable ? handleItemToggle(tool.ts) : handleOpenFile(info.path))}
size="icon"
variant="text">
<info.icon className="opacity-70 shrink-0 size-[13px]" />
<span
className={cn(
"flex-1 min-w-0 whitespace-nowrap overflow-hidden text-ellipsis text-left [direction:rtl] text-[13px]",
{
"[direction:ltr]": !!info.displayText,
},
)}>
{(info.displayText || cleanPathPrefix(info.path)) + "\u200E"}
</span>
</Button>
</TooltipTrigger>
{hasReasoning && <TooltipContent side="bottom">{reasoning}</TooltipContent>}
</Tooltip>
{/* Expanded content for folders/search/definitions - raw text */}
<Button
className="flex items-center gap-[3px] cursor-pointer text-[13px] text-description py-[1px] hover:text-link min-w-0 max-w-full px-0 leading-tight -my-0.5"
onClick={() => (isExpandable ? handleItemToggle(tool.ts) : handleOpenFile(info.path))}
size="icon"
variant="text">
<info.icon className="opacity-70 shrink-0 size-[12px]" />
<span
className={cn(
"flex-1 min-w-0 whitespace-nowrap overflow-hidden text-ellipsis text-left [direction:rtl] text-[13px]",
{
"[direction:ltr]": !!info.displayText,
},
)}>
{(info.displayText || cleanPathPrefix(info.path)) + "\u200E"}
</span>
</Button>
{/* Expanded content for folders/search/definitions - file lists only */}
{isExpandable && isItemExpanded && content && (
<pre className="m-1 ml-4 text-xs opacity-80 whitespace-pre-wrap break-words p-2 max-h-40 overflow-auto rounded-xs">
{content}
@@ -107,24 +231,25 @@ export const ToolGroupRenderer = memo(({ messages, allMessages }: ToolGroupRende
})
/**
* Build tool items with associated reasoning (reasoning that comes BEFORE a tool).
* Only processes low-stakes tools, accumulating reasoning messages along the way.
* Build tool items WITHOUT reasoning.
* Reasoning should not be displayed in file lists - only file/folder content.
*/
function buildToolsWithReasoning(messages: ClineMessage[]): ToolWithReasoning[] {
const result: ToolWithReasoning[] = []
const reasoningBuffer: string[] = []
for (const msg of messages) {
if (msg.say === "reasoning" && msg.text) {
reasoningBuffer.push(msg.text)
} else if (isLowStakesTool(msg)) {
// Skip reasoning messages - they should not be in file lists
if (msg.say === "reasoning") {
continue
}
if (isLowStakesTool(msg)) {
const parsedTool = parseToolSafe(msg.text)
result.push({
tool: msg,
parsedTool,
reasoning: reasoningBuffer.length > 0 ? reasoningBuffer.join("\n\n") : undefined,
reasoning: undefined, // Never show reasoning in file lists
})
reasoningBuffer.length = 0
}
}
@@ -126,7 +126,7 @@ export function groupMessages(visibleMessages: ClineMessage[]): (ClineMessage |
}
}
visibleMessages.forEach((message) => {
for (const message of visibleMessages) {
if (message.ask === "browser_action_launch" || message.say === "browser_action_launch") {
// complete existing browser session if any
endBrowserSession()
@@ -144,7 +144,7 @@ export function groupMessages(visibleMessages: ClineMessage[]): (ClineMessage |
if (isCancelled) {
endBrowserSession()
result.push(message)
return
continue
}
}
}
@@ -167,7 +167,7 @@ export function groupMessages(visibleMessages: ClineMessage[]): (ClineMessage |
} else {
result.push(message)
}
})
}
// Handle case where browser session is the last group
if (currentGroup.length > 0) {
@@ -614,14 +614,21 @@ export function isApiReqAbsorbable(apiReqTs: number, allMessages: ClineMessage[]
}
let hasLowStakesTool = false
let hasReasoning = false
for (let i = apiReqIndex + 1; i < allMessages.length; i++) {
const msg = allMessages[i]
if (msg.say === "api_req_started") {
break
}
// Reasoning and checkpoints do not affect absorbability
if (msg.say === "reasoning" || msg.say === "checkpoint_created") {
// Reasoning - mark it but don't absorb if present
if (msg.say === "reasoning") {
hasReasoning = true
continue
}
// Checkpoints do not affect absorbability
if (msg.say === "checkpoint_created") {
continue
}
@@ -642,17 +649,19 @@ export function isApiReqAbsorbable(apiReqTs: number, allMessages: ClineMessage[]
}
}
return hasLowStakesTool
// Don't absorb if there's reasoning - we want to show "Thoughts >"
return hasLowStakesTool && !hasReasoning
}
/**
* Check if an api_req_started at a given index produces low-stakes tools
* (regardless of whether it also produces text).
* If so, it should be absorbed into the tool group rather than rendered separately.
* The key is: no HIGH-stakes tools (write, edit, command, etc.)
* The key is: no HIGH-stakes tools (write, edit, command, etc.) AND no reasoning
*/
function isApiReqFollowedOnlyByLowStakesTools(index: number, messages: (ClineMessage | ClineMessage[])[]): boolean {
let hasLowStakesTool = false
let hasReasoning = false
for (let i = index + 1; i < messages.length; i++) {
const item = messages[i]
if (Array.isArray(item)) {
@@ -664,8 +673,9 @@ function isApiReqFollowedOnlyByLowStakesTools(index: number, messages: (ClineMes
if (msg.say === "api_req_started") {
break
}
// Reasoning is allowed
// Reasoning - mark it but don't absorb if present
if (msg.say === "reasoning") {
hasReasoning = true
continue
}
// Low-stakes tool - mark it
@@ -686,7 +696,8 @@ function isApiReqFollowedOnlyByLowStakesTools(index: number, messages: (ClineMes
return false
}
}
return hasLowStakesTool
// Don't absorb if there's reasoning - we want to show "Thoughts >"
return hasLowStakesTool && !hasReasoning
}
/**
@@ -111,6 +111,7 @@ export const CheckmarkControl = ({ messageTs, isCheckpointCheckedOut }: Checkmar
)
} catch (err) {
console.error("Checkpoint restore task error:", err)
} finally {
setRestoreTaskDisabled(false)
}
}
@@ -127,6 +128,7 @@ export const CheckmarkControl = ({ messageTs, isCheckpointCheckedOut }: Checkmar
)
} catch (err) {
console.error("Checkpoint restore workspace error:", err)
} finally {
setRestoreWorkspaceDisabled(false)
}
}
@@ -143,6 +145,7 @@ export const CheckmarkControl = ({ messageTs, isCheckpointCheckedOut }: Checkmar
)
} catch (err) {
console.error("Checkpoint restore both error:", err)
} finally {
setRestoreBothDisabled(false)
}
}
@@ -295,7 +298,7 @@ export const CheckmarkControl = ({ messageTs, isCheckpointCheckedOut }: Checkmar
const Container = styled.div<{ isMenuOpen?: boolean; $isCheckedOut?: boolean }>`
display: flex;
align-items: center;
padding: 4px 0;
padding: 8px 0px 0px 0px;
gap: 4px;
position: relative;
min-width: 0;
@@ -304,6 +307,11 @@ const Container = styled.div<{ isMenuOpen?: boolean; $isCheckedOut?: boolean }>`
margin-bottom: 1px;
opacity: ${(props) => (props.$isCheckedOut ? 1 : props.isMenuOpen ? 1 : 0.5)};
height: 0.5rem;
&:first-of-type {
padding-top: 0px;
}
&:hover {
opacity: 1;
}
@@ -1,3 +1,4 @@
import { ChevronDownIcon, ChevronRightIcon } from "lucide-react"
import { memo, useMemo } from "react"
import CodeBlock from "@/components/common/CodeBlock"
import { cn } from "@/lib/utils"
@@ -88,7 +89,7 @@ const CodeAccordian = ({
<span>{numberOfEdits}</span>
</div>
)}
<span className={`codicon codicon-chevron-${isExpanded ? "up" : "down"}`} />
{isExpanded ? <ChevronDownIcon className="size-4" /> : <ChevronRightIcon className="size-4" />}
</Button>
)}
{(!(path || isFeedback || isConsoleLogs) || isExpanded) && (
@@ -16,7 +16,13 @@ interface WhatsNewModalProps {
export const WhatsNewModal: React.FC<WhatsNewModalProps> = ({ open, onClose, version }) => {
const { clineUser } = useClineAuth()
const { openRouterModels, setShowChatModelSelector, refreshOpenRouterModels, navigateToSettings } = useExtensionState()
const {
openRouterModels,
setShowChatModelSelector,
refreshOpenRouterModels,
navigateToSettings,
navigateToSettingsModelPicker,
} = useExtensionState()
const { handleFieldsChange } = useApiConfigurationHandlers()
const clickedModelsRef = useRef<Set<string>>(new Set())
@@ -42,6 +48,19 @@ export const WhatsNewModal: React.FC<WhatsNewModalProps> = ({ open, onClose, ver
[handleFieldsChange, openRouterModels, setShowChatModelSelector, onClose],
)
const navigateToModelPicker = useCallback(
(initialModelTab: "recommended" | "free") => {
// Switch to Cline provider first so the model picker tab works
handleFieldsChange({
planModeApiProvider: "cline",
actModeApiProvider: "cline",
})
onClose()
navigateToSettingsModelPicker({ targetSection: "api-config", initialModelTab })
},
[handleFieldsChange, navigateToSettingsModelPicker, onClose],
)
const setOpenAiCodexProvider = useCallback(() => {
handleFieldsChange({
planModeApiProvider: "openai-codex",
@@ -79,6 +98,35 @@ export const WhatsNewModal: React.FC<WhatsNewModalProps> = ({ open, onClose, ver
</Button>
)
type InlineModelLinkProps =
| { type: "model"; modelId: string; label: string }
| { type: "picker"; pickerTab: "recommended" | "free"; label: string }
const InlineModelLink: React.FC<InlineModelLinkProps> = (props) => {
if (props.type === "picker") {
return (
<span
onClick={() => navigateToModelPicker(props.pickerTab)}
style={{ color: "var(--vscode-textLink-foreground)", cursor: "pointer" }}>
{props.label}
</span>
)
}
const isClicked = clickedModelsRef.current.has(props.modelId)
if (isClicked) {
return null
}
return (
<span
onClick={() => setModel(props.modelId)}
style={{ color: "var(--vscode-textLink-foreground)", cursor: "pointer" }}>
{props.label}
</span>
)
}
return (
<Dialog onOpenChange={(isOpen) => !isOpen && onClose()} open={open}>
<DialogContent
@@ -96,25 +144,28 @@ export const WhatsNewModal: React.FC<WhatsNewModalProps> = ({ open, onClose, ver
{/* Description */}
<ul className="text-sm pl-3 list-disc" style={{ color: "var(--vscode-descriptionForeground)" }}>
<li className="mb-2">
<strong>OpenAI ChatGPT Subscription Integration:</strong> Use your ChatGPT subscription directly in
Cline with no additional token cost and no api keys to manage.{" "}
<strong>New free model: Arcee Trinity Large:</strong> strong coding performance with an open-weight
model.{" "}
<InlineModelLink label="Try free" modelId="cline:arcee-ai/trinity-large-preview:free" type="model" />
</li>
<li className="mb-2">
<strong>Try Kimi K2.5:</strong> Moonshot's latest with advanced reasoning for complex, multi-step
coding tasks. Great for front-end tasks.{" "}
<InlineModelLink label="Try now" modelId="cline:moonshotai/kimi-k2.5" type="model" />
</li>
<li className="mb-2">
<strong>Bring your ChatGPT subscription to Cline!</strong> Use your existing plan directly with no per
token costs or API keys to manage.{" "}
<span
onClick={setOpenAiCodexProvider}
style={{ color: "var(--vscode-textLink-foreground)", cursor: "pointer" }}>
Sign in
Connect
</span>
</li>
<li className="mb-2">
<strong>Jupyter Notebooks:</strong> Comprehensive AI-assisted editing of <code>.ipynb</code> files
with full cell-level context awareness.{" "}
<a
href="https://docs.cline.bot/features/jupyter-notebooks"
style={{ color: "var(--vscode-textLink-foreground)" }}>
Learn More
</a>
</li>
<li>
<strong>Grok Code Fast 1: </strong> is no longer free to use.
<strong>Grok Code Fast 1 & Devstral are saying goodbye (to free):</strong> free promotion is done but
there are plenty models in our free tier.{" "}
<InlineModelLink label="See alternatives" pickerTab="free" type="picker" />
</li>
</ul>
</div>
@@ -1,5 +1,6 @@
import { McpDisplayMode } from "@shared/McpDisplayMode"
import { VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
import { ChevronDownIcon, ChevronRightIcon } from "lucide-react"
import React, { useCallback, useEffect, useState } from "react"
import styled from "styled-components"
import ChatErrorBoundary from "@/components/chat/ChatErrorBoundary"
@@ -223,7 +224,11 @@ const McpResponseDisplay: React.FC<McpResponseDisplayProps> = ({ responseText })
marginBottom: isExpanded ? "8px" : "0px",
}}>
<div className="header-title">
<span className={`codicon codicon-chevron-${isExpanded ? "down" : "right"} header-icon`}></span>
{isExpanded ? (
<ChevronDownIcon className="header-icon" size={16} />
) : (
<ChevronRightIcon className="header-icon" size={16} />
)}
Response
</div>
<DropdownContainer
@@ -247,7 +252,11 @@ const McpResponseDisplay: React.FC<McpResponseDisplayProps> = ({ responseText })
<ResponseContainer>
<ResponseHeader onClick={toggleExpand}>
<div className="header-title">
<span className={`codicon codicon-chevron-${isExpanded ? "down" : "right"} header-icon`}></span>
{isExpanded ? (
<ChevronDownIcon className="header-icon" size={16} />
) : (
<ChevronRightIcon className="header-icon" size={16} />
)}
Response (Error)
</div>
</ResponseHeader>
@@ -0,0 +1,94 @@
import { McpPrompt } from "@shared/mcp"
type McpPromptRowProps = {
prompt: McpPrompt
serverName?: string
}
const McpPromptRow = ({ prompt, serverName }: McpPromptRowProps) => {
return (
<div
key={prompt.name}
style={{
padding: "3px 0",
}}>
<div
data-testid="prompt-row-container"
onClick={(e) => e.stopPropagation()}
style={{ display: "flex", alignItems: "center", justifyContent: "space-between", flexWrap: "wrap", gap: "4px" }}>
<div style={{ display: "flex", alignItems: "center", minWidth: 0, flex: "1 1 auto" }}>
<span className="codicon codicon-comment-discussion" style={{ marginRight: "6px", flexShrink: 0 }}></span>
<span style={{ fontWeight: 500, overflow: "hidden", textOverflow: "ellipsis" }}>
{prompt.title || prompt.name}
</span>
</div>
</div>
{prompt.description && (
<div
style={{
marginLeft: "0px",
marginTop: "4px",
opacity: 0.8,
fontSize: "12px",
}}>
{prompt.description}
</div>
)}
{prompt.arguments && prompt.arguments.length > 0 && (
<div
style={{
marginTop: "8px",
fontSize: "12px",
border: "1px solid color-mix(in srgb, var(--vscode-descriptionForeground) 30%, transparent)",
borderRadius: "3px",
padding: "8px",
}}>
<div
style={{
marginBottom: "4px",
opacity: 0.8,
fontSize: "11px",
textTransform: "uppercase",
}}>
Arguments
</div>
{prompt.arguments.map((arg) => (
<div
key={arg.name}
style={{
display: "flex",
alignItems: "baseline",
marginTop: "4px",
}}>
<code
style={{
color: "var(--vscode-textPreformat-foreground)",
marginRight: "8px",
}}>
{arg.name}
{arg.required && (
<span
style={{
color: "var(--vscode-errorForeground)",
}}>
*
</span>
)}
</code>
<span
style={{
opacity: 0.8,
overflowWrap: "break-word",
wordBreak: "break-word",
}}>
{arg.description || "No description"}
</span>
</div>
))}
</div>
)}
</div>
)
}
export default McpPromptRow
@@ -24,6 +24,7 @@ import { useExtensionState } from "@/context/ExtensionStateContext"
import { cn } from "@/lib/utils"
import { McpServiceClient } from "@/services/grpc-client"
import { getMcpServerDisplayName } from "@/utils/mcp"
import McpPromptRow from "./McpPromptRow"
import McpResourceRow from "./McpResourceRow"
import McpToolRow from "./McpToolRow"
@@ -317,6 +318,7 @@ const ServerRow = ({
<VSCodePanelTab id="resources">
Resources ({[...(server.resourceTemplates || []), ...(server.resources || [])].length || 0})
</VSCodePanelTab>
<VSCodePanelTab id="prompts">Prompts ({server.prompts?.length || 0})</VSCodePanelTab>
<VSCodePanelView id="tools-view">
{server.tools && server.tools.length > 0 ? (
@@ -354,6 +356,31 @@ const ServerRow = ({
<div className="py-2.5 text-description">No resources found</div>
)}
</VSCodePanelView>
<VSCodePanelView id="prompts-view">
{server.prompts && server.prompts.length > 0 ? (
<div
style={{
display: "flex",
flexDirection: "column",
gap: "8px",
width: "100%",
paddingTop: "8px",
}}>
{server.prompts.map((prompt) => (
<McpPromptRow key={prompt.name} prompt={prompt} serverName={server.name} />
))}
</div>
) : (
<div
style={{
padding: "10px 0",
color: "var(--vscode-descriptionForeground)",
}}>
No prompts found
</div>
)}
</VSCodePanelView>
</VSCodePanels>
<div className="my-2.5 mx-1.5">
@@ -61,6 +61,7 @@ interface ApiOptionsProps {
modelIdErrorMessage?: string
isPopup?: boolean
currentMode: Mode
initialModelTab?: "recommended" | "free"
}
// This is necessary to ensure dropdown opens downward, important for when this is used in popup
@@ -87,7 +88,14 @@ declare module "vscode" {
}
}
const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, isPopup, currentMode }: ApiOptionsProps) => {
const ApiOptions = ({
showModelOptions,
apiErrorMessage,
modelIdErrorMessage,
isPopup,
currentMode,
initialModelTab,
}: ApiOptionsProps) => {
// Use full context state for immediate save payload
const { apiConfiguration, remoteConfigSettings } = useExtensionState()
@@ -349,7 +357,12 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
)}
{apiConfiguration && selectedProvider === "cline" && (
<ClineProvider currentMode={currentMode} isPopup={isPopup} showModelOptions={showModelOptions} />
<ClineProvider
currentMode={currentMode}
initialModelTab={initialModelTab}
isPopup={isPopup}
showModelOptions={showModelOptions}
/>
)}
{apiConfiguration && selectedProvider === "asksage" && (
@@ -43,6 +43,7 @@ export interface OpenRouterModelPickerProps {
isPopup?: boolean
currentMode: Mode
showProviderRouting?: boolean
initialTab?: "recommended" | "free"
}
// Featured models for Cline provider organized by tabs
@@ -81,15 +82,20 @@ export const freeModels = [
label: "FREE",
},
{
id: "mistralai/devstral-2512:free",
description: "Mistral's latest model with strong coding abilities",
id: "arcee-ai/trinity-large-preview:free",
description: "Arcee AI's advanced large preview model in the Trinity series",
label: "FREE",
},
]
const FREE_CLINE_MODELS = freeModels.map((m) => m.id)
const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup, currentMode, showProviderRouting }) => {
const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({
isPopup,
currentMode,
showProviderRouting,
initialTab,
}) => {
const { handleModeFieldChange, handleModeFieldsChange, handleFieldChange } = useApiConfigurationHandlers()
const { apiConfiguration, favoritedModelIds, openRouterModels, refreshOpenRouterModels } = useExtensionState()
const modeFields = getModeSpecificFields(apiConfiguration, currentMode)
@@ -97,9 +103,19 @@ const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup,
const [isDropdownVisible, setIsDropdownVisible] = useState(false)
const [selectedIndex, setSelectedIndex] = useState(-1)
const [activeTab, setActiveTab] = useState<"recommended" | "free">(() => {
if (initialTab) {
return initialTab
}
const currentModelId = modeFields.openRouterModelId || openRouterDefaultModelId
return freeModels.some((m) => m.id === currentModelId) ? "free" : "recommended"
})
// If a caller wants to deep-link to the Free tab (or Recommended), honor that.
useEffect(() => {
if (initialTab) {
setActiveTab(initialTab)
}
}, [initialTab])
const dropdownRef = useRef<HTMLDivElement>(null)
const itemRefs = useRef<(HTMLDivElement | null)[]>([])
const dropdownListRef = useRef<HTMLDivElement>(null)
@@ -131,7 +131,7 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
[],
) // Empty deps - these imports never change
const { version, environment } = useExtensionState()
const { version, environment, settingsInitialModelTab } = useExtensionState()
const [activeTab, setActiveTab] = useState<string>(targetSection || SETTINGS_TABS[0].id)
@@ -233,10 +233,12 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
props.onResetState = handleResetState
} else if (activeTab === "about") {
props.version = version
} else if (activeTab === "api-config") {
props.initialModelTab = settingsInitialModelTab
}
return <Component {...props} />
}, [activeTab, handleResetState, version])
}, [activeTab, handleResetState, settingsInitialModelTab, version])
const titleColor = getEnvironmentColor(environment)
@@ -9,12 +9,13 @@ interface ClineProviderProps {
showModelOptions: boolean
isPopup?: boolean
currentMode: Mode
initialModelTab?: "recommended" | "free"
}
/**
* The Cline provider configuration component
*/
export const ClineProvider = ({ showModelOptions, isPopup, currentMode }: ClineProviderProps) => {
export const ClineProvider = ({ showModelOptions, isPopup, currentMode, initialModelTab }: ClineProviderProps) => {
return (
<div>
{/* Cline Account Info Card */}
@@ -25,7 +26,12 @@ export const ClineProvider = ({ showModelOptions, isPopup, currentMode }: ClineP
{showModelOptions && (
<>
{/* OpenRouter Model Picker - includes Provider Routing in Advanced section */}
<OpenRouterModelPicker currentMode={currentMode} isPopup={isPopup} showProviderRouting={true} />
<OpenRouterModelPicker
currentMode={currentMode}
initialTab={initialModelTab}
isPopup={isPopup}
showProviderRouting={true}
/>
</>
)}
</div>
@@ -12,9 +12,10 @@ import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandler
interface ApiConfigurationSectionProps {
renderSectionHeader?: (tabId: string) => JSX.Element | null
initialModelTab?: "recommended" | "free"
}
const ApiConfigurationSection = ({ renderSectionHeader }: ApiConfigurationSectionProps) => {
const ApiConfigurationSection = ({ renderSectionHeader, initialModelTab }: ApiConfigurationSectionProps) => {
const { planActSeparateModelsSetting, mode, apiConfiguration } = useExtensionState()
const [currentTab, setCurrentTab] = useState<Mode>(mode)
const { handleFieldsChange } = useApiConfigurationHandlers()
@@ -50,11 +51,11 @@ const ApiConfigurationSection = ({ renderSectionHeader }: ApiConfigurationSectio
{/* Content container */}
<div className="-mb-3">
<ApiOptions currentMode={currentTab} showModelOptions={true} />
<ApiOptions currentMode={currentTab} initialModelTab={initialModelTab} showModelOptions={true} />
</div>
</div>
) : (
<ApiOptions currentMode={mode} showModelOptions={true} />
<ApiOptions currentMode={mode} initialModelTab={initialModelTab} showModelOptions={true} />
)}
<div className="mb-[5px]">
@@ -824,7 +824,7 @@ export function filterOpenRouterModelIds(modelIds: string[], provider: ApiProvid
// For Cline provider: exclude :free models, but keep Minimax models
return modelIds.filter((id) => {
// Keep all Minimax and devstral models regardless of :free suffix
if (id.toLowerCase().includes("minimax-m2") || id.toLowerCase().includes("devstral-2512")) {
if (id.toLowerCase().includes("minimax-m2") || id.toLowerCase().includes("arcee-ai/trinity-large")) {
return true
}
// Filter out other :free models
@@ -54,6 +54,7 @@ export interface ExtensionStateContextType extends ExtensionState {
mcpTab?: McpViewTab
showSettings: boolean
settingsTargetSection?: string
settingsInitialModelTab?: "recommended" | "free"
showHistory: boolean
showAccount: boolean
showWorktrees: boolean
@@ -102,6 +103,7 @@ export interface ExtensionStateContextType extends ExtensionState {
// Navigation functions
navigateToMcp: (tab?: McpViewTab) => void
navigateToSettings: (targetSection?: string) => void
navigateToSettingsModelPicker: (opts: { targetSection?: string; initialModelTab?: "recommended" | "free" }) => void
navigateToHistory: () => void
navigateToAccount: () => void
navigateToWorktrees: () => void
@@ -130,6 +132,7 @@ export const ExtensionStateContextProvider: React.FC<{
const [mcpTab, setMcpTab] = useState<McpViewTab | undefined>(undefined)
const [showSettings, setShowSettings] = useState(false)
const [settingsTargetSection, setSettingsTargetSection] = useState<string | undefined>(undefined)
const [settingsInitialModelTab, setSettingsInitialModelTab] = useState<"recommended" | "free" | undefined>(undefined)
const [showHistory, setShowHistory] = useState(false)
const [showAccount, setShowAccount] = useState(false)
const [showWorktrees, setShowWorktrees] = useState(false)
@@ -146,6 +149,7 @@ export const ExtensionStateContextProvider: React.FC<{
const hideSettings = useCallback(() => {
setShowSettings(false)
setSettingsTargetSection(undefined)
setSettingsInitialModelTab(undefined)
}, [])
const hideHistory = useCallback(() => setShowHistory(false), [setShowHistory])
const hideAccount = useCallback(() => setShowAccount(false), [setShowAccount])
@@ -175,6 +179,20 @@ export const ExtensionStateContextProvider: React.FC<{
setShowAccount(false)
setShowWorktrees(false)
setSettingsTargetSection(targetSection)
setSettingsInitialModelTab(undefined)
setShowSettings(true)
},
[closeMcpView],
)
const navigateToSettingsModelPicker = useCallback(
(opts: { targetSection?: string; initialModelTab?: "recommended" | "free" }) => {
setShowHistory(false)
closeMcpView()
setShowAccount(false)
setShowWorktrees(false)
setSettingsTargetSection(opts.targetSection)
setSettingsInitialModelTab(opts.initialModelTab)
setShowSettings(true)
},
[closeMcpView],
@@ -773,6 +791,7 @@ export const ExtensionStateContextProvider: React.FC<{
mcpTab,
showSettings,
settingsTargetSection,
settingsInitialModelTab,
showHistory,
showAccount,
showWorktrees,
@@ -793,6 +812,7 @@ export const ExtensionStateContextProvider: React.FC<{
// Navigation functions
navigateToMcp,
navigateToSettings,
navigateToSettingsModelPicker,
navigateToHistory,
navigateToAccount,
navigateToWorktrees,
@@ -0,0 +1,228 @@
import type { McpServer } from "@shared/mcp"
import { describe, expect, it } from "vitest"
import { getMatchingSlashCommands, getMcpPromptCommands, slashCommandRegex, validateSlashCommand } from "../slash-commands"
// Helper to create a mock MCP server
function createMockMcpServer(overrides: Partial<McpServer> = {}): McpServer {
return {
name: "test-server",
status: "connected",
config: "{}",
prompts: [],
tools: [],
resources: [],
resourceTemplates: [],
...overrides,
}
}
describe("slash-commands", () => {
describe("getMcpPromptCommands", () => {
it("should return empty array when no servers provided", () => {
const result = getMcpPromptCommands([])
expect(result).toEqual([])
})
it("should return empty array when servers have no prompts", () => {
const servers = [createMockMcpServer({ prompts: [] })]
const result = getMcpPromptCommands(servers)
expect(result).toEqual([])
})
it("should skip disconnected servers", () => {
const servers = [
createMockMcpServer({
status: "disconnected",
prompts: [{ name: "test-prompt", description: "A test prompt" }],
}),
]
const result = getMcpPromptCommands(servers)
expect(result).toEqual([])
})
it("should skip servers with connecting status", () => {
const servers = [
createMockMcpServer({
status: "connecting",
prompts: [{ name: "test-prompt", description: "A test prompt" }],
}),
]
const result = getMcpPromptCommands(servers)
expect(result).toEqual([])
})
it("should generate commands for connected servers with prompts", () => {
const servers = [
createMockMcpServer({
name: "my-server",
prompts: [{ name: "summarize", description: "Summarize text" }],
}),
]
const result = getMcpPromptCommands(servers)
expect(result).toEqual([
{
name: "mcp:my-server:summarize",
description: "Summarize text",
section: "mcp",
},
])
})
it("should use title as fallback description", () => {
const servers = [
createMockMcpServer({
name: "server",
prompts: [{ name: "prompt", title: "My Prompt Title" }],
}),
]
const result = getMcpPromptCommands(servers)
expect(result[0].description).toBe("My Prompt Title")
})
it("should use default description when no description or title", () => {
const servers = [
createMockMcpServer({
name: "server",
prompts: [{ name: "prompt" }],
}),
]
const result = getMcpPromptCommands(servers)
expect(result[0].description).toBe("MCP prompt from server")
})
it("should handle multiple prompts from single server", () => {
const servers = [
createMockMcpServer({
name: "multi-server",
prompts: [
{ name: "prompt1", description: "First prompt" },
{ name: "prompt2", description: "Second prompt" },
{ name: "prompt3", description: "Third prompt" },
],
}),
]
const result = getMcpPromptCommands(servers)
expect(result).toHaveLength(3)
expect(result.map((c) => c.name)).toEqual([
"mcp:multi-server:prompt1",
"mcp:multi-server:prompt2",
"mcp:multi-server:prompt3",
])
})
it("should handle multiple servers with prompts", () => {
const servers = [
createMockMcpServer({
name: "server-a",
prompts: [{ name: "promptA", description: "From A" }],
}),
createMockMcpServer({
name: "server-b",
prompts: [{ name: "promptB", description: "From B" }],
}),
]
const result = getMcpPromptCommands(servers)
expect(result).toHaveLength(2)
expect(result[0].name).toBe("mcp:server-a:promptA")
expect(result[1].name).toBe("mcp:server-b:promptB")
})
it("should skip servers with undefined prompts", () => {
const servers = [
createMockMcpServer({
name: "server",
prompts: undefined,
}),
]
const result = getMcpPromptCommands(servers)
expect(result).toEqual([])
})
})
describe("getMatchingSlashCommands with MCP servers", () => {
const mcpServers = [
createMockMcpServer({
name: "test-server",
prompts: [
{ name: "summarize", description: "Summarize content" },
{ name: "translate", description: "Translate text" },
],
}),
]
it("should include MCP commands in results when no query", () => {
const result = getMatchingSlashCommands("", {}, {}, undefined, undefined, mcpServers)
const mcpCommands = result.filter((cmd) => cmd.section === "mcp")
expect(mcpCommands).toHaveLength(2)
})
it("should filter MCP commands by query prefix", () => {
const result = getMatchingSlashCommands("mcp:test", {}, {}, undefined, undefined, mcpServers)
const mcpCommands = result.filter((cmd) => cmd.section === "mcp")
expect(mcpCommands).toHaveLength(2)
})
it("should filter to specific MCP prompt", () => {
const result = getMatchingSlashCommands("mcp:test-server:sum", {}, {}, undefined, undefined, mcpServers)
expect(result).toHaveLength(1)
expect(result[0].name).toBe("mcp:test-server:summarize")
})
it("should return empty for non-matching MCP query", () => {
const result = getMatchingSlashCommands("mcp:nonexistent", {}, {}, undefined, undefined, mcpServers)
expect(result).toHaveLength(0)
})
})
describe("validateSlashCommand with MCP servers", () => {
const mcpServers = [
createMockMcpServer({
name: "server",
prompts: [{ name: "prompt", description: "Test" }],
}),
]
it("should return full for exact MCP command match", () => {
const result = validateSlashCommand("mcp:server:prompt", {}, {}, undefined, undefined, mcpServers)
expect(result).toBe("full")
})
it("should return partial for partial MCP command match", () => {
const result = validateSlashCommand("mcp:server:pro", {}, {}, undefined, undefined, mcpServers)
expect(result).toBe("partial")
})
it("should return partial for server prefix only", () => {
const result = validateSlashCommand("mcp:serv", {}, {}, undefined, undefined, mcpServers)
expect(result).toBe("partial")
})
it("should return null for non-matching MCP command", () => {
const result = validateSlashCommand("mcp:unknown:cmd", {}, {}, undefined, undefined, mcpServers)
expect(result).toBe(null)
})
})
describe("slashCommandRegex with MCP format", () => {
it("should match MCP command format with colons", () => {
const text = "/mcp:server:prompt"
const match = text.match(slashCommandRegex)
expect(match).not.toBeNull()
expect(match![2]).toBe("/mcp:server:prompt")
})
it("should match MCP command in middle of text", () => {
const text = "Please run /mcp:server:prompt now"
const match = text.match(slashCommandRegex)
expect(match).not.toBeNull()
expect(match![2]).toBe("/mcp:server:prompt")
})
it("should not match MCP-like pattern in URL", () => {
const text = "http://example.com/mcp:test"
const match = text.match(slashCommandRegex)
// Should not match because / is not preceded by whitespace or start
expect(match).toBeNull()
})
})
})
+38 -5
View File
@@ -1,6 +1,9 @@
import type { McpServer } from "@shared/mcp"
import { PLATFORM_CONFIG, PlatformType } from "@/config/platform.config"
import { BASE_SLASH_COMMANDS, type SlashCommand, VSCODE_ONLY_COMMANDS } from "../../../src/shared/slashCommands.ts"
export type { SlashCommand }
export const DEFAULT_SLASH_COMMANDS: SlashCommand[] =
PLATFORM_CONFIG.type === PlatformType.VSCODE ? [...BASE_SLASH_COMMANDS, ...VSCODE_ONLY_COMMANDS] : BASE_SLASH_COMMANDS
@@ -67,14 +70,39 @@ export function getWorkflowCommands(
return workflows
}
/**
* Gets MCP prompt commands from connected MCP servers
* Format: mcp:<server-name>:<prompt-name>
*/
export function getMcpPromptCommands(mcpServers: McpServer[] = []): SlashCommand[] {
const commands: SlashCommand[] = []
for (const server of mcpServers) {
if (server.status !== "connected" || !server.prompts) {
continue
}
for (const prompt of server.prompts) {
commands.push({
name: `mcp:${server.name}:${prompt.name}`,
description: prompt.description || prompt.title || `MCP prompt from ${server.name}`,
section: "mcp",
})
}
}
return commands
}
// Regex for detecting slash commands in text
// Must be at start of string OR preceded by whitespace to avoid matching URLs/paths
// e.g., matches "/newtask" or "text /newtask" but not "http://example.com/newtask"
export const slashCommandRegex = /(^|\s)(\/[a-zA-Z0-9_.-]+)(?=\s|$)/
// Note: Colons are allowed to support MCP prompt commands like /mcp:server:prompt
export const slashCommandRegex = /(^|\s)(\/[a-zA-Z0-9_.:@-]+)(?=\s|$)/
export const slashCommandRegexGlobal = new RegExp(slashCommandRegex.source, "g")
// Regex for detecting a slash command at the end of text (for deletion)
// Must be at start OR preceded by whitespace, captures the whole command including slash
export const slashCommandDeleteRegex = /(^|\s)(\/[a-zA-Z0-9_.-]+)$/
export const slashCommandDeleteRegex = /(^|\s)(\/[a-zA-Z0-9_.:@-]+)$/
/**
* Removes a slash command at the cursor position
@@ -133,7 +161,8 @@ export function shouldShowSlashCommandsMenu(text: string, cursorPosition: number
// Check if there's already a valid slash command earlier in the text.
// A valid earlier slash command is one that: starts at beginning or after whitespace,
// and is followed by whitespace (meaning it's complete).
const firstSlashCommandRegex = /(^|\s)\/[a-zA-Z0-9_.-]+\s/
// Note: Colons are allowed to support MCP prompt commands like /mcp:server:prompt
const firstSlashCommandRegex = /(^|\s)\/[a-zA-Z0-9_.:@-]+\s/
const textBeforeCurrentSlash = text.slice(0, slashIndex)
if (firstSlashCommandRegex.test(textBeforeCurrentSlash)) {
return false
@@ -151,6 +180,7 @@ export function getMatchingSlashCommands(
globalWorkflowToggles: Record<string, boolean> = {},
remoteWorkflowToggles?: Record<string, boolean>,
remoteWorkflows?: any[],
mcpServers: McpServer[] = [],
): SlashCommand[] {
const workflowCommands = getWorkflowCommands(
localWorkflowToggles,
@@ -158,7 +188,8 @@ export function getMatchingSlashCommands(
remoteWorkflowToggles,
remoteWorkflows,
)
const allCommands = [...DEFAULT_SLASH_COMMANDS, ...workflowCommands]
const mcpPromptCommands = getMcpPromptCommands(mcpServers)
const allCommands = [...DEFAULT_SLASH_COMMANDS, ...workflowCommands, ...mcpPromptCommands]
if (!query) {
return allCommands
@@ -201,6 +232,7 @@ export function validateSlashCommand(
globalWorkflowToggles: Record<string, boolean> = {},
remoteWorkflowToggles?: Record<string, boolean>,
remoteWorkflows?: any[],
mcpServers: McpServer[] = [],
): "full" | "partial" | null {
if (!command) {
return null
@@ -212,7 +244,8 @@ export function validateSlashCommand(
remoteWorkflowToggles,
remoteWorkflows,
)
const allCommands = [...DEFAULT_SLASH_COMMANDS, ...workflowCommands]
const mcpPromptCommands = getMcpPromptCommands(mcpServers)
const allCommands = [...DEFAULT_SLASH_COMMANDS, ...workflowCommands, ...mcpPromptCommands]
// case insensitive matching
const exactMatch = allCommands.some((cmd) => cmd.name.toLowerCase() === command.toLowerCase())