Compare commits

..

1 Commits

Author SHA1 Message Date
Arafatkatze 9af73709d6 Empty commit for CI test 2025-09-29 18:45:53 -07:00
1783 changed files with 46041 additions and 553934 deletions
-211
View File
@@ -1,211 +0,0 @@
---
name: create-pull-request
description: Create a GitHub pull request following project conventions. Use when the user asks to create a PR, submit changes for review, or open a pull request. Handles commit analysis, branch management, PR template usage, and PR creation using the gh CLI tool.
---
# Create Pull Request
This skill guides you through creating a well-structured GitHub pull request that follows project conventions and best practices.
## Prerequisites Check
Before proceeding, verify the following:
### 1. Check if `gh` CLI is installed
```bash
gh --version
```
If not installed, inform the user:
> The GitHub CLI (`gh`) is required but not installed. Please install it:
> - macOS: `brew install gh`
> - Other: https://cli.github.com/
### 2. Check if authenticated with GitHub
```bash
gh auth status
```
If not authenticated, guide the user to run `gh auth login`.
### 3. Verify clean working directory
```bash
git status
```
If there are uncommitted changes, ask the user whether to:
- Commit them as part of this PR
- Stash them temporarily
- Discard them (with caution)
## Gather Context
### 1. Identify the current branch
```bash
git branch --show-current
```
Ensure you're not on `main` or `master`. If so, ask the user to create or switch to a feature branch.
### 2. Find the base branch
```bash
git remote show origin | grep "HEAD branch"
```
This is typically `main` or `master`.
### 3. Analyze recent commits relevant to this PR
```bash
git log origin/main..HEAD --oneline --no-decorate
```
Review these commits to understand:
- What changes are being introduced
- The scope of the PR (single feature/fix or multiple changes)
- Whether commits should be squashed or reorganized
### 4. Review the diff
```bash
git diff origin/main..HEAD --stat
```
This shows which files changed and helps identify the type of change.
## Information Gathering
Before creating the PR, you need the following information. Check if it can be inferred from:
- Commit messages
- Branch name (e.g., `fix/issue-123`, `feature/new-login`)
- Changed files and their content
If any critical information is missing, use `ask_followup_question` to ask the user:
### Required Information
1. **Related Issue Number**: Look for patterns like `#123`, `fixes #123`, or `closes #123` in commit messages
2. **Description**: What problem does this solve? Why were these changes made?
3. **Type of Change**: Bug fix, new feature, breaking change, refactor, cosmetic, documentation, or workflow
4. **Test Procedure**: How was this tested? What could break?
### Example clarifying question
If the issue number is not found:
> I couldn't find a related issue number in the commit messages or branch name. What GitHub issue does this PR address? (Enter the issue number, e.g., "123" or "N/A" for small fixes)
## Git Best Practices
Before creating the PR, consider these best practices:
### Commit Hygiene
1. **Atomic commits**: Each commit should represent a single logical change
2. **Clear commit messages**: Follow conventional commit format when possible
3. **No merge commits**: Prefer rebasing over merging to keep history clean
### Branch Management
1. **Rebase on latest main** (if needed):
```bash
git fetch origin
git rebase origin/main
```
2. **Squash if appropriate**: If there are many small "WIP" commits, consider interactive rebase:
```bash
git rebase -i origin/main
```
Only suggest this if commits appear messy and the user is comfortable with rebasing.
### Push Changes
Ensure all commits are pushed:
```bash
git push origin HEAD
```
If the branch was rebased, you may need:
```bash
git push origin HEAD --force-with-lease
```
## Create the Pull Request
**IMPORTANT**: Read and use the PR template at `.github/pull_request_template.md`. The PR body format must **strictly match** the template structure. Do not deviate from the template format.
When filling out the template:
- Replace `#XXXX` with the actual issue number, or keep as `#XXXX` if no issue exists (for small fixes)
- Fill in all sections with relevant information gathered from commits and context
- Mark the appropriate "Type of Change" checkbox(es)
- Complete the "Pre-flight Checklist" items that apply
### Create PR with gh CLI
**Use a temporary file for the PR body** to avoid shell escaping issues, newline problems, and other command-line flakiness:
1. Write the PR body to a temporary file:
```
/tmp/pr-body.md
```
2. Create the PR using the file:
```bash
gh pr create --title "PR_TITLE" --body-file /tmp/pr-body.md --base main
```
3. Clean up the temporary file:
```bash
rm /tmp/pr-body.md
```
For draft PRs:
```bash
gh pr create --title "PR_TITLE" --body-file /tmp/pr-body.md --base main --draft
```
**Why use a file?** Passing complex markdown with newlines, special characters, and checkboxes directly via `--body` is error-prone. The `--body-file` flag handles all content reliably.
## Post-Creation
After creating the PR:
1. **Display the PR URL** so the user can review it
2. **Remind about CI checks**: Tests and linting will run automatically
3. **Suggest next steps**:
- Add reviewers if needed: `gh pr edit --add-reviewer USERNAME`
- Add labels if needed: `gh pr edit --add-label "bug"`
## Error Handling
### Common Issues
1. **No commits ahead of main**: The branch has no changes to submit
- Ask if the user meant to work on a different branch
2. **Branch not pushed**: Remote doesn't have the branch
- Push the branch first: `git push -u origin HEAD`
3. **PR already exists**: A PR for this branch already exists
- Show the existing PR: `gh pr view`
- Ask if they want to update it instead
4. **Merge conflicts**: Branch conflicts with base
- Guide user through resolving conflicts or rebasing
## Summary Checklist
Before finalizing, ensure:
- [ ] `gh` CLI is installed and authenticated
- [ ] Working directory is clean
- [ ] All commits are pushed
- [ ] Branch is up-to-date with base branch
- [ ] Related issue number is identified, or placeholder is used
- [ ] PR description follows the template exactly
- [ ] Appropriate type of change is selected
- [ ] Pre-flight checklist items are addressed
+8
View File
@@ -0,0 +1,8 @@
# Changesets
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Enhance Test Workflow and Report Coverage to Qlty on Main
+11
View File
@@ -0,0 +1,11 @@
{
"$schema": "https://unpkg.com/@changesets/config@3.0.5/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "restricted",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
fix: update vercel provider cost note and sign-up url
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Fix repeated API error 400 in SAP AI Core provider
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
fix crashing bug when custom url was incorrect
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Add us-west-1 to Amazon Bedrock regions
+26
View File
@@ -0,0 +1,26 @@
changesDir: .changes
unreleasedDir: unreleased
headerPath: header.tpl.md
changelogPath: CHANGELOG.md
versionExt: md
versionFormat: '## {{.Version}} - {{.Time.Format "2006-01-02"}}'
kindFormat: "### {{.Kind}}"
changeFormat: "* {{.Body}}"
kinds:
- label: Added
auto: minor
- label: Changed
auto: major
- label: Deprecated
auto: minor
- label: Removed
auto: major
- label: Fixed
auto: patch
- label: Security
auto: patch
newlines:
afterChangelogHeader: 1
beforeChangelogVersion: 1
endOfVersion: 1
envPrefix: CHANGIE_
-1
View File
@@ -1 +0,0 @@
../../.clinerules/workflows/hotfix-release.md
-1
View File
@@ -1 +0,0 @@
../../.clinerules/workflows/release.md
@@ -1,51 +0,0 @@
#!/bin/bash
set -euo pipefail
# Only run in Claude Code remote environments
if [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then
exit 0
fi
cd "$CLAUDE_PROJECT_DIR"
echo "=== Claude Code for Web Setup ==="
echo ""
# Install latest gh CLI tool
echo "Installing GitHub CLI..."
GH_VERSION=$(curl -s https://api.github.com/repos/cli/cli/releases/latest | grep '"tag_name"' | cut -d'"' -f4 | sed 's/^v//')
curl -sL "https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_amd64.tar.gz" -o /tmp/gh.tar.gz
tar -xzf /tmp/gh.tar.gz -C /tmp
sudo mv "/tmp/gh_${GH_VERSION}_linux_amd64/bin/gh" /usr/local/bin/gh
rm -rf /tmp/gh.tar.gz /tmp/gh_${GH_VERSION}_linux_amd64
echo "Installed gh version: $(gh --version | head -1)"
echo ""
# Check if GITHUB_TOKEN is set and configure gh
if [ -n "${GITHUB_TOKEN:-}" ]; then
echo "GITHUB_TOKEN is configured - gh CLI is ready to use"
echo ""
echo "You can use gh commands directly, for example:"
echo " gh issue list --repo cline/cline --limit 5"
echo " gh pr list --repo cline/cline --state open"
echo " gh issue view 123 --repo cline/cline"
echo ""
else
echo "GITHUB_TOKEN is not set - gh CLI will have limited functionality"
echo ""
echo "To enable full GitHub API access:"
echo "1. Create a Fine-grained Personal Access Token at https://github.com/settings/tokens?type=beta"
echo "2. Add it as GITHUB_TOKEN in your Claude Code environment settings"
echo ""
fi
# Install project dependencies
echo "Installing dependencies..."
npm run install:all
# Generate gRPC/protobuf types (required for TypeScript)
echo "Generating proto types..."
npm run protos
echo ""
echo "Session setup complete!"
-14
View File
@@ -1,14 +0,0 @@
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/claude-code-for-web-setup.sh"
}
]
}
]
}
}
-33
View File
@@ -1,33 +0,0 @@
# CLI Development
The CLI lives in `cli/` and uses React Ink for terminal UI.
- If needed, look at `cli/src/constants/colors.ts` for re-used terminal colors, e.g. `COLORS.primaryBlue` highlight color (selections, spinners, success states).
- Never use `dimColor` with gray (e.g. `<Text color="gray" dimColor>`) - it's too hard to read. Use `color="gray"` for secondary text and normal foreground (no color) for primary text.
- When thinking about how to handle state or messages from core, look at webview for how it communicates with the vs code extension.
- When updating the webview, consider and suggest to the user to update the CLI TUI since we want to provide a similar experience to our terminal users as we do our vs code extension users.
## Adding New API Providers
When adding a new API provider to the extension, you must also update the CLI:
1. **Update `cli/src/components/ModelPicker.tsx`**: Add the provider to the `providerModels` map so `getDefaultModelId()` returns the correct default model. Import the models and default ID from `@shared/api`:
```typescript
import { newProviderDefaultModelId, newProviderModels } from "@/shared/api"
export const providerModels = {
// ...existing providers
"new-provider": { models: newProviderModels, defaultId: newProviderDefaultModelId },
}
```
2. **Use `applyProviderConfig()` for auth flows**: When implementing OAuth or other auth flows for the provider, use the shared utility at `cli/src/utils/provider-config.ts`:
```typescript
import { applyProviderConfig } from "../utils/provider-config"
// After successful auth:
await applyProviderConfig({ providerId: "new-provider", controller })
```
This handles setting provider, default model, API key mapping, state persistence, and rebuilding the API handler.
3. **Provider-specific auth**: If the provider uses OAuth (like `openai-codex`), add handling in `SettingsPanelContent.tsx`'s `handleProviderSelect` callback. See the existing Codex OAuth flow as a reference.
-205
View File
@@ -1,205 +0,0 @@
This file is the secret sauce for working effectively in this codebase. It captures tribal knowledge—the nuanced, non-obvious patterns that make the difference between a quick fix and hours of back-and-forth & human intervention.
**When to add to this file:**
- User had to intervene, correct, or hand-hold
- Multiple back-and-forth attempts were needed to get something working
- You discovered something that required reading many files to understand
- A change touched files you wouldn't have guessed
- Something worked differently than you expected
- User explicitly asks to "add this to CLAUDE.md"
**Proactively suggest additions** when any of the above happen—don't wait to be asked.
**What NOT to add:** Stuff you can figure out from reading a few files, obvious patterns, or standard practices. This file should be high-signal, not comprehensive.
## Miscellaneous
- This is a VS Code extension—check `package.json` for available scripts before trying to verify builds (e.g., `npm run compile`, not `npm run build`).
- When creating PRs, contributors should not create changelog-entry files. Maintainers handle release versioning and changelog curation during the release process.
- When adding new feature flags, see this PR as a reference https://github.com/cline/cline/pull/7566
- Additional instructions about making requests: @.clinerules/network.md
## gRPC/Protobuf Communication
The extension and webview communicate via gRPC-like protocol over VS Code message passing.
**Proto files live in `proto/`** (e.g., `proto/cline/task.proto`, `proto/cline/ui.proto`)
- Each feature domain has its own `.proto` file
- For simple data, use shared types in `proto/cline/common.proto` (`StringRequest`, `Empty`, `Int64Request`)
- For complex data, define custom messages in the feature's `.proto` file
- Naming: Services `PascalCaseService`, RPCs `camelCase`, Messages `PascalCase`
- For streaming responses, use `stream` keyword (see `subscribeToAuthCallback` in `account.proto`)
**Run `npm run protos`** after any proto changes—generates types in:
- `src/shared/proto/` - Shared type definitions
- `src/generated/grpc-js/` - Service implementations
- `src/generated/nice-grpc/` - Promise-based clients
- `src/generated/hosts/` - Generated handlers
**Adding new enum values** (like a new `ClineSay` type) requires updating conversion mappings in `src/shared/proto-conversions/cline-message.ts`
**Adding new RPC methods** requires:
- Handler in `src/core/controller/<domain>/`
- Call from webview via generated client: `UiServiceClient.scrollToSettings(StringRequest.create({ value: "browser" }))`
**Example—the `explain-changes` feature touched:**
- `proto/cline/task.proto` - Added `ExplainChangesRequest` message and `explainChanges` RPC
- `proto/cline/ui.proto` - Added `GENERATE_EXPLANATION = 29` to `ClineSay` enum
- `src/shared/ExtensionMessage.ts` - Added `ClineSayGenerateExplanation` type
- `src/shared/proto-conversions/cline-message.ts` - Added mapping for new say type
- `src/core/controller/task/explainChanges.ts` - Handler implementation
- `webview-ui/src/components/chat/ChatRow.tsx` - UI rendering
## Adding a New API Provider
When adding a new provider (e.g., "openai-codex"), you must update the proto conversion layer in THREE places or the provider will silently reset to Anthropic:
1. `proto/cline/models.proto` - Add to the `ApiProvider` enum (e.g., `OPENAI_CODEX = 40;`)
2. `convertApiProviderToProto()` in `src/shared/proto-conversions/models/api-configuration-conversion.ts` - Add case mapping string to proto enum
3. `convertProtoToApiProvider()` in the same file - Add case mapping proto enum back to string
**Why this matters:** Without these, the provider string hits the `default` case and returns `ANTHROPIC`. The webview, provider list, and handler all work fine, but the state silently resets when it round-trips through proto serialization. No error is thrown.
**Other files to update when adding a provider:**
- `src/shared/api.ts` - Add to `ApiProvider` union type, define models
- `src/shared/providers/providers.json` - Add to provider list for dropdown
- `src/core/api/index.ts` - Register handler in `createHandlerForProvider()`
- `webview-ui/src/components/settings/utils/providerUtils.ts` - Add cases in `getModelsForProvider()` and `normalizeApiConfiguration()`
- `webview-ui/src/utils/validate.ts` - Add validation case
- `webview-ui/src/components/settings/ApiOptions.tsx` - Render provider component
## Responses API Providers (OpenAI Codex, OpenAI Native)
Providers using OpenAI's Responses API require native tool calling. XML tools don't work with the Responses API.
**Symptoms of broken native tool calling:**
- Tools get called multiple times (e.g., `ask_followup_question` asks the same question twice)
- Tool arguments get duplicated or malformed
- The model responds but tools aren't recognized
**Root causes to check:**
1. **Provider missing from `isNextGenModelProvider()`** in `src/utils/model-utils.ts`. The native variant matchers (e.g., `native-gpt-5/config.ts`) call this function. If your provider isn't in the list, the matcher returns false and falls back to XML tools.
2. **Model missing `apiFormat: ApiFormat.OPENAI_RESPONSES`** in its model info (`src/shared/api.ts`). This property signals that the model requires native tool calling. The task runner in `src/core/task/index.ts` checks this and forces `enableNativeToolCalls: true` regardless of user settings.
**When adding a new Responses API provider:**
1. Add provider to `isNextGenModelProvider()` list in `src/utils/model-utils.ts`
2. Set `apiFormat: ApiFormat.OPENAI_RESPONSES` on all models that use the Responses API
3. The variant matcher and task runner will handle the rest automatically
## Adding Tools to System Prompt
This is tricky—multiple prompt variants and configs. **Always search for existing similar tools first and follow their pattern.** Look at the full chain from prompt definition → variant configs → handler → UI before implementing.
1. **Add to `ClineDefaultTool` enum** in `src/shared/tools.ts`
2. **Tool definition** in `src/core/prompts/system-prompt/tools/` (create file like `generate_explanation.ts`)
- Define variants for each `ModelFamily` (generic, next-gen, xs, etc.)
- Export variants array (e.g., `export const my_tool_variants = [GENERIC, NATIVE_NEXT_GEN, XS]`)
- **Fallback behavior**: If a variant isn't defined for a model family, `ClineToolSet.getToolByNameWithFallback()` automatically falls back to GENERIC. So you only need to export `[GENERIC]` unless the tool needs model-specific behavior.
3. **Register in `src/core/prompts/system-prompt/tools/init.ts`** - Import and spread into `allToolVariants`
4. **Add to variant configs** - Each model family has its own config in `src/core/prompts/system-prompt/variants/*/config.ts`. Add your tool's enum to the `.tools()` list:
- `generic/config.ts`, `next-gen/config.ts`, `gpt-5/config.ts`, `native-gpt-5/config.ts`, `native-gpt-5-1/config.ts`, `native-next-gen/config.ts`, `gemini-3/config.ts`, `glm/config.ts`, `hermes/config.ts`, `xs/config.ts`
- **Important**: If you add to a variant's config, make sure the tool spec exports a variant for that ModelFamily (or relies on GENERIC fallback)
5. **Create handler** in `src/core/task/tools/handlers/`
6. **Wire up in `ToolExecutor.ts`** if needed for execution flow
7. **Add to tool parsing** in `src/core/assistant-message/index.ts` if needed
8. **If tool has UI feedback**: add `ClineSay` enum in proto, update `src/shared/ExtensionMessage.ts`, update `src/shared/proto-conversions/cline-message.ts`, update `webview-ui/src/components/chat/ChatRow.tsx`
## Modifying System Prompt
**Read these first:** `src/core/prompts/system-prompt/README.md`, `tools/README.md`, `__tests__/README.md`
System prompt is modular: **components** (reusable sections) + **variants** (model-specific configs) + **templates** (with `{{PLACEHOLDER}}` resolution).
**Key directories:**
- `components/` - Shared sections: `rules.ts`, `capabilities.ts`, `editing_files.ts`, etc.
- `variants/` - Model-specific: `generic/`, `next-gen/`, `xs/`, `gpt-5/`, `gemini-3/`, `hermes/`, `glm/`, etc.
- `templates/` - Template engine and placeholder definitions
**Variant tiers (ask user which to modify):**
- **Next-gen** (Claude 4, GPT-5, Gemini 2.5): `next-gen/`, `native-next-gen/`, `native-gpt-5/`, `native-gpt-5-1/`, `gemini-3/`, `gpt-5/`
- **Standard** (default fallback): `generic/`
- **Local/small models**: `xs/`, `hermes/`, `glm/`
**How overrides work:** Variants can override components via `componentOverrides` in their `config.ts`, or provide a custom template in `template.ts` (e.g., `next-gen/template.ts` exports `rules_template`). If no override, the shared component from `components/` is used.
**Example: Adding a rule to RULES section**
1. Check if variant overrides rules: look for `rules_template` in `variants/*/template.ts` or `componentOverrides.RULES` in `config.ts`
2. If shared: modify `components/rules.ts`
3. If overridden: modify that variant's template
4. XS variant is special—has heavily condensed inline content in `template.ts`
**After any changes, regenerate snapshots:**
```bash
UPDATE_SNAPSHOTS=true npm run test:unit
```
Snapshots live in `__tests__/__snapshots__/`. Tests validate across model families and context variations (browser, MCP, focus chain).
## Modifying Default Slash Commands
Three places need updates:
- `src/core/slash-commands/index.ts` - Command definitions
- `src/core/prompts/commands.ts` - System prompt integration
- `webview-ui/src/utils/slash-commands.ts` - Webview autocomplete
## Adding New Global State Keys
Adding a new key to global state requires updates in multiple places. Missing any step causes silent failures.
Required steps:
1. Type definition in `src/shared/storage/state-keys.ts` - Add to `GlobalState` or `Settings` interface
2. Read from globalState in `src/core/storage/utils/state-helpers.ts`:
- Add `const myKey = context.globalState.get<GlobalStateAndSettings["myKey"]>("myKey")` in `readGlobalStateFromDisk()`
- Add to the return object: `myKey: myKey ?? defaultValue,`
3. StateManager handles read/write via `setGlobalState()`/`getGlobalStateKey()` after initialization
Common mistake: Adding only the return value without the `context.globalState.get()` call. This compiles but the value is always `undefined` on load.
Settings plumbing gotcha: if a key is user-toggleable from settings, wire both controller update paths:
- `src/core/controller/state/updateSettings.ts` for webview `updateSetting(...)`
- `src/core/controller/state/updateSettingsCli.ts` for CLI/ACP settings updates
Missing one path causes a toggle to appear to change in one surface while the backend state stays unchanged.
Webview toggle gotcha: settings changes must also round-trip back in state payloads.
- Add the field to `UpdateSettingsRequest` in `proto/cline/state.proto` (for webview update requests), then run `npm run protos`
- Include the key in `Controller.getStateToPostToWebview()` (`src/core/controller/index.ts`)
- Ensure `ExtensionState` and webview defaults include the key (`src/shared/ExtensionMessage.ts`, `webview-ui/src/context/ExtensionStateContext.tsx`)
If this round-trip wiring is missing, the backend value can update but the toggle in webview appears stuck or reverts.
## StateManager Cache vs Direct globalState Access
StateManager uses an in-memory cache populated during `StateManager.initialize(context)` in `common.ts`. For most state, use `controller.stateManager.setGlobalState()`/`getGlobalStateKey()`.
Exception: State needed immediately at extension startup (before cache is ready)
When Window A sets state and immediately opens Window B, the new window's StateManager cache is populated from `context.globalState` during initialization. If you need to read state in Window B right at startup (e.g., in `common.ts` during `initialize()`), read directly from `context.globalState.get()` instead of StateManager's cache.
Example pattern (see `lastShownAnnouncementId` and `worktreeAutoOpenPath`):
```typescript
// Writing (normal pattern)
controller.stateManager.setGlobalState("myKey", value)
// Reading at startup in common.ts (bypass cache)
const value = context.globalState.get<string>("myKey")
```
This is only needed for cross-window state read during the brief startup window before StateManager cache is fully usable. Normal state access after initialization should use StateManager.
## ChatRow Cancelled/Interrupted States
When a ChatRow displays a loading/in-progress state (spinner), you must handle what happens when the task is cancelled. This is non-obvious because cancellation doesn't update the message content—you have to infer it from context.
**The pattern:**
1. A message has a `status` field (e.g., `"generating"`, `"complete"`, `"error"`) stored in `message.text` as JSON
2. When cancelled mid-operation, the status stays `"generating"` forever—no one updates it
3. To detect cancellation, check TWO conditions:
- `!isLast` — if this message is no longer the last message, something else happened after it (interrupted)
- `lastModifiedMessage?.ask === "resume_task" || "resume_completed_task"` — task was just cancelled and is waiting to resume
**Example from `generate_explanation`:**
```tsx
const wasCancelled =
explanationInfo.status === "generating" &&
(!isLast ||
lastModifiedMessage?.ask === "resume_task" ||
lastModifiedMessage?.ask === "resume_completed_task")
const isGenerating = explanationInfo.status === "generating" && !wasCancelled
```
**Why both checks?**
- `!isLast` catches: cancelled → resumed → did other stuff → this old message is stale
- `lastModifiedMessage?.ask === "resume_task"` catches: just cancelled, hasn't resumed yet, this message is still technically "last"
**See also:** `BrowserSessionRow.tsx` uses similar pattern with `isLastApiReqInterrupted` and `isLastMessageResume`.
**Backend side:** When streaming is cancelled, clean up properly (close tabs, clear comments, etc.) by checking `taskState.abort` after the streaming function returns.
-423
View File
@@ -1,423 +0,0 @@
# Cline Hooks Documentation
## Overview
Cline hooks allow you to execute custom scripts at specific points in the agentic workflow. Hooks can be placed in either:
- **Global hooks directory**: `~/Documents/Cline/Hooks/` (applies to all workspaces)
- **Workspace hooks directory**: `.clinerules/hooks/` (applies to the workspace the repo is part of)
Hooks run automatically when enabled.
## Enabling Hooks
1. Open Cline settings in VSCode
2. Navigate to the Feature Settings section
3. Check the "Enable Hooks" checkbox
4. Hooks must be executable files (on Unix/Linux/macOS use `chmod +x hookname`)
## Available Hooks
### TaskStart Hook
- **When**: Runs when a NEW task is started (not when resuming)
- **Purpose**: Initialize task context, validate task requirements, set up environment
- **Global Location**: `~/Documents/Cline/Hooks/TaskStart`
- **Workspace Location**: `.clinerules/hooks/TaskStart`
### TaskResume Hook
- **When**: Runs when an EXISTING task is resumed (after user clicks resume button)
- **Purpose**: Validate resumed task state, restore context, check for changes since last run
- **Global Location**: `~/Documents/Cline/Hooks/TaskResume`
- **Workspace Location**: `.clinerules/hooks/TaskResume`
### TaskCancel Hook
- **When**: Runs when a task is cancelled or a hook is aborted by the user (only if there's actual active work or work was started)
- **Purpose**: Clean up resources, log cancellation, save state
- **Global Location**: `~/Documents/Cline/Hooks/TaskCancel`
- **Workspace Location**: `.clinerules/hooks/TaskCancel`
- **Note**: This hook is NOT cancellable
### TaskComplete Hook (coming soon!)
- **When**: Runs when a task is marked as complete
- **Purpose**: Log completion status, perform final cleanup, generate reports
- **Global Location**: `~/Documents/Cline/Hooks/TaskComplete`
- **Workspace Location**: `.clinerules/hooks/TaskComplete`
### UserPromptSubmit Hook
- **When**: Runs when the user submits a prompt/message (initial task, resume, or feedback)
- **Purpose**: Validate user input, preprocess prompts, add context to user messages
- **Global Location**: `~/Documents/Cline/Hooks/UserPromptSubmit`
- **Workspace Location**: `.clinerules/hooks/UserPromptSubmit`
### PreToolUse Hook
- **When**: Runs BEFORE a tool is executed
- **Purpose**: Validate parameters, block execution, or add context
- **Global Location**: `~/Documents/Cline/Hooks/PreToolUse`
- **Workspace Location**: `.clinerules/hooks/PreToolUse`
### PostToolUse Hook
- **When**: Runs AFTER a tool completes
- **Purpose**: Observe results, track patterns, or add context
- **Global Location**: `~/Documents/Cline/Hooks/PostToolUse`
- **Workspace Location**: `.clinerules/hooks/PostToolUse`
### PreCompact Hook (coming soon!)
- **When**: Runs BEFORE the conversation context is compacted/truncated
- **Purpose**: Observe compaction events, log context management, track token usage
- **Global Location**: `~/Documents/Cline/Hooks/PreCompact`
- **Workspace Location**: `.clinerules/hooks/PreCompact`
## Cross-Platform Hook Format
Cline uses a git-style approach for hooks that works consistently across all platforms:
### Hook Files (All Platforms)
- **No file extensions**: Hooks are named exactly `PreToolUse` or `PostToolUse` (no `.bat`, `.cmd`, `.sh` etc.)
- **Shebang required**: First line must be a shebang (e.g., `#!/usr/bin/env bash` or `#!/usr/bin/env node`)
- **Executable on Unix**: On Unix/Linux/macOS, hooks must be executable: `chmod +x PreToolUse`
- **Windows**: Not currently supported.
### How It Works
Like git hooks, Cline executes hook files through a shell that interprets the shebang line:
- On Unix/Linux/macOS: Native shell execution with shebang support
This means:
- ✅ Same hook script works on all platforms
- ✅ Write once, run anywhere
- ✅ Use any scripting language (bash, node, python, etc.)
### Creating Hooks
**On Unix/Linux/macOS:**
```bash
# Create hook file
nano ~/Documents/Cline/Hooks/PreToolUse
# Make executable
chmod +x ~/Documents/Cline/Hooks/PreToolUse
```
## Context Injection Timing
**IMPORTANT**: Context injected by hooks affects **FUTURE AI decisions**, not the current tool execution.
### Why This Matters
When a hook runs:
1. The AI has already decided what tool to use and with what parameters
2. The hook cannot modify those parameters
3. Context from the hook is added to the conversation
4. The AI sees this context in the **NEXT API request** and can adjust future decisions
### PreToolUse Hook Flow
```
1. AI decides: "I'll use write_to_file with these parameters"
2. PreToolUse hook runs → can block or add context
3. If allowed, tool executes with original parameters
4. Context is added to conversation
5. Next API request includes this context
6. AI adjusts future decisions based on context
```
### PostToolUse Hook Flow
```
1. Tool completes execution
2. PostToolUse hook runs → observes results
3. Hook adds context about the outcome
4. Context is added to conversation
5. Next API request includes this context
6. AI can learn from the results
```
## Hook Input/Output
### Input (via stdin as JSON)
All hooks receive:
```json
{
"clineVersion": "string",
"hookName": "TaskStart" | "TaskResume" | "TaskCancel" | "TaskComplete" | "UserPromptSubmit" | "PreToolUse" | "PostToolUse" | "PreCompact",
"timestamp": "string",
"taskId": "string",
"workspaceRoots": ["string"],
"userId": "string",
"taskStart": { // Only for TaskStart
"taskMetadata": {
"taskId": "string",
"ulid": "string",
"initialTask": "string"
}
},
"taskResume": { // Only for TaskResume
"taskMetadata": {
"taskId": "string",
"ulid": "string"
},
"previousState": {
"lastMessageTs": "string",
"messageCount": "string",
"conversationHistoryDeleted": "string"
}
},
"taskCancel": { // Only for TaskCancel
"taskMetadata": {
"taskId": "string",
"ulid": "string",
"completionStatus": "string"
}
},
"taskComplete": { // Only for TaskComplete
"taskMetadata": {
"taskId": "string",
"ulid": "string"
}
},
"userPromptSubmit": { // Only for UserPromptSubmit
"prompt": "string",
"attachments": ["string"]
},
"preToolUse": { // Only for PreToolUse
"toolName": "string",
"parameters": {}
},
"postToolUse": { // Only for PostToolUse
"toolName": "string",
"parameters": {},
"result": "string",
"success": boolean,
"executionTimeMs": number
},
"preCompact": { // Only for PreCompact
"contextSize": number,
"messagesToCompact": number,
"compactionStrategy": "string"
}
}
```
### Output (via stdout as JSON)
All hooks must return:
```json
{
"cancel": boolean, // Required: false to continue, true to block execution
"contextModification": "string", // Optional: Context for future AI decisions
"errorMessage": "string" // Optional: Error details if blocking
}
```
**Note**: The `cancel` field works as follows:
- `false` (or omitted): Allow execution to continue
- `true`: Block execution and show error message to user
## Hook Execution Limits
- **Timeout**: Hooks must complete within 30 seconds (configurable via `HOOK_EXECUTION_TIMEOUT_MS`)
- **Context Size**: Context modifications are limited to 50KB (configurable via `MAX_CONTEXT_MODIFICATION_SIZE`)
- **Error Handling**: Expected errors (file not found, permission denied, not a directory) are handled silently; unexpected file system errors are propagated
## Common Use Cases
### 1. Validation - Block Invalid Operations
```bash
#!/usr/bin/env bash
input=$(cat)
tool_name=$(echo "$input" | jq -r '.preToolUse.toolName')
path=$(echo "$input" | jq -r '.preToolUse.parameters.path // ""')
if [[ "$tool_name" == "write_to_file" && "$path" == *.js ]]; then
cat <<EOF
{
"cancel": true,
"errorMessage": "Cannot create .js files in TypeScript project",
"contextModification": "Use .ts/.tsx extensions only"
}
EOF
exit 0
fi
echo '{"cancel": false}'
```
### 2. Context Building - Learn from Operations
```bash
#!/usr/bin/env bash
input=$(cat)
tool_name=$(echo "$input" | jq -r '.postToolUse.toolName')
success=$(echo "$input" | jq -r '.postToolUse.success')
path=$(echo "$input" | jq -r '.postToolUse.parameters.path // ""')
if [[ "$tool_name" == "write_to_file" && "$success" == "true" ]]; then
cat <<EOF
{
"cancel": false,
"contextModification": "Created '$path'. Maintain consistency with this file's patterns in future operations."
}
EOF
else
echo '{"cancel": false}'
fi
```
### 3. Performance Monitoring
```bash
#!/usr/bin/env bash
input=$(cat)
execution_time=$(echo "$input" | jq -r '.postToolUse.executionTimeMs')
tool_name=$(echo "$input" | jq -r '.postToolUse.toolName')
if [[ "$execution_time" -gt 5000 ]]; then
cat <<EOF
{
"cancel": false,
"contextModification": "Tool '$tool_name' took ${execution_time}ms. Consider optimizing future similar operations."
}
EOF
else
echo '{"cancel": false}'
fi
```
### 4. Logging and Telemetry
```bash
#!/usr/bin/env bash
input=$(cat)
# Log to file
echo "$input" >> ~/.cline/hook-logs/tool-usage.jsonl
# Allow execution
echo '{"cancel": false}'
```
## Global vs Workspace Hooks
Cline supports two levels of hooks:
### Global Hooks
- **Location**: `~/Documents/Cline/Hooks/` (macOS/Linux)
- **Scope**: Apply to ALL workspaces and projects
- **Use Case**: Organization-wide policies, personal preferences, universal validations
- **Priority**: Order not guaranteed when combined with workspace hooks
### Workspace Hooks
- **Location**: `.clinerules/hooks/` in each workspace root
- **Scope**: Apply only to the specific workspace
- **Use Case**: Project-specific rules, team conventions, repository requirements
- **Priority**: Order not guaranteed when combined with global hooks
### Hook Execution
When multiple hooks exist (global and/or workspace):
- All hooks for a given step are executed **concurrently** using `Promise.all`
- **Execution order is not guaranteed** - hooks run in parallel
- If ALL hooks allow execution (`cancel: false`), the tool proceeds
- If ANY hook blocks (`cancel: true`), execution is blocked
**Result Combination:**
- `cancel`: If ANY hook returns `true`, execution is blocked
- `contextModification`: All context strings are concatenated with double newlines (`\n\n`)
- `errorMessage`: All error messages are concatenated with single newlines (`\n`)
### Setting Up Global Hooks
1. The global hooks directory is automatically created at:
- macOS/Linux: `~/Documents/Cline/Hooks/`
2. Add your hook script:
```bash
# Unix/Linux/macOS
nano ~/Documents/Cline/Hooks/PreToolUse
chmod +x ~/Documents/Cline/Hooks/PreToolUse
```
3. Enable hooks in Cline settings
### Example: Global + Workspace Hooks
**Global Hook** (applies to all projects):
```bash
#!/usr/bin/env bash
# ~/Documents/Cline/Hooks/PreToolUse
# Universal rule: Never delete package.json
input=$(cat)
tool_name=$(echo "$input" | jq -r '.preToolUse.toolName')
path=$(echo "$input" | jq -r '.preToolUse.parameters.path // ""')
if [[ "$tool_name" == "write_to_file" && "$path" == *"package.json"* ]]; then
echo '{"cancel": true, "errorMessage": "Global policy: Cannot modify package.json"}'
exit 0
fi
echo '{"cancel": false}'
```
**Workspace Hook** (applies to specific project):
```bash
#!/usr/bin/env bash
# .clinerules/hooks/PreToolUse
# Project rule: Only TypeScript files
input=$(cat)
tool_name=$(echo "$input" | jq -r '.preToolUse.toolName')
path=$(echo "$input" | jq -r '.preToolUse.parameters.path // ""')
if [[ "$tool_name" == "write_to_file" && "$path" == *.js ]]; then
echo '{"cancel": true, "errorMessage": "Project rule: Use .ts files only"}'
exit 0
fi
echo '{"cancel": false}'
```
**All hooks must allow execution for the tool to proceed.** Hooks may execute concurrently.
## Multi-Root Workspaces
If you have multiple workspace roots, you can place hooks in each root's `.clinerules/hooks/` directory. All hooks (global and workspace) may execute concurrently. Their results will be combined:
- **cancel**: If ANY hook returns `true`, execution is blocked
- **contextModification**: All context modifications are concatenated
- **errorMessage**: All error messages are concatenated
**Note:** No execution order is guaranteed between hooks from different directories.
## Troubleshooting
### Hook Not Running
- Ensure the "Enable Hooks" setting is checked
- Verify the hook file is executable (`chmod +x hookname`)
- Check the hook file has no syntax errors
- Look for errors in VSCode's Output panel (Cline channel)
### Hook Timing Out
- Reduce complexity of the hook script
- Avoid expensive operations (network calls, heavy computations)
- Consider moving complex logic to a background process
### Context Not Affecting Behavior
- Remember: context affects FUTURE decisions, not the current tool
- Ensure context modifications are clear and actionable
- Check that context isn't being truncated (50KB limit)
## Security Considerations
- Hooks run with the same permissions as VSCode
- Be cautious with hooks from untrusted sources
- Review hook scripts before enabling them
- Consider using `.gitignore` to avoid committing sensitive hook logic
- Hooks can access all workspace files and environment variables
## Best Practices
1. **Keep hooks fast** - Aim for <100ms execution time
2. **Make context actionable** - Be specific about what the AI should do
3. **Use structured prefixes** - Help the AI categorize context
4. **Handle errors gracefully** - Always return valid JSON
5. **Log for debugging** - Keep logs of hook executions for troubleshooting
6. **Test incrementally** - Start with simple hooks and add complexity
7. **Document your hooks** - Add comments explaining the purpose and logic
-90
View File
@@ -1,90 +0,0 @@
# Networking & Proxy Support
To ensure Cline works correctly in all environments (VSCode, JetBrains, CLI) and with various network configurations (especially corporate proxies), strictly follow these guidelines for all network activity.
In extension code, do NOT use the global `fetch` or a default `axios` instance. (Note, `shared/net.ts` is exempt from these rules because it sets up the fetch wrappers.) In Webview code, you SHOULD use global `fetch`.
Global `fetch` and default `axios` do not automatically pick up proxy configurations in all environments (specifically JetBrains and CLI). You MUST use the provided utilities in `@/shared/net` which handle proxy agent configuration. In the webview, the browser/embedder handles proxies.
## Guidelines
### 1. Using `fetch`
Instead of `fetch(...)`, import the proxy-aware wrapper:
```typescript
import { fetch } from '@/shared/net'
// Usage is identical to global fetch
const response = await fetch('https://api.example.com/data')
```
### 2. Using `axios`
When using `axios`, you must apply the settings from `getAxiosSettings()`:
```typescript
import axios from 'axios'
import { getAxiosSettings } from '@/shared/net'
const response = await axios.get('https://api.example.com/data', {
headers: { 'Authorization': '...' },
...getAxiosSettings() // <--- CRITICAL: Injects the proxy agent if needed
})
```
### 3. Third-Party Clients (OpenAI, Ollama, etc.)
Most API client libraries allow you to customize the `fetch` implementation. You **MUST** pass the proxy-aware `fetch` to these clients.
**Example (OpenAI):**
```typescript
import OpenAI from "openai"
import { fetch } from "@/shared/net"
this.client = new OpenAI({
apiKey: '...',
fetch, // <--- CRITICAL: Pass our fetch wrapper
})
```
### 4. Tests
Use `mockFetchForTesting` to mock the underlying fetch implementation.
**Example (callback):**
```
import { mockFetchForTesting } from "@/shared/net"
...
let mockFetch = ...
mockFetchForTesting(mockFetch, () => {
// This calls mockFetch
fetch('https://foo.example').then(...)
})
// Original fetch is restored immediately when the call returns.
```
**Example (Promise):**
```
import { mockFetchForTesting } from "@/shared/net"
...
let mockFetch = ...
await mockFetchForTesting(mockFetch, async () => {
await ...
// This calls mockFetch
await fetch('https://foo.example')
...
})
// Original fetch is restored when the Promise from the callback settles
```
## Verification
If you are adding a new network call or integration:
1. Check `@/shared/net.ts` is imported.
2. Ensure `fetch` or `getAxiosSettings` is being used.
3. Verify that third-party clients are configured to use the custom fetch.
-64
View File
@@ -1,64 +0,0 @@
# Storage Architecture
Global settings, secrets and workspace state are stored in **file-backed JSON stores** under `~/.cline/data/`. This is the shared storage layer used by VSCode, CLI, and JetBrains.
## Key Abstractions
### `StorageContext` (src/shared/storage/storage-context.ts)
The entry point. Created via `createStorageContext()` and passed to `StateManager.initialize()`. Contains three `ClineFileStorage` instances:
- `globalState``~/.cline/data/globalState.json`
- `secrets``~/.cline/data/secrets.json` (mode 0o600)
- `workspaceState``~/.cline/data/workspaces/<hash>/workspaceState.json`
### `ClineFileStorage` (src/shared/storage/ClineFileStorage.ts)
Synchronous JSON key-value store backed by a single file. Supports `get()`, `set()`, `setBatch()`, `delete()`. Writes are atomic (write-then-rename).
### `StateManager` (src/core/storage/StateManager.ts)
In-memory cache on top of `StorageContext`. All runtime reads hit the cache; writes update cache immediately and debounce-flush to disk.
## ⚠️ Do NOT Use VSCode's ExtensionContext for Storage
**Do not** read from or write to `context.globalState`, `context.workspaceState`, or `context.secrets` for persistent data. These are VSCode-specific and not available on CLI or JetBrains.
Instead, use:
```typescript
// Reading state
StateManager.get().getGlobalStateKey("myKey")
StateManager.get().getSecretKey("mySecretKey")
StateManager.get().getWorkspaceStateKey("myWsKey")
// Writing state
StateManager.get().setGlobalState("myKey", value)
StateManager.get().setSecret("mySecretKey", value)
StateManager.get().setWorkspaceState("myWsKey", value)
```
Remember that your data may be read by a different client than the one that wrote it. For example, a value written by Cline in JetBrains may be read by Cline CLI.
## VSCode Migration (src/hosts/vscode/vscode-to-file-migration.ts)
On VSCode startup, a migration copies data from VSCode's `ExtensionContext` storage into the file-backed stores. This runs in `src/common.ts` before `StateManager.initialize()`.
- **Sentinel**: `__vscodeMigrationVersion` key in global state and workspace state — prevents re-migration.
- **Merge strategy**: File store wins. Existing values are never overwritten.
- **Safe downgrade**: VSCode storage is NOT cleared, so older extension versions still work.
## Adding New Storage Keys
1. Add to `src/shared/storage/state-keys.ts` (see existing patterns)
2. Read/write via `StateManager` (NOT via `context.globalState`)
3. If adding a secret, add to `SecretKeys` array in `state-keys.ts`
## File Layout
```
~/.cline/
data/
globalState.json # Global settings & state
secrets.json # API keys (mode 0o600)
tasks/
taskHistory.json # Task history (separate file)
workspaces/
<hash>/
workspaceState.json # Per-workspace toggles
```
@@ -1,29 +0,0 @@
# Address PR Comments
Review and address all comments on the current branch's PR.
## Steps
1. Get the current branch name and find the associated PR:
```bash
gh pr view --json number,title,body
```
2. Understand the PR context:
- Get the full diff: `git diff origin/main...HEAD`
- Read the changed files to understand what the PR is doing
- Read related files if needed to understand the broader context
- Understand the intent and spirit of the changes, not just the code
3. Fetch all PR comments:
- Inline comments: `gh api repos/{owner}/{repo}/pulls/{pr_number}/comments`
- General comments: `gh pr view {pr_number} --json comments,reviews`
4. Present a summary of all comments with your recommendation for each (apply, skip, or respond). Ignore bot noise (release automation, CI status, etc.).
5. **Wait for my approval** before proceeding.
6. After approval:
- Apply code changes and commit
- Reply to comments that were addressed or intentionally skipped
- Push commits
+549
View File
@@ -0,0 +1,549 @@
The goal of this workflow is to take a changeset for a release of Cline, an autonomous coding agent extension that plugs right into your IDE, and write the updated announcement component, and the updated changelog.
For reference, here are some examples of how we converted previous changesets to announcement components / changelogs.
- 3.14
<changeset>
This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and publish to npm yourself or setup this action to publish automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.
Releases
claude-dev@3.14.0
Minor Changes
77c9863: create clinerules folder if its currently a file and creating new rule
0ffb7dd: disabling shift hint for now & improving tooltip behavior
79b76fd: Add support for custom model ID in AWS Bedrock provider, enabling use of Application Inference Profile.
eb6e481: Full support for LaTeX rendering
df37f29: Add support for custom API request timeout. Previously, timeouts were hardcoded to 30 seconds for providers like Ollama or 15 seconds for OpenRouter and Cline. Now users can set a custom timeout value in milliseconds through the settings interface.
e4d26be: allow cursorrules and windsurfrules
c5de50f: Fix Handle @withRetry() SyntaxError when running extension locally issue
61d2f42: enabled pricing calculation for gemini and vertex + more robust caching & cache tracking for gemini & vertex
aed152b: add truncation notice when truncating manually
2fe2405: Migrate Cline Tools Section to new docs
19cc8bc: Add a timeout setting for the terminal connection, allowing users to adjust this if they are having timeout issues
03d4410: Added copy button to code blocks.
c78fe23: addressed race condition in terminal command usage
91e222f: add checkpoints after more messages
14230e7: add newrule slash command
1c7d33a: Add remote config with posthog allowing for disabling new features until they're reading, making for a better developer experience.
4196c14: add cache ui for open router and cline provider
d97424f: showing expanded task by default
5294e78: Refactor to not pass a message for showing the MCP View from the servers modal
70cc437: Fix Windows path issue: Correct handling of import.meta.url to avoid leading slash in pathname
4b697d8: Migrate the addRemoteServer to protobus
Patch Changes
c63d9a1: updated drag and drop text to say "drop" instead of "drag"
459adf0: Add markdown copy to chat
74ec823: Minor UX improvement to drag and drop ux
b0961f4: Remove linear pull request action
e9ce384: searchCommits protobus migration
5802b68: createRuleFile protobus migration
df7f9fc: Add dependsOn to more blocks in the tasks.json
41ae732: Fix for git commit mentions in repos with no git commits
7e78445: Adding args to allow Cursor to open workspaces (for checkpoint testing/development)
bdfda6f: feat(bedrock): Introduce Amazon Nova Premier
65243ad: Introduce UI library for future UI development
4565e06: checkIsImageURL migrated to protobus
5a8e9d8: protobus migration for openImage
deeda6e: Lowering Gemini cache TTL time
db0b022: Adding UI to show openrouter balance next to provider
4650ffa: deleteRuleFile protobus migration
d4bd755: fix cost calculation
</changeset>
<changelog>
## [3.14.0]
- Add UI to show openrouter balance next to provider
- Add support for custom model ID in AWS Bedrock provider, enabling use of Application Inference Profile (Thanks @clicube!)
- Add more robust caching & cache tracking for gemini & vertex providers
- Add support for LaTeX rendering
- Add support for custom API request timeout. Timeouts were 15-30s, but can now be configured via settings for OpenRouter/Cline & Ollama (Thanks @WingsDrafterwork!)
- Add truncation notice when truncating manually
- Add a timeout setting for the terminal connection, allowing users to set a time to wait for terminal startup
- Add copy button to code blocks
- Add copy button to markdown blocks (Thanks @weshoke!)
- Add checkpoints to more messages
- Add slash command to create a new rules file (/newrule)
- Add cache ui for open router and cline provider
- Add Amazon Nova Premier model to Bedrock (Thanks @watany!)
- Add support for cursorrules and windsurfrules
- Add support for batch history deletion (Thanks @danix800!)
- Improve Drag & Drop experience
- Create clinerules folder creating new rule if it's needed
- Enable pricing calculation for gemini and vertex providers
- Refactor message handling to not show the MCP View of the server modal
- Migrate the addRemoteServer to protobus (Thanks @DaveFres!)
- Update task header to be expanded by default
- Update Gemini cache TTL time to 15 minutes
- Fix race condition in terminal command usage
- Fix to correctly handle `import.meta.url`, avoiding leading slash in pathname for Windows (Thanks @DaveFres!)
- Fix @withRetry() decoration syntax error when running extension locally (Thanks @DaveFres!)
- Fix for git commit mentions in repos with no git commits
- Fix cost calculation (Thanks @BarreiroT!)
</changelog>
<announcement-component>
const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
const minorVersion = version.split(".").slice(0, 2).join(".") // 2.0.0 -> 2.0
return (
<div style={containerStyle}>
<VSCodeButton appearance="icon" onClick={hideAnnouncement} style={closeIconStyle}>
<span className="codicon codicon-close"></span>
</VSCodeButton>
<h3 style={h3TitleStyle}>
🎉{" "}New in v{minorVersion}
</h3>
<ul style={ulStyle}>
<li>
<b>Gemini prompt caching:</b> Gemini and Vertex providers now support prompt caching and price tracking for
Gemini models.
</li>
<li>
<b>Copy Buttons:</b> Buttons were added to Markdown and Code blocks that allow you to copy their contents
easily.
</li>
<li>
<b>/newrule command:</b> New slash command to have cline write your .clinerules for you based on your
workflow.
</li>
<li>
<b>Drag and drop improvements:</b> Don't forget to hold shift while dragging files!
</li>
<li>Added more checkpoints across the task, allowing you to restore from more than just file changes.</li>
<li>Added support for rendering LaTeX in message responses. (Try asking Cline to show the quadratic formula)</li>
</ul>
<Accordion isCompact className="pl-0">
<AccordionItem
key="1"
aria-label="Previous Updates"
title="Previous Updates:"
classNames={{
trigger: "bg-transparent border-0 pl-0 pb-0 w-fit",
title: "font-bold text-[var(--vscode-foreground)]",
indicator:
"text-[var(--vscode-foreground)] mb-0.5 -rotate-180 data-[open=true]:-rotate-90 rtl:rotate-0 rtl:data-[open=true]:-rotate-90",
}}>
<ul style={ulStyle}>
<li>
<b>Global Cline Rules:</b> store multiple rules files in Documents/Cline/Rules to share between
projects.
</li>
<li>
<b>Cline Rules Popup:</b> New button in the chat area to view workspace and global cline rules files
to plug and play specific rules for the task
</li>
<li>
<b>Slash Commands:</b> Type <code>/</code> in chat to see the list of quick actions, like starting a
new task (more coming soon!)
</li>
<li>
<b>Edit Messages:</b> You can now edit a message you sent previously by clicking on it. Optionally
restore your project when the message was sent!
</li>
</ul>
</AccordionItem>
</Accordion>
{/*
// Leave this here for an example of how to structure the announcement
<ul style={{ margin: "0 0 8px", paddingLeft: "12px" }}>
<li>
OpenRouter now supports prompt caching! They also have much higher rate limits than other providers,
so I recommend trying them out.
<br />
{!apiConfiguration?.openRouterApiKey && (
<VSCodeButtonLink
href={getOpenRouterAuthUrl(vscodeUriScheme)}
style={{
transform: "scale(0.85)",
transformOrigin: "left center",
margin: "4px -30px 2px 0",
}}>
Get OpenRouter API Key
</VSCodeButtonLink>
)}
{apiConfiguration?.openRouterApiKey && apiConfiguration?.apiProvider !== "openrouter" && (
<VSCodeButton
onClick={() => {
vscode.postMessage({
type: "apiConfiguration",
apiConfiguration: { ...apiConfiguration, apiProvider: "openrouter" },
})
}}
style={{
transform: "scale(0.85)",
transformOrigin: "left center",
margin: "4px -30px 2px 0",
}}>
Switch to OpenRouter
</VSCodeButton>
)}
</li>
<li>
<b>Edit Cline's changes before accepting!</b> When he creates or edits a file, you can modify his
changes directly in the right side of the diff view (+ hover over the 'Revert Block' arrow button in
the center to undo "<code>{"// rest of code here"}</code>" shenanigans)
</li>
<li>
New <code>search_files</code> tool that lets Cline perform regex searches in your project, letting
him refactor code, address TODOs and FIXMEs, remove dead code, and more!
</li>
<li>
When Cline runs commands, you can now type directly in the terminal (+ support for Python
environments)
</li>
</ul>*/}
<div style={hrStyle} />
<p style={linkContainerStyle}>
Join us on{" "}
<VSCodeLink style={linkStyle} href="https://x.com/cline">
X,
</VSCodeLink>{" "}
<VSCodeLink style={linkStyle} href="https://discord.gg/cline">
discord,
</VSCodeLink>{" "}
or{" "}
<VSCodeLink style={linkStyle} href="https://www.reddit.com/r/cline/">
r/cline
</VSCodeLink>
for more updates!
</p>
</div>
)
}
</announcement-component>
- 3.13
<changeset>
Minor Changes
2964388: Added copy button to MermaidBlock component
75143a7: Add the ability to fetch from global cline rules files
Patch Changes
a0252e7: convert inline style to tailwind css of file SettingsView.tsx
ab59bd9: Add stream options back to xai provider
7276f50: Icons to indicate an action is occuring outside of the users workspace
0b19ba6: update to NEW model
</changeset>
<changelog>
## [3.13.0]
- Add Cline rules popover under the chat field, allowing you to easily add, enable & disable workspace level or global rule files
- Add new slash command menu letting you type “/“ to do quick actions like creating new tasks
- Add ability to edit past messages, with options to restore your workspace back to that point
- Allow sending a message when selecting an option provided by the question or plan tool
- Add command to jump to Cline's chat input
- Add support for OpenAI o3 & 4o-mini (Thanks @PeterDaveHello and @arafatkatze!)
- Add baseURL option for Google Gemini provider (Thanks @owengo and @olivierhub!)
- Add support for Azure's DeepSeek model. (Thanks @yt3trees!)
- Add ability for models that support it to receive image responses from MCP servers (Thanks @rikaaa0928!)
- Improve search and replace diff editing by making it more flexible with models that fail to follow structured output instructions. (Thanks @chi-cat!)
- Add detection of Ctrl+C termination in terminal, improving output reading issues
- Fix issue where some commands with large output would cause UI to freeze
- Fix token usage tracking issues with vertex provider (Thanks @mzsima!)
- Fix issue with xAI reasoning content not being parsed (Thanks @mrubens!)
</changelog>
<announcement-component>
const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
const minorVersion = version.split(".").slice(0, 2).join(".") // 2.0.0 -> 2.0
return (
<div style={containerStyle}>
<VSCodeButton appearance="icon" onClick={hideAnnouncement} style={closeIconStyle}>
<span className="codicon codicon-close"></span>
</VSCodeButton>
<h3 style={h3TitleStyle}>
🎉{" "}New in v{minorVersion}
</h3>
<ul style={ulStyle}>
<li>
<b>Global Cline Rules:</b> store multiple rules files in Documents/Cline/Rules to share between projects.
</li>
<li>
<b>Cline Rules Popup:</b> New button in the chat area to view workspace and global cline rules files to plug
and play specific rules for the task
</li>
<li>
<b>Slash Commands:</b> Type <code>/</code> in chat to see the list of quick actions, like starting a new task
(more coming soon!)
</li>
<li>
<b>Edit Messages:</b> You can now edit a message you sent previously by clicking on it. Optionally restore
your project when the message was sent!
</li>
</ul>
<h4 style={{ margin: "5px 0 5px" }}>Previous Updates:</h4>
<ul style={ulStyle}>
<li>
<b>Model Favorites:</b> You can now mark your favorite models when using Cline & OpenRouter providers for
quick access!
</li>
<li>
<b>Faster Diff Editing:</b> Improved animation performance for large files, plus a new indicator in chat
showing the number of edits Cline makes.
</li>
<li>
<b>New Auto-Approve Options:</b> Turn off Cline's ability to read and edit files outside your workspace.
</li>
</ul>
{/*
// Leave this here for an example of how to structure the announcement
<ul style={{ margin: "0 0 8px", paddingLeft: "12px" }}>
<li>
OpenRouter now supports prompt caching! They also have much higher rate limits than other providers,
so I recommend trying them out.
<br />
{!apiConfiguration?.openRouterApiKey && (
<VSCodeButtonLink
href={getOpenRouterAuthUrl(vscodeUriScheme)}
style={{
transform: "scale(0.85)",
transformOrigin: "left center",
margin: "4px -30px 2px 0",
}}>
Get OpenRouter API Key
</VSCodeButtonLink>
)}
{apiConfiguration?.openRouterApiKey && apiConfiguration?.apiProvider !== "openrouter" && (
<VSCodeButton
onClick={() => {
vscode.postMessage({
type: "apiConfiguration",
apiConfiguration: { ...apiConfiguration, apiProvider: "openrouter" },
})
}}
style={{
transform: "scale(0.85)",
transformOrigin: "left center",
margin: "4px -30px 2px 0",
}}>
Switch to OpenRouter
</VSCodeButton>
)}
</li>
<li>
<b>Edit Cline's changes before accepting!</b> When he creates or edits a file, you can modify his
changes directly in the right side of the diff view (+ hover over the 'Revert Block' arrow button in
the center to undo "<code>{"// rest of code here"}</code>" shenanigans)
</li>
<li>
New <code>search_files</code> tool that lets Cline perform regex searches in your project, letting
him refactor code, address TODOs and FIXMEs, remove dead code, and more!
</li>
<li>
When Cline runs commands, you can now type directly in the terminal (+ support for Python
environments)
</li>
</ul>*/}
<div style={hrStyle} />
<p style={linkContainerStyle}>
Join us on{" "}
<VSCodeLink style={linkStyle} href="https://x.com/cline">
X,
</VSCodeLink>{" "}
<VSCodeLink style={linkStyle} href="https://discord.gg/cline">
discord,
</VSCodeLink>{" "}
or{" "}
<VSCodeLink style={linkStyle} href="https://www.reddit.com/r/cline/">
r/cline
</VSCodeLink>
for more updates!
</p>
</div>
)
}
</announcement-component>
We have a changeset PR that automatically generated as new unreleased PRs are merged into main, the PR is always called "Changeset version bump" and the author is github-actions.
The Changeset PR description looks something like this:
<changeset-pr-description>
This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and publish to npm yourself or [setup this action to publish automatically](https://github.com/changesets/action#with-publishing). If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.
# Releases
## claude-dev@3.16.0
### Minor Changes
- c6e8b04: Recent task list is now collapsible, allowing users to hide their recent tasks (e.g. when sharing their screen).
- aabe4ae: Add detection for new users to display special components
- 6c18d51: adds global endpoint for vertex ai users
- 080ed7c: Add Tailwind CSS IntelliSense to the the recommended extensions list
- 5147e28: new workflow feature
### Patch Changes
- c0b3c69: fix eternal loading states when the last message is a checkpoint
- 570ece3: selectImages protos migration
- 8d8452e: askResponse protobus migration
- cd1ff2a: Finishing the migration of Vscode Advanced settings to Settings Webview
</changeset-pr-description>
The changeset pr is ALWAYS on the following branch: `changeset-release/main`.
I have the `gh` command line tool set up and authenticated, so you have everything you need.
The first step is to get the full diff from the changeset PR to look at the changes that were automatically made to the `CHANGELOG.md` file. By default it will automatically add a new section to the changelog.md file with the new version. The problem with the automatically generated section is that it just takes the text that the developers threw into their changeset files for each corresponding PR, and they can be pretty vague and bad. Additionally there's some stuff that is totally irrelevant for the end user, like minor refactoring changes. So I manually typically go in and update this section to be a proper changelog that will show up in our patchnotes. You can look at how the rest of the file is done because those are all good examples of us updating this to use good language for the end user. We usually put new features up top (and the most exciting flagship features at the very top), and then bug fixes/improvements at the bottom. Having some basic organization to the ordering of the bullet points by content is nice. But use common sense.
To handle this process effectively, do the following:
For each of the automatically generated bullet points in the Changelog.md, you should
1. Take the commit hash at the start of the bullet point, and use the `gh` command line tool find the PR that it was associated with.
2. Use the `gh` command to get the PR title/description/discussion to understand the context surrounding the PR.
3. Use the `gh` command line tool to get the full PR diff to fully understand the changes made in the code.
4. Synthesize that knowledge to determine (a) whether or not this change is relevant to end users and (b) what the text & ordering of the line should be.
5. Update the `CHANGELOG.md` accordingly
Do this for every single item in the list from the autogenerated bullet points. We want to be diligent and have a full understanding of every feature so we can make the best changelog ever!
Here are some principles for good changelogs from keepchangelog.com, a handy guide:
<keepachangelog-pinciples-for-good-changelogs>
### Guiding Principles
- Changelogs are for humans, not machines.
- There should be an entry for every single version.
- The same types of changes should be grouped.
- The latest version comes first.
### Bullet points in the changelog should follow these principles:
- Types of changes
- Added for new features.
- Changed for changes in existing functionality.
- Deprecated for soon-to-be removed features.
- Removed for now removed features.
- Fixed for any bug fixes.
- Security in case of vulnerabilities.
</keepachangelog-pinciples-for-good-changelogs>
Lastly, when developers make a PR, they typically make a changeset. And they have 3 options when making the changeset:
1. Patch
2. Minor
3. Major
Sometimes they label something as minor when really it should just be a patch. Or vice versa. Because of this, the automatic version bump may be incorrect. So when starting out this workflow, you should use the <ask_followup_question> tool to confirm with me whether or not this should be a patch bump (show the old version number and what the proposed new version number would be) or a minor bump. Part of the release process is making sure the version in package.json that is automatically changed actually corresponds with what we decided the bump should actually be based on the features. ALL these modifications happen in the `changeset-release/main` branch btw.
<important_note>
Before doing any of this, make sure you check out the `changeset-release/main` and pull the most recent up to date changes. Then perform all this work in that branch.
New announcement banners should ONLY be made for minor version bumps or higher. That's another reason why double checking if the changelog warrants the bump is important.
Also, SUPER important: For any external contributors that aren't part of the cline github organization, we always want to add a (Thanks @username!) at the end of the changelog to attribute them properly. We're an open source project and it's ethical to do this.
</important_note>
Once the changelog looks good, and the version number looks good, we gotta double check that the version number in the changelog has the brackets around it. And as a final step, double check the package.json version number matches the latest number in the changelog. And as the ultimate final step we run `npm run install:all` to make sure the package version number permiates through the lock file.
<detailed_sequence_of_steps>
# Cline Release Process - Detailed Sequence of Steps
## Before Starting
1. First, examine the changeset PR without checking it out:
```bash
gh pr view changeset-release/main
```
2. View the PR diff to see the auto-generated CHANGELOG.md changes:
```bash
gh pr diff changeset-release/main > changeset-diff.txt
cat changeset-diff.txt | grep -A 50 "CHANGELOG.md"
```
## Initial Setup
3. Once you're ready to start, checkout and update the changeset release branch:
```bash
git checkout changeset-release/main
git pull origin changeset-release/main
```
## Analyzing Each Change
4. For each commit hash in the auto-generated changelog entries:
a. Find the PR number associated with a commit hash:
```bash
gh pr list --search "<commit-hash>" --state merged
```
b. Get PR details for better context:
```bash
gh pr view <PR-number>
```
c. Check if the contributor is external to determine if attribution is needed:
```bash
# Extract username from PR
USERNAME=$(gh pr view <PR-number> --json author --jq .author.login)
# Check if user is a member of the Cline organization
# this command is a bit finnicky, but it 100% works.
# if you see a `Error executing command: The command ran successfully, but we couldn't capture its output. Please proceed accordingly.` error, just retry it until you actually get the output
# don't make any assumptions, just retry the command to actually get the output and determine if they're external or not.
# no output means they are an external contributor, otherwise if there is output they are an internal contributor (part of our github org)
gh api "orgs/cline/members" --jq "map(.login)" | grep -i "pashpashpash"
```
d. View the full PR diff to understand code changes:
```bash
gh pr diff <PR-number> > pr-diff-<PR-number>.txt
cat pr-diff-<PR-number>.txt
```
## Updating the Changelog
5. Based on PR analysis, update the CHANGELOG.md with user-friendly descriptions:
- Use the `<replace_in_file>` tool to edit the CHANGELOG.md file
- Group by feature type (Added, Changed, Fixed)
- Put most exciting features at the top
- Move bug fixes and small improvements to the bottom
- Use clear, end-user focused language
- For external contributors, add attribution at the end of the relevant entry: `(Thanks @username!)`
## Version Number Verification
6. Confirm the version bump is appropriate:
- Check package.json to verify the auto-generated version number:
```bash
cat package.json | grep "\"version\""
```
- If the feature set doesn't warrant a minor bump, use the `<replace_in_file>` tool to modify package.json
7. Ensure the version in CHANGELOG.md has brackets around it:
```
## [3.16.0]
```
## Creating the Announcement (for minor/major versions only)
8. If this is a minor version bump, create/update the announcement component:
- Use the `<replace_in_file>` tool to edit the src/views/components/announcement.tsx file
- Update the highlights based on key features
- Move previous version highlights to the "Previous Updates" section
- Use the previous announcement components as reference for structure
## Finalizing the Release
9. Update dependencies with the new version number:
```bash
npm run install:all
```
10. Commit your changes:
```bash
git add CHANGELOG.md package.json package-lock.json src/views/components/announcement.tsx
git commit -m "Update CHANGELOG.md and announcement for version 3.16.0"
```
11. Push your changes to the changeset branch:
```bash
git push origin changeset-release/main
```
12. Check that your changes pushed successfully:
```bash
git status
```
</detailed_sequence_of_steps>
@@ -1,49 +0,0 @@
# Find Best Reviewers for Current Branch
Analyze my current branch to find the best people to review my PR based on **domain expertise** and git history.
## Steps
1. Get the current branch name and verify it's not `main`
2. Get the diff between the current branch and `origin/main`:
- Use `git diff origin/main...HEAD --name-only` to get changed files
- Use `git diff origin/main...HEAD` to understand the nature/spirit of the changes
3. **Identify the domain/feature area** being changed:
- Read the diff carefully to understand WHAT is being changed conceptually (e.g., "slash commands", "authentication", "API client", "UI components")
- This semantic understanding is crucial for finding the right reviewers
4. Find domain experts by searching for related files and their contributors:
- Identify all files related to the feature/domain (not just the ones changed)
- Example: if changing slash commands, find ALL slash-command related files across the codebase
- Use `git log --format="%an <%ae>" -- <related-files-pattern>` to find who has expertise in that domain
5. For additional context, also gather:
- `git blame -L <start>,<end> origin/main -- <file-path>` for exact lines changed
- Recent commit activity on related files
6. Score and rank contributors by:
- **Highest weight: Domain expertise** - who has the most commits to files in this feature area (even files not touched by this PR)
- **Medium weight: Direct file expertise** - commits to the specific files being changed
- **Lower weight: Line-level ownership** - authored the exact lines being modified
7. Exclude myself (check against my git config user.email)
8. Present the top 5 reviewers as an ordered list
## Output Format
Output an ordered list:
1. **Name** - Domain expert: 15 commits to slash-command related files, authored core parsing logic
2. **Name** - 8 commits to affected files, recently added the feature being modified
3. ...
## Commands Reference
```bash
git config user.email
git diff origin/main...HEAD --name-only
git diff origin/main...HEAD
# Find related files for a domain (adjust pattern based on what you learn from the diff)
find . -type f \( -name "*slash-command*" -o -name "*SlashCommand*" \) | head -20
# Get contributors for related files
find . -type f \( -name "*slash-command*" -o -name "*SlashCommand*" \) -print0 | xargs -0 git log --format="%an <%ae>" -- | sort | uniq -c | sort -rn
git log --format="%an <%ae>" -- <file> | sort | uniq -c | sort -rn
git blame -L 10,20 origin/main -- <file>
```
Do NOT ask questions - analyze the changes, identify the domain, and output the reviewer list.
-187
View File
@@ -1,187 +0,0 @@
# Hotfix Release
Create a hotfix release by cherry-picking specific commits from main onto the latest release tag.
## Overview
This workflow helps you:
1. Select specific commits from main to include in a hotfix
2. Create a release notes commit on main (changelog + version bump)
3. Cherry-pick everything onto the latest release tag
4. Tag and push the new release
## Step 1: Setup and Gather Information
First, ensure we're on main and up to date:
```bash
git checkout main && git pull origin main
```
Get the latest release tag:
```bash
git tag --sort=-v:refname | head -1
```
## Step 2: Present Commits Since Last Release
Show all commits on main since the last release tag:
```bash
LAST_TAG=$(git tag --sort=-v:refname | head -1)
git log ${LAST_TAG}..HEAD --oneline --format="%h %s (%an)"
```
Also get the commit messages already on the tag (to identify previously cherry-picked commits). Note: Run these as separate commands to avoid shell parsing issues with parentheses in author names:
```bash
LAST_TAG=$(git tag --sort=-v:refname | head -1)
PREV_TAG=$(git tag --sort=-v:refname | head -2 | tail -1)
```
```bash
git log $PREV_TAG..$LAST_TAG --oneline --format="%s"
```
**Present the list** to the user in a numbered format with commit hash, subject, and author. For any commits whose subject line already appears in the tag's history (previously cherry-picked in an earlier hotfix) or are "Release Notes" commits, add `(already in previous hotfix)` or `(release notes - skip)` after them so the user knows to skip those.
Ask which commits to include in the hotfix.
Use the ask_followup_question tool to let the user specify which commits they want (by number or hash).
## Step 3: Analyze Selected Commits
For each selected commit:
1. Get the full commit message: `git show --no-patch --format="%B" <hash>`
2. Get the diff to understand the change: `git show <hash> --stat`
3. Find the associated PR if any: `gh pr list --search "<hash>" --state merged --json number,title --jq '.[0]'`
Build a mental model of what these changes do for the changelog.
## Step 4: Determine New Version Number
Parse the current version from package.json and the last tag:
```bash
LAST_TAG=$(git tag --sort=-v:refname | head -1)
echo "Last release: $LAST_TAG"
cat package.json | grep '"version"'
```
Hotfixes always increment the patch version (e.g., 3.40.0 -> 3.40.1, or 3.40.1 -> 3.40.2).
**Ask the user to confirm the new version number.**
## Step 5: Create Release Notes Commit on Main
On the main branch, create a commit that updates:
1. **CHANGELOG.md** - Add a new section for the hotfix version at the top:
```markdown
## [3.40.1]
- Description of fix 1
- Description of fix 2
```
Write clear, user-friendly descriptions based on your analysis of the commits.
2. **package.json** - Update the version field to the new version
3. No changelog-entry file cleanup is needed. Contributors do not create changelog-entry files in this repo.
**Skip running `npm run install:all`** - release automation handles lockfile consistency as needed.
Commit with message format: `v{VERSION} Release Notes (hotfix)`
In the commit body, mention:
- This is for a hotfix release
- List the cherry-picked commits that will be included
```bash
git add CHANGELOG.md package.json
git commit -m "v3.40.1 Release Notes (hotfix)
Hotfix release including:
- <commit1-hash>: <description>
- <commit2-hash>: <description>
"
```
Push to main:
```bash
git push origin main
```
## Step 6: Build the Hotfix on the Tag
Checkout the last release tag (detached HEAD):
```bash
LAST_TAG=$(git tag --sort=-v:refname | head -1)
git checkout $LAST_TAG
```
Cherry-pick the selected commits in order:
```bash
git cherry-pick <commit1-hash>
git cherry-pick <commit2-hash>
# ... etc
```
Finally, cherry-pick the release notes commit you just pushed to main:
```bash
# Get the hash of the release notes commit (should be HEAD of main)
RELEASE_NOTES_COMMIT=$(git rev-parse main)
git cherry-pick $RELEASE_NOTES_COMMIT
```
## Step 7: Tag and Push
After all cherry-picks are applied successfully:
```bash
# Tag the new release
git tag v{VERSION}
# Push the tag to remote
git push origin v{VERSION}
```
## Step 8: Return to Main and Summary
Return to main branch:
```bash
git checkout main
```
**Copy a Slack announcement message to clipboard** with the version and PR links for each included fix:
```
VS Code Hotfix v{VERSION} Published
- Description of fix 1 https://github.com/cline/cline/pull/{PR_NUMBER}
- Description of fix 2 https://github.com/cline/cline/pull/{PR_NUMBER}
```
Present a final summary:
- New version: v{VERSION}
- Tag pushed: yes
- Commits included: (list them)
- Slack message copied to clipboard: yes
Remind the user to:
1. Manually trigger the publish release GitHub Action at: https://github.com/cline/cline/actions/workflows/publish.yml (paste `v{VERSION}` as the tag)
2. Post the Slack message to announce the hotfix
## Important Notes
- This workflow does NOT create a release branch - only tags
- The release notes commit goes to main first, then gets cherry-picked to the tag
- This keeps main's history accurate while allowing hotfix releases from tags
- If cherry-pick conflicts occur, resolve them before continuing
+2
View File
@@ -347,6 +347,8 @@ A few notes:
So until the settings page is update, and this is added to settings in a way that's clean and doesn't confuse new users, I don't think we can merge this. Please bear with us.
</request_changes_comment>
<request_changes_comment>
Also, don't forget to add a changeset since this fixes a user-facing bug.
The architectural change is solid - moving the focus logic to the command handlers makes sense. Just don't want to introduce subtle timing issues by removing those timeouts.
</request_changes_comment>
</example_comments_that_i_have_written_before>
-64
View File
@@ -1,64 +0,0 @@
# Release
Prepare and publish a release directly from `main`.
## Overview
This workflow helps you:
1. Select/confirm the target version
2. Curate `CHANGELOG.md` entries manually for end users
3. Ensure `package.json` version matches the changelog
4. Create and push a release commit + tag
5. Trigger publish workflow
6. Update GitHub release notes and share a summary
## Process
### 1) Sync and determine version
```bash
git checkout main
git pull origin main
cat package.json | grep '"version"'
```
Confirm the release version with the maintainer (patch/minor/major).
### 2) Curate changelog and version
- Edit `CHANGELOG.md` for the target version using human-friendly release notes.
- Ensure version headers use bracket format, e.g. `## [3.66.1]`.
- Update `package.json` version to the same value.
### 3) Commit and tag
```bash
git add CHANGELOG.md package.json package-lock.json
git commit -m "v<version> Release Notes"
git push origin main
git tag v<version>
git push origin v<version>
```
### 4) Trigger publish workflow
Tell the maintainer to run:
https://github.com/cline/cline/actions/workflows/publish.yml
Use `v<version>` as the release tag.
### 5) Update GitHub release notes
After publish completes:
```bash
gh release view v<version> --json body --jq '.body'
gh release edit v<version> --notes "<final curated release notes>"
```
### 6) Final summary
Provide:
- Released version/tag
- Link to release page
- Summary of top end-user changes
-49
View File
@@ -1,49 +0,0 @@
# THIS IS AUTOGENERATED. DO NOT EDIT MANUALLY
version = 1
name = "cline"
[setup]
script = '''
if [ ! -d "node_modules" ]; then
MAIN_WORKTREE="$(git worktree list | head -n1 | awk '{print $1}')"
ln -s "$MAIN_WORKTREE/node_modules" node_modules
ln -s "$MAIN_WORKTREE/webview-ui/node_modules" webview-ui/node_modules
fi
'''
[[actions]]
name = "VS Code"
icon = "run"
command = "chmod +x ./scripts/run-extension-host.sh && ./scripts/run-extension-host.sh production"
[[actions]]
name = "CLI"
icon = "run"
command = '''
npm run cli:build
npm run cli:run
'''
[[actions]]
name = "npm install"
icon = "tool"
command = '''
rm node_modules
rm webview-ui/node_modules
npm run install:all
'''
[[actions]]
name = "pull main"
icon = "tool"
command = '''
git fetch origin main
if ! git merge-base --is-ancestor main origin/main; then
echo "Local main has commits not on origin/main. Aborting..."
exit 1
fi
git update-ref refs/heads/main refs/remotes/origin/main
echo "main updated to $(git rev-parse --short main)"
'''
-134
View File
@@ -1,134 +0,0 @@
# Cline Development Environment Variables
# Copy this file to .env and fill in your actual values
# Values should be obtained from 1Password shared vault for development
# ============================================================================
# DEVELOPMENT FLAGS
# Recomend not changing these unless you know what you're doing they are set by the launch.json normally
# ============================================================================
# IS_DEV=true
# CLINE_ENVIRONMENT=local
# ============================================================================
# POSTHOG TELEMETRY (Existing)
# ============================================================================
# Get these values from 1Password shared vault
TELEMETRY_SERVICE_API_KEY=your-posthog-telemetry-api-key
ERROR_SERVICE_API_KEY=your-posthog-error-tracking-api-key
# ============================================================================
# OPENTELEMETRY (Optional - for advanced telemetry)
# ============================================================================
# OpenTelemetry provides flexible telemetry collection with multiple export options
# Can run alongside PostHog or independently
# Primary focus: Logs (events), with optional metrics support
# Enable OpenTelemetry (set to 1 to enable)
# OTEL_TELEMETRY_ENABLED=1
# Exporters: "console" for local debugging, "otlp" for remote collector
# Logs are the primary signal (recommended)
# OTEL_LOGS_EXPORTER=console
# OTEL_METRICS_EXPORTER=otlp
# OTLP Protocol: "grpc", "http/json", or "http/protobuf"
# OTEL_EXPORTER_OTLP_PROTOCOL=grpc
# OTLP Endpoint (without /v1/logs or /v1/metrics path - auto-appended)
# For gRPC: use "localhost:4317" (no http:// prefix)
# For HTTP: use "http://localhost:4318"
# OTEL_EXPORTER_OTLP_ENDPOINT=localhost:4317
# OTLP Headers (for authentication, e.g., bearer tokens)
# OTEL_EXPORTER_OTLP_HEADERS=authorization=Bearer your-token-here
# Use insecure gRPC connections (for local testing only, NOT for production)
# OTEL_EXPORTER_OTLP_INSECURE=true
# Metric export interval in milliseconds (default: 60000)
# OTEL_METRIC_EXPORT_INTERVAL=10000
# Batch configuration for logs (optional)
# OTEL_LOG_BATCH_SIZE=512 # Max logs per batch (default: 512)
# OTEL_LOG_BATCH_TIMEOUT=5000 # Max wait time in ms (default: 5000)
# OTEL_LOG_MAX_QUEUE_SIZE=2048 # Max queue size (default: 2048)
# Enable detailed export diagnostics (for debugging)
# TEL_DEBUG_DIAGNOSTICS=true
# Advanced: Separate endpoints for metrics and logs (optional)
# OTEL_EXPORTER_OTLP_METRICS_PROTOCOL=http/protobuf
# OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=http://metrics.example.com:4318
# OTEL_EXPORTER_OTLP_LOGS_PROTOCOL=grpc
# OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=logs.example.com:4317
# Example configurations:
#
# Console debugging (logs only):
# OTEL_TELEMETRY_ENABLED=true
# OTEL_LOGS_EXPORTER=console
# TEL_DEBUG_DIAGNOSTICS=true
#
# OTLP with gRPC (insecure, for local testing):
# OTEL_TELEMETRY_ENABLED=true
# OTEL_LOGS_EXPORTER=otlp
# OTEL_EXPORTER_OTLP_PROTOCOL=grpc
# OTEL_EXPORTER_OTLP_ENDPOINT=localhost:4317
# OTEL_EXPORTER_OTLP_INSECURE=true
# OTEL_EXPORTER_OTLP_HEADERS=authorization=Bearer your-token
#
# OTLP with HTTP/JSON (production):
# OTEL_TELEMETRY_ENABLED=true
# OTEL_LOGS_EXPORTER=otlp
# OTEL_EXPORTER_OTLP_PROTOCOL=http/json
# OTEL_EXPORTER_OTLP_ENDPOINT=https://otel.example.com
# OTEL_EXPORTER_OTLP_HEADERS=authorization=Bearer your-token
# ============================================================================
# OBJECT STORE CONFIGURATION
# ============================================================================
# TO ENABLE S3 OR R2 STORAGE, UNCOMMENT AND FILL IN THE FOLLOWING:
# CLINE_STORAGE_ADAPTER="s3" # Options: "s3" or "r2"
# CLINE_STORAGE_BUCKET="cline"
# CLINE_STORAGE_ACCESS_KEY_ID="key"
# CLINE_STORAGE_SECRET_ACCESS_KEY="secrets"
#
# [OPTIONAL FIELDS FOR R2]
# CLINE_STORAGE_ACCOUNT_ID = "account-id"
# Default R2 endpoint (if not set): "https://<CLINE_STORAGE_ACCOUNT_ID>.r2.cloudflarestorage.com"
# CLINE_STORAGE_ENDPOINT = "http://localhost:8333"
#
# [OPTIONAL FIELDS FOR S3]
# CLINE_STORAGE_REGION = "us-west-1" # AWS Bucket Region (default: "us-east-1")
# Default S3 endpoint (if not set): "https://s3.<CLINE_STORAGE_REGION>.amazonaws.com"
# CLINE_STORAGE_ENDPOINT = "http://localhost:8333"
#
# [OPTIONAL FIELDS FOR ALL STORAGE TYPES]
# CLINE_STORAGE_SYNC_INTERVAL_MS = 30000 # Interval for sync worker in milliseconds
# CLINE_STORAGE_SYNC_MAX_RETRIES = 5 # Max retries for failed sync operations
# CLINE_STORAGE_SYNC_BATCH_SIZE = 10 # Number of files to sync in each batch
# CLINE_STORAGE_SYNC_BACKFILL_ENABLED = false # Enable backfill of existing data on startup
# ============================================================================
# OPTIONAL DEVELOPMENT SETTINGS
# ============================================================================
# Uncomment and modify as needed for development
# Multi-root workspace debugging
# MULTI_ROOT_TRACE=true
# gRPC recorder for testing
# GRPC_RECORDER_ENABLED=true
# GRPC_RECORDER_FILE_NAME=test-recording
# Test mode
# E2E_TEST=true
# IS_TEST=true
# ============================================================================
# USAGE INSTRUCTIONS
# ============================================================================
# 1. Copy this file: cp .env.example .env
# 2. Get PostHog keys from 1Password shared vault
# 3. Update the values in .env
# 4. The .env file is gitignored for security
-1
View File
@@ -1,5 +1,4 @@
demo.gif filter=lfs diff=lfs merge=lfs -text
assets/docs/demo.gif filter=lfs diff=lfs merge=lfs -text
webview-ui/src/assets/cline_kanban_demo.webm filter=lfs diff=lfs merge=lfs -text
* text=auto eol=lf
+3 -2
View File
@@ -1,2 +1,3 @@
/.github/ @saoudrizwan @arafatkatze @maxpaulus43 @candieduniverse
/README.md @saoudrizwan @juanpflores
/docs/
/.github/ @saoudrizwan @garoth @sjf
/README.md @saoudrizwan @nickbaumann98
-1
View File
@@ -14,7 +14,6 @@ body:
options:
- VSCode Extension
- JetBrains Plugin
- CLI
default: 0
validations:
required: true
-58
View File
@@ -1,58 +0,0 @@
# Copilot Instructions for Cline
This is a VS Code extension. Read `.clinerules/general.md` for tribal knowledge and nuanced patterns.
## Architecture
- **Core** (`src/`): `extension.ts``WebviewProvider``Controller` (single source of truth) → `Task` (agent loop).
- **Webview** (`webview-ui/`): React/Vite app. State via `ExtensionStateContext.tsx`, synced through message passing.
- **CLI** (`cli/`): React Ink terminal UI sharing core logic. Update CLI when changing webview features.
- **Communication**: Protobuf-defined gRPC-like protocol over VS Code message passing. Schemas in `proto/`.
- **MCP**: `src/services/mcp/McpHub.ts`.
## Build & Test (Critical — non-obvious commands)
- **Build**: `npm run compile` — NOT `npm run build`.
- **Watch**: `npm run watch` (extension + webview).
- **Protos**: `npm run protos` — run **immediately** after any `.proto` change. Generates into `src/shared/proto/`, `src/generated/`.
- **Tests**: `npm run test:unit`. After prompt/tool changes: `UPDATE_SNAPSHOTS=true npm run test:unit`.
## Protobuf RPC Workflow (4 steps)
1. **Define** in `proto/cline/*.proto`. Naming: `PascalCaseService`, `camelCase` RPCs, `PascalCase` Messages. Use `common.proto` shared types for simple data.
2. **Generate**: `npm run protos`.
3. **Backend handler**: `src/core/controller/<domain>/`.
4. **Frontend call**: `UiServiceClient.myMethod(Request.create({...}))`.
- Adding enums (e.g. `ClineSay`) → also update `src/shared/proto-conversions/cline-message.ts`.
## Adding API Providers (silent failure risk)
Three proto conversion updates are **required** or the provider silently resets to Anthropic:
1. `proto/cline/models.proto` — add to `ApiProvider` enum.
2. `convertApiProviderToProto()` in `src/shared/proto-conversions/models/api-configuration-conversion.ts`.
3. `convertProtoToApiProvider()` in the same file.
Also update: `src/shared/api.ts`, `src/shared/providers/providers.json`, `src/core/api/index.ts`, `webview-ui/.../providerUtils.ts`, `webview-ui/.../validate.ts`, `webview-ui/.../ApiOptions.tsx`, and `cli/src/components/ModelPicker.tsx`.
For Responses API providers: add to `isNextGenModelProvider()` in `src/utils/model-utils.ts` and set `apiFormat: ApiFormat.OPENAI_RESPONSES` on models.
## Adding Tools to System Prompt (5+ file chain)
1. Add enum to `ClineDefaultTool` in `src/shared/tools.ts`.
2. Create definition in `src/core/prompts/system-prompt/tools/` (export `[GENERIC]` minimum).
3. Register in `src/core/prompts/system-prompt/tools/init.ts`.
4. Whitelist in `src/core/prompts/system-prompt/variants/*/config.ts` for each model family.
5. Handler in `src/core/task/tools/handlers/`, wire in `ToolExecutor.ts`.
6. If tool has UI: add `ClineSay` enum in proto → `ExtensionMessage.ts``cline-message.ts``ChatRow.tsx`.
7. Regenerate snapshots: `UPDATE_SNAPSHOTS=true npm run test:unit`.
## Modifying System Prompt
Modular: `components/` (shared) + `variants/` (model-specific) + `templates/` (`{{PLACEHOLDER}}`). Variants override components via `componentOverrides` in `config.ts` or custom `template.ts`. XS variant is heavily condensed inline. Always regenerate snapshots after changes.
## Global State Keys (silent failure risk)
Adding a key requires: type in `src/shared/storage/state-keys.ts`, read via `context.globalState.get()` in `src/core/storage/utils/state-helpers.ts` `readGlobalStateFromDisk()`, and add to return object. Missing the `.get()` call compiles fine but value is always `undefined`.
## Slash Commands (3 places)
- `src/core/slash-commands/index.ts` — definitions.
- `src/core/prompts/commands.ts` — system prompt integration.
- `webview-ui/src/utils/slash-commands.ts` — webview autocomplete.
## Conventions
- **Paths**: Always use `src/utils/path` helpers (`toPosixString`) for cross-platform compatibility.
- **Logging**: `src/shared/services/Logger.ts`.
- **Feature flags**: See PR #7566 as reference pattern.
+1
View File
@@ -60,6 +60,7 @@ We're not looking for exhaustive documentation - just evidence that you've thoug
- [ ] Changes are limited to a single feature, bugfix or chore (split larger changes into separate PRs)
- [ ] Tests are passing (`npm test`) and code is formatted and linted (`npm run format && npm run lint`)
- [ ] I have created a changeset using `npm run changeset` (required for user-facing changes)
- [ ] I have reviewed [contributor guidelines](https://github.com/cline/cline/blob/main/CONTRIBUTING.md)
### Screenshots
@@ -0,0 +1,79 @@
"""
This script updates a specific version's release notes section in CHANGELOG.md with new content
or reformats existing content.
The script:
1. Takes a version number, changelog path, and optionally new content as input from environment variables
2. Finds the section in the changelog for the specified version
3. Either:
a) Replaces the content with new content if provided, or
b) Reformats existing content by:
- Removing the first two lines of the changeset format
- Ensuring version numbers are wrapped in square brackets
4. Writes the updated changelog back to the file
Environment Variables:
CHANGELOG_PATH: Path to the changelog file (defaults to 'CHANGELOG.md')
VERSION: The version number to update/format
PREV_VERSION: The previous version number (used to locate section boundaries)
NEW_CONTENT: Optional new content to insert for this version
"""
#!/usr/bin/env python3
import os
CHANGELOG_PATH = os.environ.get("CHANGELOG_PATH", "CHANGELOG.md")
VERSION = os.environ['VERSION']
PREV_VERSION = os.environ.get("PREV_VERSION", "")
NEW_CONTENT = os.environ.get("NEW_CONTENT", "")
def overwrite_changelog_section(changelog_text: str, new_content: str):
# Find the section for the specified version
version_pattern = f"## {VERSION}\n"
unformmatted_prev_version_pattern = f"## {PREV_VERSION}\n"
prev_version_pattern = f"## [{PREV_VERSION}]\n"
print(f"latest version: {VERSION}")
print(f"prev_version: {PREV_VERSION}")
notes_start_index = changelog_text.find(version_pattern) + len(version_pattern)
notes_end_index = changelog_text.find(prev_version_pattern, notes_start_index) if PREV_VERSION and (prev_version_pattern in changelog_text or unformmatted_prev_version_pattern in changelog_text) else len(changelog_text)
if new_content:
return changelog_text[:notes_start_index] + f"{new_content}\n" + changelog_text[notes_end_index:]
else:
changeset_lines = changelog_text[notes_start_index:notes_end_index].split("\n")
filtered_lines = []
for line in changeset_lines:
# If the previous line is a changeset format
if len(filtered_lines) > 1 and filtered_lines[-1].startswith("### "):
# Remove the last two lines from the filted_lines
filtered_lines.pop()
filtered_lines.pop()
else:
filtered_lines.append(line.strip())
# Prepend a new line to the first line of filtered_lines
if filtered_lines:
filtered_lines[0] = "\n" + filtered_lines[0]
# Print filted_lines wiht a "\n" at the end of each line
for line in filtered_lines:
print(line.strip())
parsed_lines = "\n".join(line for line in filtered_lines)
updated_changelog = changelog_text[:notes_start_index] + parsed_lines + changelog_text[notes_end_index:]
return updated_changelog
with open(CHANGELOG_PATH, 'r') as f:
changelog_content = f.read()
new_changelog = overwrite_changelog_section(changelog_content, NEW_CONTENT)
# print("----------------------------------------------------------------------------------")
# print(new_changelog)
# print("----------------------------------------------------------------------------------")
# Write back to CHANGELOG.md
with open(CHANGELOG_PATH, 'w') as f:
f.write(new_changelog)
print(f"{CHANGELOG_PATH} updated successfully!")
+113
View File
@@ -0,0 +1,113 @@
name: Changeset Converter
run-name: Changeset Conversion
on:
workflow_dispatch:
pull_request:
types: [closed]
env:
REPO_PATH: ${{ github.repository }}
GIT_REF: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || 'main' }}
NODE_VERSION: 20.18.1
jobs:
# Job 1: Create version bump PR when changesets are merged to main
changeset-pr-version-bump:
if: |
github.event_name == 'workflow_dispatch' ||
(
github.event_name == 'pull_request' &&
github.event.pull_request.merged == true &&
github.event.pull_request.base.ref == 'main' &&
github.actor != 'github-actions'
)
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Check user for team affiliation
id: team_check
if: github.event_name == 'workflow_dispatch'
uses: morfien101/actions-authorized-user@4a3cfbf0bcb3cafe4a71710a278920c5d94bb38b
with:
username: ${{ github.actor }}
org: ${{ github.repository_owner }}
team: "deployer"
github_token: ${{ secrets.GITHUB_TOKEN }}
- name: Check if user is authorized
if: github.event_name == 'workflow_dispatch'
run: |
if [ "${{ steps.team_check.outputs.authorized }}" != "true" ]; then
echo "User is not authorized to run this workflow."
exit 1
fi
- name: Git Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ env.GIT_REF }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: "npm"
- name: Install Dependencies
run: npm install changeset
# Check if there are any new changesets to process
- name: Check for changesets
id: check-changesets
run: |
NEW_CHANGESETS=$(find .changeset -name "*.md" ! -name "README.md" | wc -l | tr -d ' ')
echo "Changesets diff with previous version: $NEW_CHANGESETS"
echo "new_changesets=$NEW_CHANGESETS" >> $GITHUB_OUTPUT
# Create version bump PR using changesets/action if there are new changesets
- name: Create Changeset Pull Request
if: steps.check-changesets.outputs.new_changesets != '0'
uses: changesets/action@v1
with:
commit: "changeset version bump"
title: "Changeset version bump"
version: npm run version-packages # This performs the changeset version bump
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Get current and previous versions to edit changelog entry
- name: Get version
id: get_version
run: |
VERSION=$(git show HEAD:package.json | jq -r '.version')
echo "version=$VERSION" >> $GITHUB_OUTPUT
PREV_VERSION=$(git show origin/main:package.json | jq -r '.version')
echo "prev_version=$PREV_VERSION" >> $GITHUB_OUTPUT
echo "version=$VERSION"
echo "prev_version=$PREV_VERSION"
# Update CHANGELOG.md with proper format
- name: Update Changelog Format
env:
VERSION: ${{ steps.get_version.outputs.version }}
PREV_VERSION: ${{ steps.get_version.outputs.prev_version }}
run: python .github/scripts/overwrite_changeset_changelog.py
# Commit and push changelog updates
- name: Push Changelog updates to Pull Request
run: |
git config user.name "github-actions"
git config user.email github-actions@github.com
echo "Running git add and commit..."
git add CHANGELOG.md
git commit -m "Updating CHANGELOG.md format"
git status
echo "--------------------------------------------------------------------------------"
echo "Pushing to remote..."
echo "--------------------------------------------------------------------------------"
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
git push origin $CURRENT_BRANCH
-83
View File
@@ -1,83 +0,0 @@
name: CLI TUI Tests
on:
pull_request:
branches:
- main
workflow_dispatch:
workflow_call:
permissions:
contents: read
jobs:
cli-tui-tests:
name: CLI TUI Tests
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
- name: Install dependencies
run: npm ci
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build CLI
run: npm run cli:build
- name: Run TUI Tests
id: tui_tests
run: |
npm run test:e2e:cli:tui 2>&1 | tee tui-test-output.log
exit_code=${PIPESTATUS[0]}
echo "tui_exit_code=$exit_code" >> $GITHUB_OUTPUT
exit $exit_code
- name: Write failure summary
if: always() && steps.tui_tests.outcome != 'success' && steps.tui_tests.outcome != 'skipped'
run: |
echo "## ❌ CLI TUI Tests Failed" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Step outcome:** \`${{ steps.tui_tests.outcome }}\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Test Output" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
if [ -f tui-test-output.log ]; then
cat tui-test-output.log >> $GITHUB_STEP_SUMMARY
else
echo "(no test output captured — process may have been killed before output was flushed)" >> $GITHUB_STEP_SUMMARY
fi
echo '```' >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Debugging" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **TUI traces** are attached as artifacts below — download and inspect them to see terminal state at the point of failure." >> $GITHUB_STEP_SUMMARY
echo "- **To view a trace replay/Run a TUI Trace: ** run \`npx tui-test show-trace path/to/trace/file\` in your terminal" >> $GITHUB_STEP_SUMMARY
echo "- **Full test log** is also attached as an artifact." >> $GITHUB_STEP_SUMMARY
echo "- Tests run with \`retries: 2\` so any failure shown is a consistent failure, not a flake." >> $GITHUB_STEP_SUMMARY
- name: Upload TUI traces
if: always() && steps.tui_tests.outcome != 'success' && steps.tui_tests.outcome != 'skipped'
uses: actions/upload-artifact@v4
with:
name: tui-test-traces
path: tests/e2e/cli/tui-traces/
retention-days: 14
if-no-files-found: warn
- name: Upload test log
if: always() && steps.tui_tests.outcome != 'success' && steps.tui_tests.outcome != 'skipped'
uses: actions/upload-artifact@v4
with:
name: tui-test-log
path: tui-test-output.log
retention-days: 14
if-no-files-found: warn
@@ -1,85 +0,0 @@
name: Smoke Tests
on:
push:
branches: [main]
paths:
- 'src/core/**'
- 'src/shared/**'
- 'proto/**'
- 'evals/**'
- '.github/workflows/cline-evals-regression.yml'
pull_request:
paths:
- 'src/core/**'
- 'src/shared/**'
- 'proto/**'
- 'evals/**'
- '.github/workflows/cline-evals-regression.yml'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: smoke-tests-${{ github.ref }}
cancel-in-progress: true
jobs:
smoke-tests:
name: Smoke Tests
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build and install CLI
run: |
npm run protos
cd cli && npm install && npm run build && npm link
echo "$(npm config get prefix)/bin" >> $GITHUB_PATH
- name: Verify CLI
run: cline --version
- name: Run smoke tests
env:
CLINE_API_KEY: ${{ secrets.CLINE_API_KEY }}
run: |
cline auth -p cline -k "$CLINE_API_KEY" -m "anthropic/claude-sonnet-4.5"
max_attempts=3
for attempt in $(seq 1 $max_attempts); do
echo "::group::Attempt $attempt of $max_attempts"
if npx tsx evals/smoke-tests/run-smoke-tests.ts --trials 1 --parallel; then
echo "::endgroup::"
echo "Smoke tests passed on attempt $attempt"
exit 0
fi
echo "::endgroup::"
if [ $attempt -lt $max_attempts ]; then
echo "::warning::Smoke tests failed on attempt $attempt, retrying..."
sleep 10
fi
done
echo "::error::Smoke tests failed after $max_attempts attempts"
exit 1
- name: Generate summary
if: always()
run: cat evals/smoke-tests/results/latest/summary.md >> $GITHUB_STEP_SUMMARY
- name: Upload results
uses: actions/upload-artifact@v4
if: always()
with:
name: smoke-test-results-${{ github.run_id }}
path: evals/smoke-tests/results/latest/
retention-days: 30
+2 -5
View File
@@ -80,16 +80,13 @@ jobs:
playwright-browsers-${{ runner.os }}-
- name: Install root dependencies
if: steps.root-cache.outputs.cache-hit != 'true'
run: npm ci
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install webview-ui dependencies
if: steps.webview-cache.outputs.cache-hit != 'true'
run: cd webview-ui && npm ci
- name: Install vsce
run: npm install -g @vscode/vsce
- name: Install xvfb on Linux
if: matrix.runner == 'ubuntu'
run: sudo apt-get update && sudo apt-get install -y xvfb
@@ -1,53 +0,0 @@
name: Auto-label Issues
on:
issues:
types: [opened, edited]
jobs:
label:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- uses: actions/github-script@v7
with:
script: |
const body = context.payload.issue.body || '';
const labels = context.payload.issue.labels.map(l => l.name);
// Check if JetBrains Plugin is selected
if (body.match(/###\s*Plugin Type\s*\n+JetBrains Plugin/i)) {
if (!labels.includes('JetBrains')) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ['JetBrains']
});
}
}
// Check if VSCode Extension is selected
if (body.match(/###\s*Plugin Type\s*\n+VSCode Extension/i)) {
if (!labels.includes('VS Code')) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ['VS Code']
});
}
}
// Check if CLI is selected
if (body.match(/###\s*Plugin Type\s*\n+CLI/i)) {
if (!labels.includes('CLI')) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ['CLI']
});
}
}
-93
View File
@@ -1,93 +0,0 @@
name: Publish NPM Release
on:
workflow_call:
inputs:
confirm_publish:
description: 'Type "publish" to confirm you want to publish to NPM'
required: true
type: string
permissions:
contents: write # Required for pushing tags
id-token: write # Required for npm trusted publishing (OIDC)
checks: write # Required by test workflow
pull-requests: write # Required by test workflow
jobs:
test:
uses: ./.github/workflows/test.yml
publish-npm-release:
needs: test
name: Publish Cline CLI to NPM
if: github.repository == 'cline/cline' && github.ref == 'refs/heads/main' && inputs.confirm_publish == 'publish'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "24.x"
registry-url: "https://registry.npmjs.org"
- name: Install root dependencies and CLI dependencies
run: npm ci --include=optional # this will also install cli deps because "cli" in included in root package.json workspaces field
- name: Generate Protos
run: npm run protos
- name: Read release version
id: version
run: |
# Read version from cli/package.json
VERSION=$(node -p "require('./cli/package.json').version")
echo "Release version: $VERSION"
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Build standalone NPM package
env:
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
CLINE_ENVIRONMENT: production
OTEL_TELEMETRY_ENABLED: "1"
OTEL_METRICS_EXPORTER: otlp
OTEL_LOGS_EXPORTER: otlp
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 }}
run: node scripts/package-npm.mjs
- name: Verify build output
run: |
echo "Checking dist-standalone directory..."
ls -la dist-standalone/
echo "Verifying CLI binaries..."
ls -lh cli/bin/cline-* || echo "Warning: CLI binaries not found"
echo "Checking package.json in dist-standalone..."
cat dist-standalone/package.json | grep version
- name: Publish to NPM with latest tag
run: |
echo "Publishing version ${{ steps.version.outputs.version }} to NPM with tag 'latest'..."
cd dist-standalone
npm publish --tag latest --access public
- name: Tag release
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag "v${{ steps.version.outputs.version }}-cli"
git push origin "v${{ steps.version.outputs.version }}-cli"
- name: Summary
run: |
echo "✅ Successfully published cline@${{ steps.version.outputs.version }} to NPM with tag 'latest'"
echo ""
echo "📦 Install with: npm install -g cline"
echo "🔗 NPM: https://www.npmjs.com/package/cline/v/${{ steps.version.outputs.version }}"
-134
View File
@@ -1,134 +0,0 @@
name: Publish NPM Nightly
on:
workflow_call:
inputs:
force_publish:
description: "Force publish even if there are no commits in the last 24 hours"
required: false
type: boolean
default: false
permissions:
contents: read
id-token: write # Required for npm trusted publishing (OIDC)
checks: write # Required by test workflow
pull-requests: write # Required by test workflow
jobs:
test:
uses: ./.github/workflows/test.yml
publish-npm-nightly:
needs: test
name: Publish Cline CLI (Nightly) to NPM
if: github.repository == 'cline/cline' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Check for recent commits
id: check_commits
run: |
if [ "${{ inputs.force_publish }}" = "true" ]; then
echo "force_publish enabled, proceeding with publish"
echo "skip=false" >> $GITHUB_OUTPUT
exit 0
fi
if [ $(git rev-list --count HEAD --since="24 hours ago") -eq 0 ]; then
echo "No commits in last 24 hours, skipping publish"
echo "skip=true" >> $GITHUB_OUTPUT
else
echo "Found recent commits, proceeding with publish"
echo "skip=false" >> $GITHUB_OUTPUT
fi
- name: Setup Node.js
if: steps.check_commits.outputs.skip != 'true'
uses: actions/setup-node@v4
with:
node-version: "24.x"
registry-url: "https://registry.npmjs.org"
- name: Install root dependencies and CLI dependencies
if: steps.check_commits.outputs.skip != 'true'
run: npm ci --include=optional # this will also install cli deps because "cli" in included in root package.json workspaces field
- name: Generate Protos
if: steps.check_commits.outputs.skip != 'true'
run: npm run protos
- name: Generate nightly version with timestamp
if: steps.check_commits.outputs.skip != 'true'
id: version
run: |
# Read base version from cli/package.json (e.g., "2.0.0")
BASE_VERSION=$(node -p "require('./cli/package.json').version")
# Generate timestamp (Unix epoch seconds)
TIMESTAMP=$(date +%s)
# Create unique nightly version: 1.0.9-nightly.1736365200
VERSION="${BASE_VERSION}-nightly.${TIMESTAMP}"
echo "Base version: $BASE_VERSION"
echo "Generated nightly version: $VERSION"
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Update cli/package.json with nightly version
if: steps.check_commits.outputs.skip != 'true'
run: |
# Update version with timestamp-based nightly version
node -e "
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('cli/package.json', 'utf8'));
pkg.version = '${{ steps.version.outputs.version }}';
fs.writeFileSync('cli/package.json', JSON.stringify(pkg, null, '\t'));
"
echo "Using version ${{ steps.version.outputs.version }} for build"
cat cli/package.json | grep '"version"'
- name: Build and package CLI
if: steps.check_commits.outputs.skip != 'true'
env:
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
CLINE_ENVIRONMENT: production
OTEL_TELEMETRY_ENABLED: "1"
OTEL_METRICS_EXPORTER: otlp
OTEL_LOGS_EXPORTER: otlp
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 }}
run: node scripts/package-npm.mjs
- name: Verify build output
if: steps.check_commits.outputs.skip != 'true'
run: |
echo "Checking dist-standalone directory..."
ls -la dist-standalone/
echo "Verifying CLI binaries..."
ls -lh cli/bin/cline-* || echo "Warning: CLI binaries not found"
echo "Checking package.json in dist-standalone..."
cat dist-standalone/package.json | grep version
- name: Publish to NPM with nightly tag
if: steps.check_commits.outputs.skip != 'true'
run: |
echo "Publishing version ${{ steps.version.outputs.version }} to NPM with tag 'nightly'..."
cd dist-standalone
npm publish --tag nightly --access public
- name: Summary
if: steps.check_commits.outputs.skip != 'true'
run: |
echo "✅ Successfully published cline@${{ steps.version.outputs.version }} to NPM with tag 'nightly'"
echo ""
echo "📦 Install with: npm install -g cline@nightly"
echo "🔗 NPM: https://www.npmjs.com/package/cline/v/${{ steps.version.outputs.version }}"
-215
View File
@@ -1,215 +0,0 @@
# Build and Pack CLI
#
# Builds a CLI tarball from any branch/commit and publishes it as a GitHub Release.
# Requires write access to the repository (maintainers/collaborators only).
#
# Security: Split into two jobs to isolate untrusted build code from write tokens.
# The build job runs arbitrary ref code with zero permissions. The release job
# only runs trusted GitHub Actions with write scope.
#
# Usage (helper script, auto-detects current branch):
# ./scripts/build-cli-artifact.sh
# ./scripts/build-cli-artifact.sh feature/my-changes
# ./scripts/build-cli-artifact.sh feature/my-changes 1234 # comments on PR
#
# Usage (gh CLI directly):
# gh workflow run pack-cli.yml -f ref=main
# gh workflow run pack-cli.yml -f ref=abc123 -f pr_number=1234
#
# Install the built CLI (no auth required):
# npm install -g https://github.com/cline/cline/releases/download/cli-build-<sha>/cline-<ver>.tgz
#
# Find releases:
# gh release list --limit 10
name: Build and Pack CLI
permissions:
contents: read
on:
workflow_dispatch:
inputs:
ref:
description: 'Branch, tag, or commit SHA to build (leave empty for default branch)'
required: false
type: string
pr_number:
description: 'PR number to comment on with install instructions (optional)'
required: false
type: number
jobs:
# ── Build job: runs untrusted ref code with ZERO permissions ──
build:
name: Build CLI
runs-on: ubuntu-latest
permissions: {}
outputs:
commit_sha: ${{ steps.commit.outputs.sha }}
tarball: ${{ steps.pack.outputs.tarball }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
persist-credentials: false
- name: Get commit SHA
id: commit
run: |
COMMIT_SHA=$(git rev-parse --short HEAD)
echo "sha=$COMMIT_SHA" >> $GITHUB_OUTPUT
echo "Building from commit: $COMMIT_SHA"
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20.x"
- name: Install dependencies
run: npm ci --include=optional
- name: Generate Protos
run: npm run protos
- name: Build standalone package
run: node scripts/package-npm.mjs
- name: Create Tarball
id: pack
run: |
cd dist-standalone
TARBALL=$(npm pack)
echo "tarball=$TARBALL" >> $GITHUB_OUTPUT
echo "Created tarball: $TARBALL"
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: cli-tarball
path: dist-standalone/*.tgz
# ── Release job: only trusted Actions code, with write permissions ──
release:
name: Release CLI
needs: build
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
steps:
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: cli-tarball
path: dist-standalone
- name: Create GitHub Release
id: create_release
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const path = require('path');
const commit = '${{ needs.build.outputs.commit_sha }}';
const tarball = '${{ needs.build.outputs.tarball }}';
// Delete existing release/tag if re-running for the same commit
const tagName = `cli-build-${commit}`;
try {
const existing = await github.rest.repos.getReleaseByTag({
owner: context.repo.owner,
repo: context.repo.repo,
tag: tagName
});
await github.rest.repos.deleteRelease({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: existing.data.id
});
await github.rest.git.deleteRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: `tags/${tagName}`
});
core.info(`Deleted existing release for ${tagName}`);
} catch (e) {
// Release doesn't exist yet, that's fine
}
// Create a release
const release = await github.rest.repos.createRelease({
owner: context.repo.owner,
repo: context.repo.repo,
tag_name: tagName,
name: `CLI Build (${commit})`,
body: `Automated CLI build from commit ${commit}\n\nInstall with:\n\`\`\`bash\nnpm install -g https://github.com/${context.repo.owner}/${context.repo.repo}/releases/download/${tagName}/${tarball}\n\`\`\``,
draft: false,
prerelease: true
});
// Upload the tarball as a release asset
const tarballPath = path.join('dist-standalone', tarball);
const tarballData = fs.readFileSync(tarballPath);
await github.rest.repos.uploadReleaseAsset({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: release.data.id,
name: tarball,
data: tarballData
});
const downloadUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/releases/download/${tagName}/${tarball}`;
core.setOutput('release_url', release.data.html_url);
core.setOutput('download_url', downloadUrl);
- name: Comment on PR with download instructions
if: inputs.pr_number != ''
uses: actions/github-script@v7
with:
script: |
const commit = '${{ needs.build.outputs.commit_sha }}';
const releaseUrl = '${{ steps.create_release.outputs.release_url }}';
const downloadUrl = '${{ steps.create_release.outputs.download_url }}';
const prNumber = ${{ inputs.pr_number || 0 }};
if (!prNumber) return;
const comment = `## 📦 CLI Build Ready
A CLI build has been created for commit \`${commit}\`.
### Install Directly from URL (No Authentication Required!)
\`\`\`bash
npm install -g ${downloadUrl}
\`\`\`
### Alternative: Download and Install
\`\`\`bash
curl -L ${downloadUrl} -o cline.tgz
npm install -g ./cline.tgz
\`\`\`
📦 [View Release](${releaseUrl})
`;
await github.rest.issues.createComment({
issue_number: prNumber,
owner: context.repo.owner,
repo: context.repo.repo,
body: comment
});
- name: Summary
run: |
echo "✅ CLI build complete!"
echo ""
echo "📦 Release: ${{ steps.create_release.outputs.release_url }}"
echo "🔗 Download URL: ${{ steps.create_release.outputs.download_url }}"
echo ""
echo "Install from anywhere (no authentication required):"
echo " npm install -g ${{ steps.create_release.outputs.download_url }}"
@@ -1,60 +0,0 @@
name: Publish CLI (Trusted)
on:
schedule:
- cron: "0 12 * * *" # 4 AM PST (UTC-8) = 12 UTC
workflow_dispatch:
inputs:
publish_target:
description: "Which publish flow to run"
required: true
default: "main"
type: choice
options:
- main
- nightly
confirm_publish:
description: 'Required when publish_target=main. Type "publish" to confirm release publish.'
required: false
type: string
force_nightly_publish:
description: "Force nightly publish even with no commits in last 24h"
required: false
type: boolean
default: false
permissions:
id-token: write # Required for npm trusted publishing (OIDC)
contents: write # Required because npm-main creates/pushes git tags
checks: write # Required by nested reusable test workflow
pull-requests: write # Required by nested reusable test workflow
jobs:
cli-tui-tests:
uses: ./.github/workflows/cli-tui-tests.yml
publish-main:
needs: cli-tui-tests
if: |
github.repository == 'cline/cline' && (
github.event_name == 'workflow_dispatch' &&
github.event.inputs.publish_target == 'main' &&
github.event.inputs.confirm_publish == 'publish' &&
!endsWith(github.actor, '[bot]')
)
uses: ./.github/workflows/npm-main.yaml
secrets: inherit
with:
confirm_publish: ${{ github.event.inputs.confirm_publish }}
publish-nightly:
needs: cli-tui-tests
if: |
github.repository == 'cline/cline' && (
github.event_name == 'schedule' ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.publish_target == 'nightly')
)
uses: ./.github/workflows/npm-nightly.yaml
secrets: inherit
with:
force_publish: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.force_nightly_publish == 'true' }}
+20 -21
View File
@@ -24,8 +24,6 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
lfs: true
- name: Check for recent commits
run: |
@@ -38,27 +36,35 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
# Keep publish environment aligned with test workflow/tooling lockfile expectations.
# Newer LTS (Node 24 / npm 11) can make `npm list` fail with ELSPROBLEMS during vsce packaging.
node-version: 22
node-version: "lts/*"
# Cache root dependencies - only reuse if package-lock.json exactly matches
- name: Cache root dependencies
uses: actions/cache@v4
id: root-cache
with:
path: node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
# Cache webview-ui dependencies - only reuse if package-lock.json exactly matches
- name: Cache webview-ui dependencies
uses: actions/cache@v4
id: webview-cache
with:
path: webview-ui/node_modules
key: ${{ runner.os }}-npm-webview-${{ hashFiles('webview-ui/package-lock.json') }}
- name: Install root dependencies
if: steps.root-cache.outputs.cache-hit != 'true'
run: npm ci --include=optional
- name: Install webview-ui dependencies
if: steps.webview-cache.outputs.cache-hit != 'true'
run: cd webview-ui && npm ci --include=optional
- name: Install Publishing Tools
run: npm install -g @vscode/vsce ovsx
- name: Verify LFS media assets are resolved
run: |
FILE="webview-ui/src/assets/cline_kanban_demo.webm"
if grep -q "git-lfs.github.com/spec/v1" "$FILE"; then
echo "Error: $FILE is still a Git LFS pointer in CI checkout"
exit 1
fi
- name: Publish Extension as Pre-release
env:
VSCE_PAT: ${{ secrets.VSCE_PAT }}
@@ -66,11 +72,4 @@ jobs:
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
CLINE_ENVIRONMENT: production
# OpenTelemetry production defaults (can be overridden at runtime)
OTEL_TELEMETRY_ENABLED: ${{ secrets.OTEL_TELEMETRY_ENABLED }}
OTEL_LOGS_EXPORTER: otlp
OTEL_METRICS_EXPORTER: otlp
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 }}
run: npm run publish:marketplace:nightly
run: npm run publish:marketplace:nightly
+44 -112
View File
@@ -11,13 +11,8 @@ on:
options:
- pre-release
- release
auto_create_tag_from_main:
description: "Auto-create and push the provided tag from the tested main commit (recommended)"
required: true
default: true
type: boolean
tag:
description: "Tag to publish (required in both modes, e.g., v3.1.2)"
description: "Enter existing tag to publish (e.g., v3.1.2)"
required: true
type: string
@@ -40,80 +35,36 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: main
fetch-depth: 0
fetch-tags: true
lfs: true
- name: Resolve Release Tag
id: resolve_tag
run: |
TAG="${{ github.event.inputs.tag }}"
AUTO_CREATE="${{ github.event.inputs.auto_create_tag_from_main }}"
TESTED_SHA="${{ github.sha }}"
WORKFLOW_REF="${{ github.ref }}"
if [[ -z "$TAG" ]]; then
echo "Error: tag input is required"
exit 1
fi
if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z.]+)?$ ]]; then
echo "Error: tag must match vX.Y.Z (optionally with -suffix or .suffix)"
exit 1
fi
TAG_REF="refs/tags/$TAG"
git fetch origin main --tags
if [[ "$AUTO_CREATE" == "true" ]]; then
if [[ "$WORKFLOW_REF" != "refs/heads/main" ]]; then
echo "Error: auto-create mode requires dispatching from main (current ref: $WORKFLOW_REF)"
exit 1
fi
echo "Auto-create enabled. Using tested workflow SHA: $TESTED_SHA"
if ! git merge-base --is-ancestor "$TESTED_SHA" origin/main; then
echo "Error: tested SHA $TESTED_SHA is not on origin/main"
exit 1
fi
if git show-ref --verify --quiet "$TAG_REF"; then
TAG_SHA=$(git rev-list -n 1 "$TAG_REF^{commit}")
if [[ "$TAG_SHA" != "$TESTED_SHA" ]]; then
echo "Error: tag '$TAG' already exists at $TAG_SHA, not at tested SHA ($TESTED_SHA)"
exit 1
fi
echo "Tag '$TAG' already exists at tested SHA. Continuing."
else
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag "$TAG" "$TESTED_SHA"
git push origin "$TAG_REF"
echo "Created and pushed tag '$TAG' from tested SHA $TESTED_SHA."
fi
else
if ! git show-ref --verify --quiet "$TAG_REF"; then
echo "Error: tag '$TAG' does not exist in the repository"
exit 1
fi
echo "Using existing tag '$TAG'."
fi
git checkout --detach "$TAG_REF^{commit}"
echo "tag=$TAG" >> $GITHUB_OUTPUT
echo "resolved_sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
ref: ${{ github.event.inputs.tag }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
node-version: "lts/*"
# Cache root dependencies - only reuse if package-lock.json exactly matches
- name: Cache root dependencies
uses: actions/cache@v4
id: root-cache
with:
path: node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
# Cache webview-ui dependencies - only reuse if package-lock.json exactly matches
- name: Cache webview-ui dependencies
uses: actions/cache@v4
id: webview-cache
with:
path: webview-ui/node_modules
key: ${{ runner.os }}-npm-webview-${{ hashFiles('webview-ui/package-lock.json') }}
- name: Install root dependencies
run: npm install --include=optional
if: steps.root-cache.outputs.cache-hit != 'true'
run: npm ci --include=optional
- name: Install webview-ui dependencies
run: cd webview-ui && npm install --include=optional
if: steps.webview-cache.outputs.cache-hit != 'true'
run: cd webview-ui && npm ci --include=optional
- name: Install Publishing Tools
run: npm install -g @vscode/vsce ovsx
@@ -124,24 +75,21 @@ jobs:
VERSION=$(node -p "require('./package.json').version")
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Verify Tag Matches Package Version
- name: Validate Tag
id: validate_tag
run: |
TAG="${{ steps.resolve_tag.outputs.tag }}"
VERSION="v${{ steps.get_version.outputs.version }}"
if [[ "$TAG" != "$VERSION" ]]; then
echo "Error: tag '$TAG' does not match package version '$VERSION'"
exit 1
fi
echo "Tag and package version match: $TAG"
TAG="${{ github.event.inputs.tag }}"
echo "tag=$TAG" >> $GITHUB_OUTPUT
echo "Using existing tag: $TAG"
- name: Verify LFS media assets are resolved
run: |
FILE="webview-ui/src/assets/cline_kanban_demo.webm"
if grep -q "git-lfs.github.com/spec/v1" "$FILE"; then
echo "Error: $FILE is still a Git LFS pointer in CI checkout"
# Verify the tag exists
if ! git rev-parse "$TAG" >/dev/null 2>&1; then
echo "Error: Tag '$TAG' does not exist in the repository"
exit 1
fi
echo "Tag '$TAG' validated successfully"
- name: Package and Publish Extension
env:
VSCE_PAT: ${{ secrets.VSCE_PAT }}
@@ -149,13 +97,6 @@ jobs:
CLINE_ENVIRONMENT: production
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
# OpenTelemetry production defaults (can be overridden at runtime)
OTEL_TELEMETRY_ENABLED: ${{ secrets.OTEL_TELEMETRY_ENABLED }}
OTEL_LOGS_EXPORTER: otlp
OTEL_METRICS_EXPORTER: otlp
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 }}
run: |
# Required to generate the .vsix
vsce package --allow-package-secrets sendgrid --out "cline-${{ steps.get_version.outputs.version }}.vsix"
@@ -168,31 +109,22 @@ jobs:
echo "Successfully published release version ${{ steps.get_version.outputs.version }} to VS Code Marketplace and Open VSX Registry"
fi
- name: Get Previous Tag
id: prev_tag
run: |
CURRENT_TAG="${{ steps.resolve_tag.outputs.tag }}"
PREV_TAG=$(git describe --tags --abbrev=0 "$CURRENT_TAG^" 2>/dev/null || echo "")
echo "prev_tag=$PREV_TAG" >> $GITHUB_OUTPUT
- name: Get Changelog Entry
id: changelog
run: |
# Get content between first ## [ and second ## [
CONTENT=$(awk '/^## \[/{if(found) exit; found=1; next} found{print}' CHANGELOG.md)
echo "content<<EOF" >> $GITHUB_OUTPUT
echo "$CONTENT" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
# - name: Get Changelog Entry
# id: changelog
# uses: mindsers/changelog-reader-action@v2
# with:
# # This expects a standard Keep a Changelog format
# # "latest" means it will read whichever is the most recent version
# # set in "## [1.2.3] - 2025-01-28" style
# version: latest
- name: Create GitHub Release
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ steps.resolve_tag.outputs.tag }}
tag_name: ${{ steps.validate_tag.outputs.tag }}
files: "*.vsix"
body: |
${{ steps.changelog.outputs.content }}
**Full Changelog**: https://github.com/${{ github.repository }}/compare/${{ steps.prev_tag.outputs.prev_tag }}...${{ steps.resolve_tag.outputs.tag }}
# body: ${{ steps.changelog.outputs.content }}
generate_release_notes: true
prerelease: ${{ github.event.inputs.release-type == 'pre-release' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+84 -37
View File
@@ -28,17 +28,27 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
cache-dependency-path: |
package-lock.json
webview-ui/package-lock.json
- name: Cache root dependencies
uses: actions/cache@v4
id: root-cache
with:
path: node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
- name: Cache webview-ui dependencies
uses: actions/cache@v4
id: webview-cache
with:
path: webview-ui/node_modules
key: ${{ runner.os }}-npm-webview-${{ hashFiles('webview-ui/package-lock.json') }}
- name: Install root dependencies
if: steps.root-cache.outputs.cache-hit != 'true'
run: npm ci
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install webview-ui dependencies
if: steps.webview-cache.outputs.cache-hit != 'true'
run: cd webview-ui && npm ci
- name: Run Quality Checks (Parallel)
@@ -63,17 +73,27 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
cache-dependency-path: |
package-lock.json
webview-ui/package-lock.json
- name: Cache root dependencies
uses: actions/cache@v4
id: root-cache
with:
path: node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
- name: Cache webview-ui dependencies
uses: actions/cache@v4
id: webview-cache
with:
path: webview-ui/node_modules
key: ${{ runner.os }}-npm-webview-${{ hashFiles('webview-ui/package-lock.json') }}
- name: Install root dependencies
if: steps.root-cache.outputs.cache-hit != 'true'
run: npm ci
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install webview-ui dependencies
if: steps.webview-cache.outputs.cache-hit != 'true'
run: cd webview-ui && npm ci
- name: Set up NPM on Windows
@@ -83,42 +103,52 @@ jobs:
# Build the extension and tests (without redundant checks)
- name: Build Tests and Extension
id: build_step
run: npm run ci:build
- name: Unit Tests with coverage - Linux
id: unit_tests_linux
if: ${{ !cancelled() && steps.build_step.outcome == 'success' && runner.os == 'Linux' }}
continue-on-error: true
if: runner.os == 'Linux'
run: |
npx nyc --nycrc-path .nycrc.unit.json --reporter=lcov npm run test:unit
- name: Unit Tests - Non-Linux
id: unit_tests_non_linux
if: ${{ !cancelled() && steps.build_step.outcome == 'success' && runner.os != 'Linux' }}
continue-on-error: true
if: runner.os != 'Linux'
run: |
npm run test:unit
- name: Extension Integration Tests - Linux
id: integration_tests_linux
if: ${{ !cancelled() && steps.build_step.outcome == 'success' && runner.os == 'Linux' }}
continue-on-error: true
if: runner.os == 'Linux'
run: xvfb-run -a npm run test:coverage
- name: Extension Integration Tests - Non-Linux
id: integration_tests_non_linux
if: ${{ !cancelled() && steps.build_step.outcome == 'success' && runner.os != 'Linux' }}
continue-on-error: true
if: runner.os != 'Linux'
run: npm run test:integration
- name: Webview Tests with Coverage
id: webview_tests
if: ${{ !cancelled() && steps.build_step.outcome == 'success' }}
continue-on-error: true
run: |
cd webview-ui
npm run test:coverage
- name: CLI Tests
id: cli_tests
if: ${{ !cancelled() && steps.build_step.outcome == 'success' }}
run: cd cli && npm run test:run
- name: Check Test Results
if: always()
run: |
failed=""
[[ "${{ steps.unit_tests_linux.outcome }}" == "failure" && "${{ runner.os }}" == "Linux" ]] && failed="$failed unit_tests_linux"
[[ "${{ steps.unit_tests_non_linux.outcome }}" == "failure" && "${{ runner.os }}" != "Linux" ]] && failed="$failed unit_tests_non_linux"
[[ "${{ steps.integration_tests_linux.outcome }}" == "failure" && "${{ runner.os }}" == "Linux" ]] && failed="$failed integration_tests_linux"
[[ "${{ steps.integration_tests_non_linux.outcome }}" == "failure" && "${{ runner.os }}" != "Linux" ]] && failed="$failed integration_tests_non_linux"
[[ "${{ steps.webview_tests.outcome }}" == "failure" ]] && failed="$failed webview_tests"
[[ -n "$failed" ]] && { echo "❌ The following test suites failed:$failed"; exit 1; }
echo "✅ All tests passed"
- name: Save Coverage Reports
uses: actions/upload-artifact@v4
@@ -141,32 +171,52 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
cache-dependency-path: |
package-lock.json
webview-ui/package-lock.json
testing-platform/package-lock.json
- name: Cache root dependencies
uses: actions/cache@v4
id: root-cache
with:
path: node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
- name: Cache webview-ui dependencies
uses: actions/cache@v4
id: webview-cache
with:
path: webview-ui/node_modules
key: ${{ runner.os }}-npm-webview-${{ hashFiles('webview-ui/package-lock.json') }}
# Cache testing-platform dependencies
- name: Cache testing-platform dependencies
uses: actions/cache@v4
id: testing-platform-cache
with:
path: testing-platform/node_modules
key: ${{ runner.os }}-npm-testing-platform-${{ hashFiles('testing-platform/package-lock.json') }}
- name: Install root dependencies
if: steps.root-cache.outputs.cache-hit != 'true'
run: npm ci
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install webview-ui dependencies
if: steps.webview-cache.outputs.cache-hit != 'true'
run: cd webview-ui && npm ci
- name: Download ripgrep binaries
run: npm run download-ripgrep
- name: Compile Standalone
- name: Compile standalone
run: npm run compile-standalone
- name: Install testing platform dependencies
if: steps.testing-platform-cache.outputs.cache-hit != 'true'
run: cd testing-platform && npm ci
- name: Running testing platform integration spec tests
continue-on-error: true
timeout-minutes: 7
run: npm run test:tp-orchestrator -- tests/specs/ --count=1 --coverage
# Temporarily wrapping the test command to always return a neutral exit code.
# This prevents the job from showing as failed and avoids distracting developers
# until the integration tests are ready to be enforced.
run: |
npm run test:tp-orchestrator -- tests/specs/ --count=1 --coverage || true
- name: Save Coverage Reports
uses: actions/upload-artifact@v4
@@ -209,14 +259,11 @@ jobs:
- name: Download test platform integration core coverage artifact
uses: actions/download-artifact@v4
continue-on-error: true
id: download-integration-coverage
with:
name: test-platform-integration-core-coverage
path: integration-core-coverage-reports
- name: Upload core integration tests coverage to Qlty
if: steps.download-integration-coverage.outcome == 'success'
uses: qltysh/qlty-action/coverage@v2
with:
token: ${{ secrets.QLTY_COVERAGE_TOKEN }}
+11 -56
View File
@@ -1,27 +1,17 @@
name: Trigger Jetbrains Plugin <-> Cline Tests
on:
pull_request_target:
types: [opened, reopened]
issue_comment:
types: [created]
pull_request:
types: [opened, synchronize, reopened]
permissions:
contents: read
pull-requests: read
concurrency:
group: jetbrains-trigger-${{ github.event.pull_request.number || github.event.issue.number }}
group: jetbrains-trigger-${{ github.event.number }}
cancel-in-progress: true
jobs:
trigger-integration-test:
name: Run Tests
runs-on: ubuntu-latest
# Run on PR open/reopen, or when someone comments /test-jetbrains on a PR
if: |
github.event_name == 'pull_request_target' ||
(github.event_name == 'issue_comment' &&
github.event.issue.pull_request &&
contains(github.event.comment.body, '/test-jetbrains') &&
contains(fromJSON('["MEMBER","OWNER","COLLABORATOR"]'), github.event.comment.author_association))
steps:
- name: Generate GitHub App Token
id: app-token
@@ -32,39 +22,7 @@ jobs:
owner: cline
repositories: intellij-plugin
- name: Get PR details (for issue_comment trigger)
id: pr-details
if: github.event_name == 'issue_comment'
env:
GH_TOKEN: ${{ github.token }}
run: |
PR_DATA=$(gh api repos/${{ github.repository }}/pulls/${{ github.event.issue.number }})
echo "head_ref=$(echo "$PR_DATA" | jq -r '.head.ref')" >> $GITHUB_OUTPUT
echo "head_sha=$(echo "$PR_DATA" | jq -r '.head.sha')" >> $GITHUB_OUTPUT
echo "title=$(echo "$PR_DATA" | jq -r '.title')" >> $GITHUB_OUTPUT
echo "html_url=$(echo "$PR_DATA" | jq -r '.html_url')" >> $GITHUB_OUTPUT
- name: Sanitize untrusted inputs
id: sanitize
env:
RAW_BRANCH_NAME: ${{ github.event_name == 'pull_request_target' && github.head_ref || steps.pr-details.outputs.head_ref }}
RAW_PR_TITLE: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.title || steps.pr-details.outputs.title }}
run: |
# Sanitize branch name for JSON
BRANCH_NAME_JSON=$(jq -n --arg b "$RAW_BRANCH_NAME" '$b')
echo "branch_name=$BRANCH_NAME_JSON" >> $GITHUB_OUTPUT
# Sanitize PR title for JSON
PR_TITLE_JSON=$(jq -n --arg t "$RAW_PR_TITLE" '$t')
echo "pr_title=$PR_TITLE_JSON" >> $GITHUB_OUTPUT
- name: Trigger IntelliJ Plugin Integration Test
env:
BRANCH_NAME: ${{ steps.sanitize.outputs.branch_name }}
PR_TITLE: ${{ steps.sanitize.outputs.pr_title }}
PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }}
PR_SHA: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || steps.pr-details.outputs.head_sha }}
PR_URL: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.html_url || steps.pr-details.outputs.html_url }}
run: |
curl -X POST \
-H "Authorization: Bearer ${{ steps.app-token.outputs.token }}" \
@@ -76,23 +34,20 @@ jobs:
{
"event_type": "cline-pr-check",
"client_payload": {
"pr_number": "$PR_NUMBER",
"branch_name": $BRANCH_NAME,
"pr_number": "${{ github.event.number }}",
"branch_name": "${{ github.head_ref }}",
"action": "${{ github.event.action }}",
"sha": "$PR_SHA",
"pr_title": $PR_TITLE,
"pr_url": "$PR_URL"
"sha": "${{ github.event.pull_request.head.sha }}",
"pr_title": ${{ toJSON(github.event.pull_request.title) }},
"pr_url": "${{ github.event.pull_request.html_url }}"
}
}
EOF
- name: Log trigger details
env:
PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }}
PR_SHA: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || steps.pr-details.outputs.head_sha }}
run: |
echo "Triggered IntelliJ Plugin integration test for:"
echo " PR #$PR_NUMBER"
echo " Trigger: ${{ github.event_name }}"
echo " PR #${{ github.event.number }}: ${{ github.event.pull_request.title }}"
echo " Branch: ${{ github.head_ref }}"
echo " Action: ${{ github.event.action }}"
echo " SHA: $PR_SHA"
echo " SHA: ${{ github.event.pull_request.head.sha }}"
+2 -21
View File
@@ -8,14 +8,12 @@ tmp
.DS_Store
.idea
.husky/_/
pnpm-lock.yaml
.clineignore
.venv
.actrc
CLAUDE.local.md
webview-ui/src/**/*.js
webview-ui/src/**/*.js.map
@@ -28,31 +26,14 @@ coverage-unit
!.github/scripts/coverage/
*evals.env
.env
.secrets
.github/act/.secrets
.worktrees
## Generated files ##
src/generated/
src/shared/proto/
webview-ui/src/services/grpc-client.ts
*.tsbuildinfo
# E2E Tests
test-results
/.github/act
/pkg
.secrets
*.tsbuildinfo
# Smoke test results (generated)
evals/smoke-tests/results/
.tui-test
secrets.json
tui-traces
tests/**/cache
## CLI pre-release ##
/cli
-3
View File
@@ -1,3 +0,0 @@
[submodule "evals/cline-bench"]
path = evals/cline-bench
url = https://github.com/cline/cline-bench.git
+5 -45
View File
@@ -12,18 +12,13 @@
"args": [
"--extensionDevelopmentPath=${workspaceFolder}",
"--disable-workspace-trust",
"--disable-extension",
"saoudrizwan.claude-dev", // Avoid conflicts with installed Cline
"--disable-extension",
"saoudrizwan.cline-nightly", // Avoid conflicts with installed Cline Nightly
"${workspaceFolder}",
"--disable-extensions"
"--disable-extensions", // Avoid conflicts with installed extensions
"${workspaceFolder}"
],
"outFiles": [
"${workspaceFolder}/dist/**/*.js"
],
"preLaunchTask": "${defaultBuildTask}",
"envFile": "${workspaceFolder}/.env",
"env": {
"IS_DEV": "true",
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}",
@@ -37,17 +32,13 @@
"args": [
"--extensionDevelopmentPath=${workspaceFolder}",
"--disable-workspace-trust",
"--disable-extension",
"saoudrizwan.claude-dev", // Avoid conflicts with installed Cline
"--disable-extension",
"saoudrizwan.cline-nightly", // Avoid conflicts with installed Cline Nightly
"--disable-extensions", // Avoid conflicts with installed extensions
"${workspaceFolder}"
],
"outFiles": [
"${workspaceFolder}/dist/**/*.js"
],
"preLaunchTask": "${defaultBuildTask}",
"envFile": "${workspaceFolder}/.env",
"env": {
"IS_DEV": "true",
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}",
@@ -61,17 +52,13 @@
"args": [
"--extensionDevelopmentPath=${workspaceFolder}",
"--disable-workspace-trust",
"--disable-extension",
"saoudrizwan.claude-dev", // Avoid conflicts with installed Cline
"--disable-extension",
"saoudrizwan.cline-nightly", // Avoid conflicts with installed Cline Nightly
"--disable-extensions", // Avoid conflicts with installed extensions
"${workspaceFolder}"
],
"outFiles": [
"${workspaceFolder}/dist/**/*.js"
],
"preLaunchTask": "${defaultBuildTask}",
"envFile": "${workspaceFolder}/.env",
"env": {
"IS_DEV": "true",
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}",
@@ -87,10 +74,7 @@
"--user-data-dir=${workspaceFolder}/dist/tmp/user",
"--profile-temp",
"--sync=off",
"--disable-extension",
"saoudrizwan.claude-dev", // Avoid conflicts with installed Cline
"--disable-extension",
"saoudrizwan.cline-nightly", // Avoid conflicts with installed Cline Nightly
"--disable-extensions", // Avoid conflicts with installed extensions
"--extensionDevelopmentPath=${workspaceFolder}",
"${workspaceFolder}"
],
@@ -100,7 +84,6 @@
"preLaunchTask": "clean-tmp-user",
"internalConsoleOptions": "openOnSessionStart",
"postDebugTask": "stop",
"envFile": "${workspaceFolder}/.env",
"env": {
"IS_DEV": "true",
"TEMP_PROFILE": "true",
@@ -131,7 +114,6 @@
"tsx"
],
"program": "scripts/test-standalone-core-api-server.ts",
"envFile": "${workspaceFolder}/.env",
"env": {
"PROTOBUS_PORT": "26040",
"HOSTBRIDGE_PORT": "26041",
@@ -169,7 +151,6 @@
"--exit",
"${file}"
],
"envFile": "${workspaceFolder}/.env",
"env": {
"TS_NODE_PROJECT": "./tsconfig.unit-test.json",
"NODE_ENV": "test",
@@ -178,27 +159,6 @@
},
"console": "integratedTerminal",
"internalConsoleOptions": "openOnSessionStart"
},
{
"name": "Open Storybook",
"type": "node",
"request": "launch",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"storybook"
],
"cwd": "${workspaceFolder}/webview-ui",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"serverReadyAction": {
"pattern": "Local:.*http://localhost:([0-9]+)",
"uriFormat": "http://localhost:%s",
"action": "openExternally"
},
"env": {
"IS_DEV": "true"
}
}
]
}
+1 -3
View File
@@ -27,7 +27,5 @@
"source.removeUnused.biome": "always",
"source.removeUnusedImports": "always",
"source.organizeImports.biome": "always"
},
// Disable auto-forwarding ports to prevent Simple Browser from opening the Vite dev server
"remote.autoForwardPorts": false
}
}
-20
View File
@@ -263,26 +263,6 @@
"watch"
],
"command": "rm -rf ${workspaceFolder}/dist/tmp/user && mkdir -p ${workspaceFolder}/dist/tmp/user"
},
{
"type": "npm",
"script": "storybook",
"group": "build",
"problemMatcher": [],
"isBackground": false,
"label": "npm: storybook",
"dependsOn": [
"npm: protos",
"npm: build:webview"
],
"presentation": {
"reveal": "always"
},
"options": {
"env": {
"IS_DEV": "true"
}
}
}
],
"inputs": [
+3 -7
View File
@@ -1,8 +1,6 @@
# Default
.vscode/**
.vscode-test/**
.worktrees/**
CLAUDE.local.md
out/
dist-standalone/
node_modules/
@@ -20,10 +18,6 @@ tsconfig*.json
eslint-rules/**
.github/**
.husky/**
.env
# cli
cli/**
# Custom
**/demo.gif
@@ -35,9 +29,11 @@ cli/**
eslint-rules/
old_docs/
evals/
.changie.yaml
.codespellrc
.mocharc.json
buf.yaml
.changeset/
.clinerules/
# Ignore all webview-ui files except the build directory (https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/frameworks/hello-world-react-cra/.vscodeignore)
@@ -73,4 +69,4 @@ test-results/
**/*.stories.tsx
*storybook.log
storybook-static
**/StorybookDecorator.tsx
**/StorybookDecorator.tsx
-1
View File
@@ -1 +0,0 @@
.gitignore
-765
View File
@@ -1,770 +1,5 @@
# Changelog
## [3.76.0]
### Added
- Add Cline Kanban launch modal in webview; CLI now launches Kanban by default with a migration view
- Add toggle to disable feature tips in chat
- Add repeated tool call loop detection to prevent infinite loops wasting tokens
### Fixed
- Fix CLI Kanban spawn on Windows by enabling shell mode for `npx.cmd`
## [3.75.0]
### Added
- Latency improvements for remote workspaces
### Fixed
- Stabilize flaky hooks tests
### Changed
- Remove example hooks in favor of reading the docs
## [3.74.0]
### Added
- Implement dynamic free model detection for Cline API
- Add file read deduplication cache to prevent repeated reads
- Add feature tips tooltip during thinking state
### Fixed
- Replace error message when not logged in to Cline
- Align ClineRulesToggleModal padding with ServersToggleModal
- Skip WebP for GLM and Devstral models running through llama.cpp
- Respect user-configured context window in LiteLLM getModel()
- Honor explicit model IDs outside static catalog in W&B provider
- Add missing Fireworks serverless models and pricing
## [3.73.0]
### Added
- Added W&B Inference by CoreWeave as a new API provider with 17 models
- Improved parallel tool calling support for OpenRouter and Cline providers
### Fixed
- Claude Code Provider: handle rate limit events, empty content arrays, error results, and unknown content types without crashing
- Tool handlers (`read_file`, `list_files`, `list_code_definition_names`, `search_files`) now return graceful errors instead of crashing
## [3.72.0]
### Added
- Added Anthropic Opus 4.6 fast mode variants
### Fixed
- Resolved native tool placeholder interpolation in prompts
- Gemini: capped Flash output tokens to 8192 across providers
- Fixed Windows unit test path normalization
- Fixed flaky hooks tests on Windows
- Bedrock: handle thinking and redacted_thinking blocks correctly in message conversion and streaming
- Prevent crash when `list_files` or `list_code_definition_names` receives a file path
### Changed
- Updated Jupyter Notebook GIFs
- Markdown image loading now requires user consent
- Added `.github/copilot-instructions.md` for coding agents
- Hooks: reintroduced feature toggle
## [3.71.0]
### Added
- Added GPT-5.4 models for ChatGPT subscription users
- Hooks: Added a `Notification` hook for attention and completion boundaries
### Fixed
- Handle streamable HTTP MCP reconnects more reliably after disconnects
## [3.70.0]
### Added
- New Cline API docs: Getting Started, Auth, Chat Completions, Models, Errors, and SDK Examples
- Hook payloads now include `model.provider` and `model.slug`
- Token/cost updates now happen immediately as usage chunks arrive, not after tool execution
### Fixed
- Improve subagent context compaction logic
- Subagent stream retry delay increased to reduce noise from transient failures
- State serialization errors are now caught and logged instead of crashing
- Removed incorrect `max_tokens` from OpenRouter requests
### Changed
- Windows test cleanup now retries on locked files and applies per-test timeouts
- Updated hooks docs
## [3.69.0]
### Added
- Add `User-Agent` header to requests sent to the Cline backend
- Add default auto-tag workflow for publish release flow
- Show Cline SDK docs on the Cline page
### Fixed
- Retry nested git restore and prevent silent `.git_disabled` leftovers in checkpoints
- Prevent Chinese filename escaping in diff view
- Trigger auto-compaction on OpenRouter context overflow errors
- Restore GPT-OSS native file editing on OpenAI-compatible models
### Changed
- Update Cline SDK docs
- Improve hooks support for Windows PowerShell
## [3.68.0]
### Added
- Add dynamic Cline provider model fetching from Cline endpoint
- Add additional Markdown formatting in CLI
- Add focus indicator on action buttons in extension
### Fixed
- Clear all OCA secrets on auth refresh failure to prevent re-auth loops
- Resolve "Could not find the file context" error in Explain Changes
- Use `JSON_SCHEMA` for `yaml.load` to prevent unsafe deserialization
- Fetch model info from API in CLI headless auth for Cline and Vercel providers
- Generate commit message from staged changes only when staging exists
- Update stale `maxTokens` values for Claude 3.7+ models across Anthropic, Bedrock, Vertex, and SAP AI Core
- Use `model.info.maxTokens` for OpenRouter instead of hardcoded `8192`
### Changed
- Increase timeout for a flaky test to reduce short-term test instability
## [3.67.1]
### Added
- Added Cline SDK API interface for programmatic access to Cline features and tools, enabling integration into custom applications.
- Added Codex 5.3 model support
### Fixed
- Fix OpenAI Codex by setting `store` to `false`
- Use `isLocatedInPath()` instead of string matching for path containment checks
## [3.67.0]
### Added
- Add support for skills and optional modelId in subagent configuration
- Add AgentConfigLoader for file-based agent configs
- Add Responses API support for OpenAI native provider
- Preconnect websocket to reduce response latency
- Fetch featured models from backend with local fallback
- Add /q command to quit CLI
- Add MCP enterprise configuration details
- Pull Cline's recommended models from internal endpoint
- Add dynamic flag to adjust banner cache duration
### Fixed
- Fix reasoning delta crash on usage-only stream chunks
- Fix OpenAI tool ID transformation restricted to native provider only
- Fix auth check for ACP mode
- Fix CLI yolo mode to not persist yolo setting to disk
- Fix inline focus-chain slider within its feature row
- Fix Gemini 3.1 Pro compatibility
- Fix Cline auth with ACP flag
### Changed
- Move PR skill to .agents/skills
- SambaNova provider: update models list
- Remove changeset-converter GitHub Action and npm run changeset
## [3.66.0]
### Added
- Gemini-3.1 Pro Preview
## [3.65.0]
### Added
- Add /skills slash command to CLI for viewing and managing installed skills
### Fixed
- Fix aggressive context compaction caused by accidental clicks on the context window progress bar silently setting a very low auto-condense threshold
- Fix infinite retry loop when write_to_file fails with missing content parameter.
- Fixed default claude model
## [3.64.0]
### Added
- Added sonnet 4.6
## [3.63.0]
### Added
- added zai GLM 5 Free promo
### Fixed
- Restore reasoning trace visibility in chat and improve the thinking row UX so reasoning is visible, then collapsible after completion.
## [3.62.0]
### Fixed
- Banners now display immediately when opening the extension instead of requiring user interaction first
- Resolved 17 security vulnerabilities including high-severity DoS issues in dependencies (body-parser, axios, qs, tar, and others)
## [3.61.0]
- UI/UX fixes with minimax model family
## [3.60.0]
- Fixes for Minimax model family
## [3.59.0]
- Added Minimax 2.5 Free Promo
- Fixed Response chaining for OpenAI's Responses API
## [3.58.0]
### Added
- Subagent: replace legacy subagents with the native `use_subagents` tool
- Bundle `endpoints.json` support so packaged distributions can ship required endpoints out-of-the-box
- Amazon Bedrock: support parallel tool calling
- New "double-check completion" experimental feature to verify work before marking tasks complete
- CLI: new task controls/flags including custom `--thinking` token budget and `--max-consecutive-mistakes` for yolo runs
- Remote config: new UI/options (including connection/test buttons) and support for syncing deletion of remotely configured MCP servers
- Vertex / Claude Code: add 1M context model options for Claude Opus 4.6
- ZAI/GLM: add GLM-5
### Fixed
- CLI: handle stdin redirection correctly in CI/headless environments
- CLI: preserve OAuth callback paths during auth redirects
- VS Code Web: generate auth callback URLs via `vscode.env.asExternalUri` (OAuth callback reliability)
- Terminal: surface command exit codes in results and improve long-running `execute_command` timeout behavior
- UI: add loading indicator and fix `api_req_started` rendering
- Task streaming: prevent duplicate streamed text rows after completion
- API: preserve selected Vercel model when model metadata is missing
- Telemetry: route PostHog networking through proxy-aware shared fetch and ensure telemetry flushes on shutdown
- CI: increase Windows E2E test timeout to reduce flakiness
### Changed
- Settings/model UX: move "reasoning effort" into model configuration and expose it in settings
- CLI provider selection: limit provider list to those remotely configured
- UI: consolidate ViewHeader component/styling across views
- Tools: add auto-approval support for `attempt_completion` commands
- Remotely configured MCP server schema now supports custom headers
## [3.57.1]
### Fixed
- Fixed Opus 4.6 for bedrock provider
## [3.57.0]
### Added
- Cline CLI 2.0 now available. Install with `npm install -g cline`
- Anthopic Opus 4.6
- Minimax-2.1 and Kimi-k2.5 now available for free for a limited time promo
- Codex-5.3 through ChatGPT subscription
### Fixed
- Fix read file tool to support reading large files
- Fix decimal input crash in OpenAI Compatible price fields (#8129)
- Fix build complete handlers when updating the api config
- Fixed missing provider from list
- Fixed Favorite Icon / Star from getting clipped in the task history view
### Changed
- Make skills always enabled and remove feature toggle setting
## [3.56.0]
### Added
- **CLI authentication:** Added Vercel AI Gateway and Cline API key provider support for headless CI/automation workflows
- **New model:** Added Kimi-K2.5 model to Moonshot provider (262K context, image support, prompt caching)
- **Prompt variant:** Added Trinity Large prompt variant for improved tool-calling support
- **OpenTelemetry:** Added support for custom headers on metrics and logs endpoints
- **Social links:** Added community icons (X, Discord, GitHub, Reddit, LinkedIn) to the What's New modal
### Fixed
- **LiteLLM:** Fixed thinking configuration not appearing for reasoning-capable models
- **OpenTelemetry:** Fixed endpoint path handling (no longer incorrectly appends `/v1/logs` or `/v1/metrics`) and ensured logs are sent regardless of VSCode telemetry settings
- **CLI auth:** Fixed `cline auth` displaying incorrect provider information after configuration
### Changed
- **Hooks:** Hook scripts now run from the workspace repository root instead of filesystem root
- **Default settings:** Enabled multi-root workspaces, parallel tool calling, and skills by default; disabled strict plan mode by default
- **Settings UI:** Refreshed feature settings section with collapsible design
## [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
## [3.53.0]
### Fixed
- Removed grok model from free tier
## [3.52.0]
### Added
- Users with ChatGPT Plus or Pro subscriptions can now use GPT-5 models directly through Cline without needing an API key. Authentication is handled via OAuth through OpenAI's authentication system.
- Grok models are now moving out of free tier and into paid plans.
- Introduces comprehensive Jupyter Notebook support for Cline, enabling AI-assisted editing of `.ipynb` files with full cell-level context awareness.
### Fixed
- Bugs in DiffViewProvider for file editing
- Ollama's recommended models to use correct identifiers
## [3.51.0]
### Added
- Adding OpenAI gpt-5.2-codex model to the model picker
## [3.50.0]
### Added
- Add gpt-5.2-codex OpenAI model support
- Add create-pull-request skill
### Fixed
- Fix the selection of remotely configured providers
- Fix act_mode_respond to prevent consecutive calls
- Fix invalid tool call IDs when switching between model formats
## [3.49.1]
### Added
- Add telemetry to track usage of skills feature
- Add version headers to Cline backend requests
- Phase in Responses API usage instead of defaulting for every supported model
### Fixed
- Fix workflow slash command search to be case-insensitive
- Fix model display in ModelPickerModal when using LiteLLM
- Fix LiteLLM model fetching with default base URL
- Fix crash when OpenAI-compatible APIs send usage chunks with empty or null choices arrays at end of streaming
- Fix model ID for Kat Coder Pro Free model
## [3.49.0]
- Enable configuring an OTEL collector at runtime
- Removing Minimax-2.1 from free model list as the free trial has ended
- Improved image display in MCP responses
- Auto-sync remote MCP servers from remote config to local settings
## [3.48.0]
### Added
- Add Skills system for reusable, on-demand agent instructions
- Add new websearch tooling in Cline provider
- Add zai-glm-4.7 to Cerebras model list
- Add model refresh and improve reasoning support for Vercel AI Gateway
### Fixed
- Revert #8341 due to regressions in diff view/document truncation (see #8423, #8429)
- Fixed extension crash when using context menu selector
## [3.47.0]
### Added
- Added experimental support for Background Edits (allows editing files in background without opening the diff view)
- Updated free model to MiniMax M2.1 (replacing MiniMax M2)
- Added support for Azure based identity authentication in OpenAI Compatible provider and Azure OpenAI
- Add `supportsReasoning` property to Baseten models
### Fixed
- Prevent expired token usage in authenticated requests
- Exclude binary files without extensions from diffs
- Preserve file endings and trailing newlines
- Fix Cerebras rate limiting
- Fix Auto Compact for Claude Code provider
- Make Workspace and Favorites history filters independent
- Fix remote MCP server connection failures (404 response handling)
- Disable native tool calling for Deepseek 3.2 speciale
- Show notification instead of opening sidebar on update
- Fix Baseten model selector
### Refactored
- Modify prompts for parallel tool usage in Claude and Gemini 3 models
## [3.46.1]
### Fixed
- Remove GLM 4.6 from free models
## [3.46.0]
### Added
- Added GLM 4.7 model
- Enhanced background terminal execution with command tracking, log file output, zombie process prevention (10-minute timeout), and clickable log paths in UI
- Apply Patch tool for GPT-5+ models (replacing current diff edit tools)
### Fixed
- Duplicate error messages during streaming for Diff Edit tool when Parallel Tool Calling is not enabled
- Banner carousel styling and dismiss functionality
- Typos in Gemini system prompt overrides
- Model picker favorites ordering, star toggle, and keyboard navigation for OpenRouter and Vercel AI Gateway providers
- Fetch remote config values from the cache
### Refactored
- Anthropic handler to use metadata for reasoning support
- Bedrock provider to use metadata for reasoning support
## [3.45.1]
- Fixed MCP settings race condition where toggling auto-approve or changing timeout settings would cause the UI to flash and revert
## [3.45.0]
- Added Gemini 3 Flash Preview model
## [3.44.2]
- Polished the model picker UI with checkmarks for selected models, tooltips on Plan/Act tabs, and consistent arrow pointers across all popup modals
- Improved WhatsNew modal responsiveness and cleaned up redundant UI elements
- Fixed GLM models outputting garbled text in thinking tags—reasoning is now properly disabled for these models
## [3.44.1]
- Fixed a critical bug where local MCP servers stopped connecting after v3.42.0—all user-configured stdio-based MCP servers should now work again
- Fixed remotely configured API keys not being extracted correctly for enterprise users
- Added support for dynamic tool instructions that adapt based on runtime context, laying groundwork for future context-aware features
## [3.44.0]
## Added
- Updating minor version to show a proper banner for the release
## [3.43.1]
### Patch Changes
- Fix GLM-4.6 Model reference id
## [3.43.0]
### Added
- GLM-4.6
- kat-coder-pro
- Add parsing of env variable patterns to the mcpconfig.json
### Fixed
- TLS Proxy support issues for VSCode
- Add supportsReasoning flag to OpenAI reasoning models
- Fix thinking not available for some models in the OpenAI provider
- Fix invalid signature field issues when switching between Gemini and Anthropic providers
- Extract OpenRouter model filtering into reusable utility and use it in different model pickers
- Fix a11y for auto approve checkbox
- Improve ModelPickerModal provider list layout
### Refactored
- Migrate WhatsNewModal to new shared dialogue component
## [3.42.0]
### Added
- Expose `getAvailableSlashCommands` rpc endpoint to UI clients
- Made slash command menu and context menu accessible and screenreader-friendly
- Made expanding/collapsing UI components accessible
### Fixed
- Devstral OpenRouter model ID and routing issues
- Incorrect pricing display for Devstral model in the extension
## [3.41.0]
### Added
- OpenAI GPT-5.2
- Devstral-2512 (formerly stealth model "Microwave")
- Improvements to chat modal model picker
- Amazon Nova 2 Lite
- DeepSeek 3.2 to native tool calling allow list
- Responses API support for Codex models in OpenAI provider (requires native tool calling)
- Xmas Special Santa Cline
- Welcome screen UI enhancements
### Fixed
- Initial checkpoint commit now non-blocking for improved responsiveness in large repositories
- Gemini Vertex models erroring when thinking parameters are not supported
- Restrictive file permissions for secrets.json
- Ollama streaming requests not aborting when task is cancelled
### Refactored
- OpenAI provider to centralize temperature configuration and include missing GPT-5 model settings
- OpenAI native handler to use metadata for model capabilities
- Vertex provider to use metadata for model capabilities
## [3.40.2]
- Fix logout on network errors during token refresh (e.g., opening laptop while offline)
## [3.40.1]
- Fix cost calculation display for Anthropic API requests
## [3.40.0]
- Fix highlighted text flashing when task header is collapsed
- Add X-Cerebras-3rd-Party-Integration header to Cerebras API requests
- Add microwave family system prompt configuration
- Remove tooltips from auto approve menu
- Fix Standalone, ensure cwd is the install dir to find resources reliably
- Fix a bug where terminal commands with double quotes are broken when "Terminal Execution Mode" is set to "Background Exec"
- Add support for slash commands anywhere in a message, not just at the beginning. This matches the behavior of @ mentions for a more flexible input experience.
- Add bottom padding to the last message to fix last response text getting cut off by auto approve settings bar.
- Add default thinking level for Gemini 3 Pro models in Gemini provider
## [3.39.2]
- Fix for microwave model and thinking settings
## [3.39.1]
- Fix Openrouter and Cline Provider model info
## [3.39.0]
- Add Explain Changes feature
- Add microwave Stealth model
- Add Tabbed Model Picker with Recommended and Free tabs
- Add support to View remote rules and workflows in the editor
- Enable NTC (Native Tool Calling) by default
- Bug fixes and improvements for LiteLLM provider
## [3.38.3]
- Task export feature now opens the task directory, allowing easy access to the full task files
- Add Grok 4.1 and Grok Code to XAI provider
- Enabled native tool calling for Baseten and Kimi K2 models
- Add thinking level to Gemini 3.0 Pro preview
- Expanded Hooks functionality
- Removed Task Timeline from Task Header
- Bug fix for slash commands
- Bug fixes for Vertex provider
- Bug fixes for thinking/reasoning issues across multiple providers when using native tool calling
- Bug fixes for terminal usage on Windows devices
## [3.38.2]
- Add Claude Opus 4.5
## [3.38.1]
### Fixed
- Fixed handling of 'signature' field in sanitizeAnthropicContentBlock to properly preserve it when thinking is enabled, as required by Anthropic's API.
## [3.38.0]
### Added
- Gemini 3 Pro Preview model
- AquaVoice Avalon model for voice-to-text dictation
### Fixed
- Automatic context truncation when AWS Bedrock token usage rate limits are exceeded
- Removed new_task tool from system prompts, updated slash command prompts, and added helper function for native tool calling validation
## [3.37.1]
- Comprehensive changes to better support GPT 5.1 - System prompt, tools, deep-planning, focus chain, etc.
- Add AGENTS.md support
- feat(models): Add free minimax/mimax-m2 model to the model picker
## [3.37.0]
### Added
- GPT-5.1 with model-specific prompting: tailored system prompts, tool usage, focus chain, and deep-planning optimizations
- Nous Research provider with Hermes 4 model family and custom system prompts
- Switched to Aqua Voice's Avalon model in speech to text transcription
- Added Linux support for speech to text
- Model-family breakouts for deep-planning prompting, laying groundwork for enhanced slash commands
- Expanded HTTP proxy support throughout the codebase
- Improved focus chain prompting for frontier models (Anthropic, OpenAI, Gemini, xAI)
### Fixed
- Duplicate tool results prevention through existence checking
- XML entity escaping in model content processor
- Commit message generation in command palette
- OpenAI Compatible provider temperature parameter type conversion
## Documentation
- Added missing proto generation step in CONTRIBUTING.md
- New `npm run dev` script for streamlined terminal workflow (fixes #7335)
## [3.36.1]
- fix: remove native tool calling support from Gemini and XAI provider due to invalid tool names issues
- fix: disable native tool callings for grok code models
- Add MCP tool usage to GLM
- Removes reasoning_details content field from Anthropic providers
## [3.36.0]
- Add: Hooks allow you to inject custom logic into Cline's workflow
- Add: new provider AIhubmix
- Add: Use http_proxy, https_proxy and no_proxy in JetBrains
- Fix: Oca Token Refresh logic
- Fix: issues where assistant message with empty content is added to conversation history
- Fix: bug where the checkbox shows in the model selector dropdown
- Fix: Switch from defaultUserAgentProvider to customUserAgent for Bedrock
- Fix: support for `<think>` tags for better compatibility with open-source models
- Fix: refinements to the GLM-4.6 system prompt
## [3.35.1]
- Add: Hicap API integration as provider
- Fix: enable Add Header button in OpenAICompatibleProvider UI
- Fix: Remove orphaned tool_results after truncation and empty content field issues in native tool call
- Fix: render model description in markdown
## [3.35.0]
- Add native tool calling support with configurable setting.
- Auto-approve is now always-on with a redesigned expanding menu. Settings simplified and notifications moved to General Settings.
- added zai-glm-4.6 as a Cerebras model
- Created GPT5 family specific system prompt template
- Fix: show reasoning budget slider to models with valid thinking config
- Requesty base URL, and API key fixes
- Delete all Auth Tokens when logging out
- Support for <think> tags for models that prefer that over <thinking>
## [3.34.1]
- Added support for MiniMax provider with MiniMax-M2 model
- Remove Cline/code-supernova-1-million model
- Changes to allow users to manually enter model names (eg. presets) when using OpenRouter
## [3.34.0]
- Cline Teams is now free through 2025 for unlimited users. Includes Jetbrains, RBAC, centralized billing and more.
- Use the “exacto” versions of GLM-4.6, Kimi-K2, and Qwen3-Coder in the Cline provider for the best balance of cost, speed, accuracy and tool-calling.
## [3.33.1]
- Fix CLI installation copy text
## [3.33.0]
- Added Cline CLI (Preview)
- Added Subagent support (Experimental)
- Added Multi-Root Workspaces support (Enable in feature settings)
- Add auto-retry with exponential backof for failed API requests
## [3.32.8]
- Add Claude Haiku 4.5 support
## [3.32.7]
- Add JP and Global inference profile options to AWS Bedrock
- Adding Improvements to VSCode multi root workspaces
- Added markdown support to focus chain text, allowing the model to display more interesting focus chains
## [3.32.6]
- Add experimental support for VSCode multi root workspaces
- Add Claude Sonnet 4.5 to Claude Code provider
- Add Glm 4.6 to Z AI provider
## [3.32.5]
- Improve thinking budget slider UI to take up less space
- Fix Vercel provider cost note and sign-up url
- Fix repeated API error 400 in SAP AI Core provider
- Add us-west-1 to Amazon Bedrock regions
- Fix OCA provider refresh logic
## [3.32.4]
- Add 1m context window support to Claude Sonnet 4.5
-3
View File
@@ -1,3 +0,0 @@
@.clinerules/general.md
@.clinerules/network.md
@.clinerules/cli.md
+26 -13
View File
@@ -46,22 +46,32 @@ We also welcome contributions to our [documentation](https://github.com/cline/cl
```bash
npm run install:all
```
4. Generate Protocol Buffer files (required before first build):
```bash
npm run protos
```
5. Launch by pressing `F5` (or `Run`->`Start Debugging`) to open a new VSCode window with the extension loaded. (You may need to install the [esbuild problem matchers extension](https://marketplace.visualstudio.com/items?itemName=connor4312.esbuild-problem-matchers) if you run into issues building the project.)
4. Launch by pressing `F5` (or `Run`->`Start Debugging`) to open a new VSCode window with the extension loaded. (You may need to install the [esbuild problem matchers extension](https://marketplace.visualstudio.com/items?itemName=connor4312.esbuild-problem-matchers) if you run into issues building the project.)
### Creating a Pull Request
1. Commit your changes.
1. Before creating a PR, generate a changeset entry:
```bash
npm run changeset
```
This will prompt you for:
- Type of change (major, minor, patch)
- `major` → breaking changes (1.0.0 → 2.0.0)
- `minor` → new features (1.0.0 → 1.1.0)
- `patch` → bug fixes (1.0.0 → 1.0.1)
- Description of your changes
2. Push your branch and create a PR on GitHub. Our CI will:
2. Commit your changes and the generated `.changeset` file
3. Push your branch and create a PR on GitHub. Our CI will:
- Run tests and checks
3. Testing
- Changesetbot will create a comment showing the version impact
- When merged to main, changesetbot will create a Version Packages PR
- When the Version Packages PR is merged, a new release will be published
4. Testing
- Run `npm run test` to run tests locally.
- Before submitting PR, run `npm run format:fix` to format your code
@@ -75,10 +85,8 @@ We also welcome contributions to our [documentation](https://github.com/cline/cl
2. **Local Development**
- Run `npm run install:all` to install dependencies
- Run `npm run protos` to generate Protocol Buffer files (required before first build)
- Run `npm run test` to run tests locally
- Run → Start Debugging or `>Debug: Select and Start Debugging` and wait for a new VS Code instance to open
- **Terminal Workflow**: Use `npm run dev` (generates protos + runs watch mode) or `npm run watch` (if protos already generated)
- Before submitting PR, run `npm run format:fix` to format your code
3. **Linux-specific Setup**
@@ -178,10 +186,15 @@ Anyone can contribute code to Cline, but we ask that you follow these guidelines
- Temporary workspaces with test fixtures
- Video recording for failed tests
4. **Versioning & Changelog Notes**
4. **Version Management with Changesets**
- Contributors do not need to create changelog-entry files as part of PRs.
- Maintainers handle release versioning and changelog curation during the release process.
- Create a changeset for any user-facing changes using `npm run changeset`
- Choose the appropriate version bump:
- `major` for breaking changes (1.0.0 → 2.0.0)
- `minor` for new features (1.0.0 → 1.1.0)
- `patch` for bug fixes (1.0.0 → 1.0.1)
- Write clear, descriptive changeset messages that explain the impact
- Documentation-only changes don't require changesets
5. **Commit Guidelines**
+1 -1
View File
@@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2026 Cline Bot Inc.
Copyright 2025 Cline Bot Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
+3 -8
View File
@@ -2,7 +2,7 @@
English | <a href="https://github.com/cline/cline/blob/main/locales/es/README.md" target="_blank">Español</a> | <a href="https://github.com/cline/cline/blob/main/locales/de/README.md" target="_blank">Deutsch</a> | <a href="https://github.com/cline/cline/blob/main/locales/ja/README.md" target="_blank">日本語</a> | <a href="https://github.com/cline/cline/blob/main/locales/zh-cn/README.md" target="_blank">简体中文</a> | <a href="https://github.com/cline/cline/blob/main/locales/zh-tw/README.md" target="_blank">繁體中文</a> | <a href="https://github.com/cline/cline/blob/main/locales/ko/README.md" target="_blank">한국어</a>
</sub></div>
# Cline
# Cline \#1 on OpenRouter
<p align="center">
<img src="https://media.githubusercontent.com/media/cline/cline/main/assets/docs/demo.gif" width="100%" />
@@ -43,7 +43,7 @@ Thanks to [Claude Sonnet's agentic coding capabilities](https://www.anthropic.c
4. When a task is completed, Cline will present the result to you with a terminal command like `open -a "Google Chrome" index.html`, which you run with a click of a button.
> [!TIP]
> Follow [this guide](https://docs.cline.bot/features/customization/opening-cline-in-sidebar) to open Cline on the right side of your editor. This lets you use Cline side-by-side with your file explorer, and see how he changes your workspace more clearly.
> Use the `CMD/CTRL + Shift + P` shortcut to open the command palette and type "Cline: Open In New Tab" to open the extension as a tab in your editor. This lets you use Cline side-by-side with your file explorer, and see how he changes your workspace more clearly.
---
@@ -141,11 +141,6 @@ For example, when working with a local web server, you can use 'Restore Workspac
To contribute to the project, start with our [Contributing Guide](CONTRIBUTING.md) to learn the basics. You can also join our [Discord](https://discord.gg/cline) to chat with other contributors in the `#contributors` channel. If you're looking for full-time work, check out our open positions on our [careers page](https://cline.bot/join-us)!
## Enterprise
Get the same Cline experience with enterprise-grade controls: SSO (SAML/OIDC), global policies and configuration, observability with audit trails, private networking (VPC/private link), and self-hosted or on-prem deployments, and enterprise support. Learn more at our [enterprise page](https://cline.bot/enterprise) or [talk to us](https://cline.bot/contact-sales).
## License
[Apache 2.0 © 2026 Cline Bot Inc.](./LICENSE)
[Apache 2.0 © 2025 Cline Bot Inc.](./LICENSE)
-27
View File
@@ -1,27 +0,0 @@
# Security Policy
## Supported Versions
We actively patch only the most recent minor release of Cline. Older versions receive fixes at our discretion.
## Reporting a Vulnerability
We appreciate your efforts to responsibly disclose your findings and will make every effort to acknowledge your contributions.
To report a security issue, please use the GitHub Security Advisory ["Report a Vulnerability"](https://github.com/cline/cline/security/advisories/new) tab.
The team will send a response indicating the next steps in handling your report. After the initial reply, the security team will keep you informed of the progress towards a fix and full announcement, and may ask for additional information or guidance.
When reporting, please include:
- A short summary of the issue
- Steps to reproduce or a proof of concept
- Any logs, stack traces, or screenshots that might help us understand the problem
We acknowledge reports within 48 hours and aim to release a fix or mitigation within 30 days. While we work on a resolution, please keep the details private.
## Escalation
If you do not receive an acknowledgement of your report within 5 business days, you may send an email to security@cline.bot.
Thank you for helping us keep Cline users safe.
-30
View File
@@ -1,30 +0,0 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
<svg xmlns="http://www.w3.org/2000/svg">
<metadata>
<json>
<![CDATA[
{
"fontFamily": "cline-bot",
"majorVersion": 1,
"minorVersion": 0,
"fontURL": "https://cline.bot",
"designerURL": "https://cline.bot",
"licenseURL": "https://cline.bot",
"version": "Version 1.0",
"fontId": "cline-bot",
"psName": "cline-bot",
"subFamily": "Regular",
"fullName": "cline-bot",
"description": "Font generated by IcoMoon."
}
]]>
</json>
</metadata>
<defs>
<font id="cline-bot" horiz-adv-x="1024">
<font-face units-per-em="1024" ascent="960" descent="-64" />
<missing-glyph horiz-adv-x="1024" />
<glyph unicode="&#x20;" horiz-adv-x="512" d="" />
<glyph unicode="&#xe900;" glyph-name="cline" data-tags="cline" horiz-adv-x="977" d="M964.553 383.11l-60.285 121.406v69.495c0 115.545-92.939 209.321-207.647 209.321h-102.986c7.536 15.071 11.722 32.654 11.722 51.074 0 64.471-51.912 116.383-115.545 116.383s-115.545-51.912-115.545-116.383 4.186-35.166 11.722-51.074h-102.986c-114.708 0-207.647-93.776-207.647-209.321v-69.495l-61.959-121.406c-5.861-11.722-5.861-26.793 0-38.515l61.959-119.732v-69.495c0-115.545 92.939-209.321 207.647-209.321h415.294c114.708 0 207.647 93.776 207.647 209.321v69.495l60.285 119.732c5.861 11.722 5.861 25.956 0 38.515v0zM426.178 284.311c0-52.749-42.702-95.451-94.613-95.451s-94.613 42.702-94.613 95.451v169.132c0 52.749 42.702 95.451 94.613 95.451s94.613-42.702 94.613-95.451v-169.132zM731.787 284.311c0-52.749-42.702-95.451-94.613-95.451s-94.613 42.702-94.613 95.451v169.132c0 52.749 42.702 95.451 94.613 95.451s94.613-42.702 94.613-95.451v-169.132z" />
</font></defs></svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.
Binary file not shown.
-3
View File
@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.317 4.15557C18.7873 3.45369 17.147 2.93658 15.4319 2.6404C15.4007 2.63469 15.3695 2.64897 15.3534 2.67754C15.1424 3.05276 14.9087 3.54225 14.7451 3.927C12.9004 3.65083 11.0652 3.65083 9.25832 3.927C9.09465 3.5337 8.85248 3.05276 8.64057 2.67754C8.62449 2.64992 8.59328 2.63564 8.56205 2.6404C6.84791 2.93563 5.20756 3.45275 3.67693 4.15557C3.66368 4.16129 3.65233 4.17082 3.64479 4.18319C0.533392 8.83155 -0.31895 13.3657 0.0991801 17.8436C0.101072 17.8655 0.11337 17.8864 0.130398 17.8997C2.18321 19.4073 4.17171 20.3225 6.12328 20.9291C6.15451 20.9386 6.18761 20.9272 6.20748 20.9015C6.66913 20.2711 7.08064 19.6063 7.43348 18.9073C7.4543 18.8664 7.43442 18.8178 7.39186 18.8016C6.73913 18.554 6.1176 18.2521 5.51973 17.9093C5.47244 17.8816 5.46865 17.814 5.51216 17.7816C5.63797 17.6873 5.76382 17.5893 5.88396 17.4902C5.90569 17.4721 5.93598 17.4683 5.96153 17.4797C9.88928 19.273 14.1415 19.273 18.023 17.4797C18.0485 17.4674 18.0788 17.4712 18.1015 17.4893C18.2216 17.5883 18.3475 17.6873 18.4742 17.7816C18.5177 17.814 18.5149 17.8816 18.4676 17.9093C17.8697 18.2588 17.2482 18.554 16.5945 18.8006C16.552 18.8168 16.533 18.8664 16.5538 18.9073C16.9143 19.6054 17.3258 20.2701 17.7789 20.9005C17.7978 20.9272 17.8319 20.9386 17.8631 20.9291C19.8241 20.3225 21.8126 19.4073 23.8654 17.8997C23.8834 17.8864 23.8948 17.8664 23.8967 17.8445C24.3971 12.6676 23.0585 8.17064 20.3482 4.18414C20.3416 4.17082 20.3303 4.16129 20.317 4.15557ZM8.02002 15.117C6.8375 15.117 5.86313 14.0313 5.86313 12.6981C5.86313 11.3648 6.8186 10.2791 8.02002 10.2791C9.23087 10.2791 10.1958 11.3743 10.1769 12.6981C10.1769 14.0313 9.22141 15.117 8.02002 15.117ZM15.9947 15.117C14.8123 15.117 13.8379 14.0313 13.8379 12.6981C13.8379 11.3648 14.7933 10.2791 15.9947 10.2791C17.2056 10.2791 18.1705 11.3743 18.1516 12.6981C18.1516 14.0313 17.2056 15.117 15.9947 15.117Z" fill="#FAFAFA"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

-3
View File
@@ -1,3 +0,0 @@
<svg viewBox="0 0 24 24" fill="black" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2C6.477 2 2 6.477 2 12C2 16.418 4.865 20.166 8.84 21.49C9.34 21.58 9.52 21.27 9.52 21C9.52 20.77 9.51 20.14 9.51 19.31C6.73 19.91 6.14 17.97 6.14 17.97C5.68 16.81 5.03 16.5 5.03 16.5C4.12 15.88 5.1 15.9 5.1 15.9C6.1 15.97 6.63 16.93 6.63 16.93C7.5 18.45 8.97 18 9.54 17.76C9.63 17.11 9.89 16.67 10.17 16.42C7.95 16.17 5.62 15.31 5.62 11.5C5.62 10.39 6 9.5 6.65 8.79C6.55 8.54 6.2 7.5 6.75 6.15C6.75 6.15 7.59 5.88 9.5 7.17C10.29 6.95 11.15 6.84 12 6.84C12.85 6.84 13.71 6.95 14.5 7.17C16.41 5.88 17.25 6.15 17.25 6.15C17.8 7.5 17.45 8.54 17.35 8.79C18 9.5 18.38 10.39 18.38 11.5C18.38 15.32 16.04 16.16 13.81 16.41C14.17 16.72 14.5 17.33 14.5 18.26C14.5 19.6 14.49 20.68 14.49 21C14.49 21.27 14.67 21.59 15.17 21.49C19.14 20.16 22 16.42 22 12C22 6.477 17.523 2 12 2Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 902 B

-10
View File
@@ -1,10 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2001_1428)">
<path d="M22.2234 0H1.77187C0.792187 0 0 0.773438 0 1.72969V22.2656C0 23.2219 0.792187 24 1.77187 24H22.2234C23.2031 24 24 23.2219 24 22.2703V1.72969C24 0.773438 23.2031 0 22.2234 0ZM7.12031 20.4516H3.55781V8.99531H7.12031V20.4516ZM5.33906 7.43438C4.19531 7.43438 3.27188 6.51094 3.27188 5.37187C3.27188 4.23281 4.19531 3.30937 5.33906 3.30937C6.47813 3.30937 7.40156 4.23281 7.40156 5.37187C7.40156 6.50625 6.47813 7.43438 5.33906 7.43438ZM20.4516 20.4516H16.8937V14.8828C16.8937 13.5562 16.8703 11.8453 15.0422 11.8453C13.1906 11.8453 12.9094 13.2937 12.9094 14.7891V20.4516H9.35625V8.99531H12.7687V10.5609H12.8156C13.2891 9.66094 14.4516 8.70938 16.1813 8.70938C19.7859 8.70938 20.4516 11.0813 20.4516 14.1656V20.4516Z" fill="#FAFAFA"/>
</g>
<defs>
<clipPath id="clip0_2001_1428">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 989 B

-3
View File
@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.0512 4.07466C15.3113 5.17727 16.301 5.99866 17.4829 5.99866C18.8627 5.99866 19.9813 4.87965 19.9813 3.49933C19.9813 2.11902 18.8627 1 17.4829 1C16.2764 1 15.2703 1.85537 15.036 2.99314C13.0155 3.20991 11.4378 4.92417 11.4378 7.00167C11.4378 7.00636 11.4378 7.00988 11.4378 7.01456C9.24041 7.10713 7.23397 7.73284 5.641 8.72062C5.04949 8.26247 4.30688 7.98945 3.50102 7.98945C1.5672 7.98945 0 9.55725 0 11.4918C0 12.8955 0.824597 14.1048 2.01581 14.6637C2.13177 18.7297 6.56047 22 12.0082 22C17.4559 22 21.8905 18.7261 22.0006 14.6567C23.1824 14.0942 24 12.8885 24 11.493C24 9.55842 22.4328 7.99063 20.499 7.99063C19.6966 7.99063 18.9575 8.2613 18.3672 8.71594C16.7602 7.72113 14.7315 7.09541 12.5119 7.01222C12.5119 7.0087 12.5119 7.00636 12.5119 7.00285C12.5119 5.51473 13.6176 4.27971 15.0512 4.077V4.07466ZM5.50044 13.7146C5.559 12.4444 6.40234 11.4695 7.38272 11.4695C8.3631 11.4695 9.11274 12.4995 9.05417 13.7697C8.99561 15.0398 8.26354 15.5015 7.28199 15.5015C6.30044 15.5015 5.44187 14.9848 5.50044 13.7146ZM16.6348 11.4695C17.6164 11.4695 18.4597 12.4444 18.5171 13.7146C18.5757 14.9848 17.716 15.5015 16.7356 15.5015C15.7552 15.5015 15.022 15.041 14.9634 13.7697C14.9048 12.4995 15.6533 11.4695 16.6348 11.4695ZM15.4682 16.6533C15.6521 16.6721 15.7693 16.8631 15.6978 17.0341C15.0946 18.4766 13.6703 19.4901 12.0082 19.4901C10.3461 19.4901 8.92299 18.4766 8.31859 17.0341C8.24714 16.8631 8.36427 16.6721 8.54817 16.6533C9.62577 16.5444 10.7912 16.4846 12.0082 16.4846C13.2252 16.4846 14.3895 16.5444 15.4682 16.6533Z" fill="#FAFAFA"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

-3
View File
@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.3263 1.90393H21.6998L14.3297 10.3274L23 21.7899H16.2112L10.894 14.838L4.80995 21.7899H1.43443L9.31743 12.78L1 1.90393H7.96111L12.7674 8.25826L18.3263 1.90393ZM17.1423 19.7707H19.0116L6.94539 3.81706H4.93946L17.1423 19.7707Z" fill="#FAFAFA"/>
</svg>

Before

Width:  |  Height:  |  Size: 358 B

+51 -88
View File
@@ -1,5 +1,5 @@
{
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
"$schema": "https://biomejs.dev/schemas/2.1.4/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
@@ -28,62 +28,61 @@
"rules": {
"recommended": true,
"correctness": {
"useExhaustiveDependencies": "info",
"useExhaustiveDependencies": "off",
"noUndeclaredVariables": "off",
"noEmptyPattern": "info",
"noEmptyPattern": "off",
"useJsxKeyInIterable": "off",
"noInnerDeclarations": "off",
"useHookAtTopLevel": "info",
"useYield": "info",
"useHookAtTopLevel": "off",
"useYield": "off",
"noConstructorReturn": "off",
"noInvalidPositionAtImportRule": "off",
"noSwitchDeclarations": "off",
"noUnusedImports": "error"
},
"a11y": "info",
"a11y": "off",
"style": {
"useNodejsImportProtocol": "off",
"useImportType": "off",
"useBlockStatements": "off",
"useBlockStatements": "warn",
"useNamingConvention": "off",
"useThrowOnlyError": "info",
"useConsistentArrayType": "off",
"noParameterAssign": "off",
"useAsConstAssertion": "off",
"useDefaultParameterLast": "off",
"noNonNullAssertion": "info",
"noNonNullAssertion": "off",
"useEnumInitializers": "off",
"useSelfClosingElements": "info",
"useSelfClosingElements": "off",
"useSingleVarDeclarator": "off",
"useNumberNamespace": "info",
"noInferrableTypes": "info",
"useTemplate": "info",
"noUselessElse": "info"
"useNumberNamespace": "off",
"noInferrableTypes": "off",
"useTemplate": "off",
"noUselessElse": "off"
},
"suspicious": {
"noDoubleEquals": "warn",
"noImplicitAnyLet": "info",
"noThenProperty": "off",
"noAsyncPromiseExecutor": "info",
"noAsyncPromiseExecutor": "off",
"noImportAssign": "off",
"noExplicitAny": "info",
"noControlCharactersInRegex": "warn",
"noExplicitAny": "off",
"noControlCharactersInRegex": "off",
"noShadowRestrictedNames": "off",
"noArrayIndexKey": "info",
"noAssignInExpressions": "info",
"useIterableCallbackReturn": "info"
"noAssignInExpressions": "warn"
},
"complexity": {
"noUselessConstructor": "info",
"useOptionalChain": "info",
"noBannedTypes": "warn",
"useLiteralKeys": "info",
"noUselessCatch": "info",
"noUselessSwitchCase": "info",
"noStaticOnlyClass": "info"
"noUselessConstructor": "off",
"useOptionalChain": "off",
"noBannedTypes": "off",
"useLiteralKeys": "off",
"noUselessCatch": "off",
"noUselessSwitchCase": "off",
"noStaticOnlyClass": "off"
},
"security": {
"noDangerouslySetInnerHtml": "info"
"noDangerouslySetInnerHtml": "warn"
}
}
},
@@ -95,11 +94,6 @@
"lineEnding": "lf",
"formatWithErrors": true
},
"css": {
"parser": {
"tailwindDirectives": true
}
},
"javascript": {
"formatter": {
"semicolons": "asNeeded",
@@ -118,21 +112,19 @@
}
},
"files": {
"ignoreUnknown": true,
"includes": [
"**",
// explicitly force files to be ignored by the scanner with !!
"!!**/dist",
"!!**/dist-*",
"!!**/out",
"!!**/evals",
"!!**/playwright",
"!!**/test-results",
"!!**/node_modules",
"!!**/webview-ui/build",
"!!**/generated",
"!!**/proto",
"!!**/tests/specs"
"!**/dist/**",
"!**/dist-*/**",
"!**/out/**",
"!**/evals/**",
"!**/playwright/**",
"!**/test-results/**",
"!**/node_modules/**",
"!**/webview-ui/build/**",
"!**/generated/**",
"!**/proto/**",
"!**/tests/specs/**"
]
},
"plugins": [
@@ -142,58 +134,29 @@
{
"includes": [
"**",
"!!**/dist",
"!!**/hosts/vscode/**",
"!!**/test/**",
"!!**/*.test.ts",
"!!src/dev/**",
"!!src/extension.ts",
"!!src/integrations/git/commit-message-generator.ts",
"!!src/integrations/terminal/**",
"!!src/core/controller/ui/openWalkthrough.ts"
"!**/hosts/vscode/**",
"!**/test/**",
"!**/*.test.ts",
"!src/dev/**",
"!src/extension.ts",
"!src/integrations/git/commit-message-generator.ts",
"!src/integrations/terminal/**",
"!src/core/controller/ui/openWalkthrough.ts"
],
"plugins": [
"src/dev/grit/vscode-api.grit"
]
},
{
// Do not use console logging directly, use the Logger service instead.
"plugins": [
"src/dev/grit/console-log.grit"
],
"includes": [
"**",
"!!**/esbuild.*",
"!!**/*.mts",
"!!**/webview-ui/**",
"!!**/evals/**",
"!!**/standalone/**",
"!!**/cli/**",
"!!**/e2e/**",
"!!**/test/**",
"!!**/__tests__/**",
"!!**/*.test.ts",
"!!**/*.stories.ts",
"!!src/dev/**",
"!!**/*.mjs",
"!!**/*.js",
"!!**/scripts/**",
"!!**/*.tsx",
"!!**/testing-platform/**",
// ACP mode must redirect console to stderr - this is intentional
"!!cli/src/acp/index.ts"
]
},
{
"includes": [
"**",
"!!src/core/storage/state-migrations.ts",
"!!src/core/storage/FileContextTracker.ts",
"!!src/core/context/context-tracking/FileContextTracker.ts",
"!!src/common.ts",
"!!src/services/logging/distinctId.ts",
"!!src/core/storage/utils/state-helpers.ts",
"!!src/extension.ts"
"!src/core/storage/state-migrations.ts",
"!src/core/storage/FileContextTracker.ts",
"!src/core/context/context-tracking/FileContextTracker.ts",
"!src/common.ts",
"!src/services/logging/distinctId.ts",
"!src/core/storage/utils/state-helpers.ts",
"!src/extension.ts"
],
"plugins": [
"src/dev/grit/use-cache-service.grit"
-242
View File
@@ -1,242 +0,0 @@
# cline
## [2.9.0]
### Added
- Latency improvements for remote workspaces
## [2.8.2]
### Fixed
- Use `kanban@latest` in `cline kanban` to always fetch the newest version
## [2.8.1]
### Added
- Implement dynamic free model detection for Cline API
- Add file read deduplication cache to prevent repeated reads
- Add feature tips tooltip during thinking state
### Fixed
- Fix flaky CLI Enter-key handling across Windows/test environments
- Replace error message when not logged in to Cline
- Align ClineRulesToggleModal padding with ServersToggleModal
- Skip WebP for GLM and Devstral models running through llama.cpp
- Respect user-configured context window in LiteLLM getModel()
- Honor explicit model IDs outside static catalog in W&B provider
- Add missing Fireworks serverless models and pricing
## [2.8.0]
### Added
- Added W&B Inference by CoreWeave as a new API provider with 17 models including DeepSeek-V3.1, Llama 4, and Qwen3-Coder
- Added CLI TUI end-to-end test suite
### Fixed
- Claude Code: handle rate limit events, empty content arrays, error results, and unknown content types without crashing
- CLI: `/q` and `/exit` slash commands now execute immediately on Enter without requiring the slash menu to be visible
- CLI: slash command filtering now prioritizes exact and prefix matches over fuzzy matches
## [2.7.0]
### Added
- Added MCP add shortcuts for stdio and HTTP servers
- Added `--continue` for the current directory
- Added `--auto-condense` flag for AI-powered context compaction
- Added `--hooks-dir` flag for runtime hook injection
- Enabled error autocapture
- Prompt rules now include test verification guidance and make `CLI_RULES` language-agnostic
### Fixed
- Fixed remount behavior so TUI remounts only on width resize
- Fixed startup prompt replay on resize remount
- Fixed task flags so they are applied before the welcome TUI mounts
### Changed
- Hooks: reintroduced feature toggle
## [2.6.1]
### Added
- Added GPT-5.4 models for ChatGPT subscription users
- Hooks: Added a `Notification` hook for attention and completion boundaries
- Added `--hooks-dir` CLI flag for runtime hook injection
- Added `--auto-approve-all` CLI flag for interactive mode
### Fixed
- Handle streamable HTTP MCP reconnects more reliably
## [2.6.0]
### Added
- Hook payloads now include `model.provider` and `model.slug`
- Token/cost updates now happen immediately as usage chunks arrive, not after tool execution
### Fixed
- Improve subagent context compaction logic
- Subagent stream retry delay increased to reduce noise from transient failures
- State serialization errors are now caught and logged instead of crashing
- Removed incorrect `max_tokens` from OpenRouter requests
## [2.5.2]
### Added
- Added Windows PowerShell support for hooks (execution, resolution, and management), improving hook behavior on Windows for CLI and shared core workflows.
### Fixed
- Restored GPT-OSS native file editing for OpenAI-compatible models used through shared core tooling.
- Improved OpenRouter context overflow error handling so auto-compaction triggers correctly for wrapped 400 errors.
- Hardened checkpoint recovery by retrying nested git restore and preventing silent `.git_disabled` leftovers.
- Added a User-Agent header for requests to the Cline back-end to improve request handling consistency.
## [2.5.1]
### Added
- Expanded CLI markdown rendering support (headings, lists, blockquotes, fenced code blocks, links, and nested lists).
### Fixed
- Fixed CLI headless auth provider model metadata loading for Cline and Vercel AI Gateway by fetching model info from API with cache fallback.
- Increased flaky CLI import test timeout on Windows CI to reduce intermittent test failures.
## [2.5.0]
### Added
- Added Cline SDK API interface for programmatic access to Cline features and tools, enabling integration into custom applications.
- Added Codex 5.3 model support
### Fixed
- Fix OpenAI Codex by setting `store` to `false`
- Use `isLocatedInPath()` instead of string matching for path containment checks
## [2.4.3]
### Added
- Add /q command to quit CLI
- Fetch featured models from backend with local fallback
### Fixed
- Fix auth check for ACP mode
- Fix Cline auth with ACP flag
- Fix yolo mode to not persist yolo setting to disk
## [2.4.2]
### Added
- Gemini-3.1 Pro Preview
### Patch Changes
- VSCode uses shared files for global, workspace and secret state.
## [2.4.1]
### Fixed
- Fix infinite retry loop when write_to_file fails with missing content parameter. Provides progressive guidance to the model, escalating from suggestions to hard stops, with context window awareness to break the loop.
## [2.4.0]
### Added
- Adding Anthropic Sonnet 4.6
- Allows users to enter custom aws region when selecting bedrock as a provider in CLI
- Keep reasoning rows visible when low-stakes tool groups start immediately after reasoning.
- Restore reasoning trace visibility in chat and improve the thinking row UX so streamed reasoning is visible, then collapsible after completion.
### Fixed
- Banners now display immediately when opening the extension instead of requiring user interaction first
- Resolved 17 security vulnerabilities including high-severity DoS issues in dependencies (body-parser, axios, qs, tar, and others)
## [2.2.2]
- Allows users to enter custom aws region when selecting bedrock as a provider
- Prevent Parent Container Scrolling In Dropdowns
## [2.2.1]
- Added Minimax 2.5 Free Promo
- Fixed Response chaining for OpenAI's Responses API
## [2.2.0]
### Added
- Subagent: replace legacy subagents with the native `use_subagents` tool
- Bundle `endpoints.json` support so packaged distributions can ship required endpoints out-of-the-box
- Amazon Bedrock: support parallel tool calling
- New "double-check completion" experimental feature to verify work before marking tasks complete
- CLI: new task controls/flags including custom `--thinking` token budget and `--max-consecutive-mistakes` for yolo runs
- Remote config: new UI/options (including connection/test buttons) and support for syncing deletion of remotely configured MCP servers
- Vertex / Claude Code: add 1M context model options for Claude Opus 4.6
- ZAI/GLM: add GLM-5
### Fixed
- CLI: handle stdin redirection correctly in CI/headless environments
- CLI: preserve OAuth callback paths during auth redirects
- VS Code Web: generate auth callback URLs via `vscode.env.asExternalUri` (OAuth callback reliability)
- Terminal: surface command exit codes in results and improve long-running `execute_command` timeout behavior
- UI: add loading indicator and fix `api_req_started` rendering
- Task streaming: prevent duplicate streamed text rows after completion
- API: preserve selected Vercel model when model metadata is missing
- Telemetry: route PostHog networking through proxy-aware shared fetch and ensure telemetry flushes on shutdown
- CI: increase Windows E2E test timeout to reduce flakiness
### Changed
- Settings/model UX: move "reasoning effort" into model configuration and expose it in settings
- CLI provider selection: limit provider list to those remotely configured
- UI: consolidate ViewHeader component/styling across views
- Tools: add auto-approval support for `attempt_completion` commands
- Remotely configured MCP server schema now supports custom headers
## [2.1.0]
### Minor Changes
- 42ce100: Add Generate API Key on Hicap Provider selection
### Patch Changes
- 195294f: Add support for bundled endpoints.json in enterprise distributions. Extensions can now include a pre-configured endpoints.json file that automatically switches Cline to self-hosted mode. Includes packaging scripts for VSIX, NPM, and JetBrains plugins.
- a1f2601: Replace the LiteLLM model list with a selector
- 739d75a: Add Claude Code provider support for Claude Opus 4.6 and Sonnet 4.5 1M variants via both full model names and aliases (`opus[1m]`, `sonnet[1m]`), and align the `opus` alias with Opus 4.6.
- 8440380: Add GitHub Actions workflow to build CLI from any commit for testing
- b1a8db2: fix(cli): prevent hang when spawned without TTY
- 7c87017: Add Claude Opus 4.6 model support
- d116ac5: Supports rendering markdown table in chat view.
- 6d8fb85: Fix CLI crashing in CI environments and with stdin redirection (e.g., `cline "prompt" < /dev/null`). Now checks both stdin and stdout TTY status before using Ink, and only errors on empty stdin when no prompt is provided.
- 70a9904: Fix JetBrains sign-in regression by adding fallback for openExternal RPC
- f440f3a: fix: use vscode.env.openExternal for auth in remote environments
Fixes OAuth authentication in VS Code Server and remote environments by routing browser URL opening through VS Code's native openExternal API instead of the npm 'open' package.
- 70a9904: fix: use vscode.env.asExternalUri for auth callback URLs only in VS Code Web
Fixes OAuth callback redirect in VS Code Web (`code serve-web`, Codespaces) by using `vscode.env.asExternalUri()` to resolve the callback URI. This is gated behind a `vscode.env.uiKind === UIKind.Web` check so regular desktop VS Code continues to use the `vscode://` URI directly. The `getCallbackUrl` API now accepts a `path` parameter so the full callback URI (including route) is resolved correctly, and callers pass their path directly instead of appending after.
- 5308ded: Updating script documentation and removing unnecessary continue on error
- b514f18: Prevent duplicate streamed text rows when a partial text update arrives after the same text was already finalized.
- 26391c9: Fix Bedrock model id
- d19a877: Unify ViewHeader Styles Across All Views
- 5dcaa8c: Add Vertex Claude Opus 4.6 1M model option and global endpoint support, and pass the 1M beta header for Vertex Claude requests.
-365
View File
@@ -1,365 +0,0 @@
# Cline CLI
The official CLI for Cline. Run Cline tasks directly from the terminal with the same underlying functionality as the VS Code extension.
## Features
- **Reuses Core Codebase**: Shares the same Controller, Task, and API handling as the VS Code extension
- **Terminal Output**: Displays Cline messages directly in your terminal with colored output
- **Task History**: Access your task history from the command line
- **Configurable**: Use custom configuration directories and working directories
- **Image Support**: Attach images to your prompts using file paths or inline references
## Prerequisites
- Node.js 20.x or later
- npm or yarn
- The parent Cline project dependencies installed
## Installation
From the repository root:
```bash
# Install all dependencies first
npm run install:all
# Ensure protos are generated
npm run protos
# Build and link the CLI globally
npm run cli:link
```
## Usage
### Interactive Mode (Default)
When you run `cline` without any command, it launches an interactive welcome prompt:
```bash
# Launch interactive mode
cline
# Or run a task directly
cline "Create a hello world function in Python"
# With options
cline -v --thinking "Analyze this codebase"
```
### Commands
#### `task` (alias: `t`)
Run a new task with a prompt.
```bash
cline task "Create a hello world function in Python"
cline t "Create a hello world function"
```
**Options:**
| Option | Description |
|--------|-------------|
| `-a, --act` | Run in act mode |
| `-p, --plan` | Run in plan mode |
| `-y, --yolo` | Enable yolo mode (auto-approve actions) |
| `-m, --model <model>` | Model to use for the task |
| `-i, --images <paths...>` | Image file paths to include with the task |
| `-v, --verbose` | Show verbose output including reasoning |
| `-c, --cwd <path>` | Working directory for the task |
| `--config <path>` | Path to Cline configuration directory |
| `-t, --thinking` | Enable extended thinking (1024 token budget) |
**Examples:**
```bash
# Run in plan mode with verbose output
cline task -p -v "Design a REST API"
# Use a specific model with yolo mode
cline task -m claude-sonnet-4-5-20250929 -y "Refactor this function"
# Include images with your prompt
cline task -i screenshot.png diagram.jpg "Fix the UI based on these images"
# Or use inline image references in the prompt
cline task "Fix the layout shown in @./screenshot.png"
# Enable extended thinking for complex tasks
cline task -t "Architect a microservices system"
# Specify working directory
cline task -c /path/to/project "Add unit tests"
```
#### `history` (alias: `h`)
List task history with pagination support.
```bash
cline history
cline h
```
**Options:**
| Option | Description |
|--------|-------------|
| `-n, --limit <number>` | Number of tasks to show (default: 10) |
| `-p, --page <number>` | Page number, 1-based (default: 1) |
| `--config <path>` | Path to Cline configuration directory |
**Examples:**
```bash
# Show last 10 tasks (default)
cline history
# Show 20 tasks
cline history -n 20
# Show page 2 with 5 tasks per page
cline history -n 5 -p 2
```
#### `config`
Show current configuration including global and workspace state.
```bash
cline config
```
**Options:**
| Option | Description |
|--------|-------------|
| `--config <path>` | Path to Cline configuration directory |
#### `auth`
Authenticate a provider and configure what model is used.
```bash
cline auth
```
**Options:**
| Option | Description |
|--------|-------------|
| `-p, --provider <id>` | Provider ID for quick setup (e.g., openai-native, anthropic) |
| `-k, --apikey <key>` | API key for the provider |
| `-m, --modelid <id>` | Model ID to configure (e.g., gpt-4o, claude-sonnet-4-5-20250929) |
| `-b, --baseurl <url>` | Base URL (optional, only for openai provider) |
| `-v, --verbose` | Show verbose output |
| `-c, --cwd <path>` | Working directory for the task |
| `--config <path>` | Path to Cline configuration directory |
**Examples:**
```bash
# Interactive authentication
cline auth
# Quick setup with provider and API key
cline auth -p anthropic -k sk-ant-xxxxx
# Full quick setup with model
cline auth -p openai-native -k sk-xxxxx -m gpt-4o
# OpenAI-compatible provider with custom base URL
cline auth -p openai -k your-api-key -b https://api.example.com/v1
```
### Global Options
These options are available for the default command (running a task directly):
| Option | Description |
|--------|-------------|
| `-v, --verbose` | Show verbose output |
| `-c, --cwd <path>` | Working directory |
| `--config <path>` | Configuration directory |
| `--thinking` | Enable extended thinking (1024 token budget) |
## Development
### Quick Start
```bash
# 1. Install all dependencies (root, webview-ui, cli)
npm run install:all
# 2. Build and link globally so you can run `cline` from anywhere
npm run cli:link
# 3. Test it
cline --help
```
### Scripts
Run these from the repository root:
| Script | Description |
|--------|-------------|
| `npm run install:all` | Install deps for root, webview-ui, and cli |
| `npm run cli:build` | Generate protos and build CLI |
| `npm run cli:build:production` | Production build (minified) |
| `npm run cli:link` | Build and `npm link` so you can run `cline` from anywhere |
| `npm run cli:unlink` | Remove the global `cline` symlink |
| `npm run cli:dev` | Link + watch mode for development |
| `npm run cli:watch` | Watch mode only (no initial build) |
| `npm run cli:test` | Run CLI tests |
### Development Workflow
1. Run `npm run cli:dev` - this links the CLI globally and starts watch mode
2. Make changes to files in `cli/src/`
3. The build automatically rebuilds on save
4. Test your changes by running `cline` in another terminal
5. When done, run `npm run cli:unlink` to clean up
### Proto Generation
The CLI uses proto-generated types for message passing (same as the VS Code extension). If you modify any `.proto` files, run:
```bash
npm run protos
```
This generates TypeScript types in `src/generated/` that both the CLI and extension use.
## Publish
#### 1. Publish to npm
```bash
npm publish
```
#### 2. Update the Homebrew formula
```bash
npm run update-brew-formula
```
#### 3. Test the formula locally
```bash
# Create a local tap
brew tap-new cline/local
cp ./cli/cline.rb "$(brew --repository)/Library/Taps/cline/homebrew-local/Formula/cline.rb"
# Build from Source
brew install --build-from-source cline/local/cline
# Install from your local tap
brew install cline/local/cline
# Clean up when done
brew untap cline/local
```
#### 4. If using a tap, commit and push
```bash
git add cline.rb
git commit -m "Update cline to v2.0.0"
git push
```
## Architecture
### How It Works
The CLI directly imports and reuses the core Cline TypeScript codebase (the same code that powers the VS Code extension). This means feature parity is easy to maintain - when core gets updated, the CLI automatically benefits.
```
┌─────────────────────────────────────────────────────────┐
│ CLI (cli/) │
│ - React Ink terminal UI │
│ - Command parsing (commander) │
│ - Terminal-specific adapters │
└─────────────────────────────────────────────────────────┘
│ direct imports
┌─────────────────────────────────────────────────────────┐
│ Core (src/core/) │
│ - Controller: task lifecycle, state management │
│ - Task: AI API calls, tool execution │
│ - StateManager: persistent storage │
│ - Proto types: message definitions │
└─────────────────────────────────────────────────────────┘
```
Unlike a client-server architecture, the CLI runs everything in a single Node.js process. The "host bridge" pattern provides terminal-appropriate implementations for things the VS Code extension would handle differently (clipboard, file dialogs, etc.).
### Key Files
| File | Purpose |
|------|---------|
| `src/index.ts` | Entry point, command definitions |
| `src/components/App.tsx` | Main React Ink app |
| `src/components/ChatView.tsx` | Task conversation UI |
| `src/controllers/CliWebviewProvider.ts` | Bridges core messages to terminal output |
| `src/vscode-context.ts` | Mock VS Code extension context for core compatibility |
| `src/vscode-shim.ts` | Shims for VS Code APIs that core depends on |
| `src/constants/colors.ts` | Terminal color definitions |
### React Ink
The CLI uses [React Ink](https://github.com/vadimdemedes/ink) for its terminal UI. This lets us build the interface with React components that render to the terminal. Key patterns:
- Components in `src/components/` render terminal UI
- Hooks in `src/hooks/` manage terminal-specific state (size, scrolling)
- The `useStateSubscriber` hook subscribes to core state changes
## Configuration
The CLI stores its data in `~/.cline/data/` by default:
- `globalState.json`: Global settings and state
- `secrets.json`: API keys and secrets
- `workspace/`: Workspace-specific state
- `tasks/`: Task history and conversation data
Override with the `--config` option or `CLINE_DIR` environment variable.
## Troubleshooting
### Build Errors
If you encounter build errors:
```bash
# Make sure all deps are installed
npm run install:all
# Regenerate proto types
npm run protos
# Then rebuild
npm run cli:build
```
### "command not found: cline"
The CLI isn't linked globally. Run:
```bash
npm run cli:link
```
### Changes Not Reflected
If your code changes aren't showing up:
1. Make sure watch mode is running (`npm run cli:dev`)
2. Check for TypeScript errors in the watch output
3. Try unlinking and relinking: `npm run cli:unlink && npm run cli:link`
### Import Errors from Core
The CLI imports from `@core/`, `@shared/`, etc. These paths are defined in the root `tsconfig.json`. If you see import errors, make sure you're building from the repo root, not from inside `cli/`.
-81
View File
@@ -1,81 +0,0 @@
# Cline
<p align="center">
<img src="https://github.com/user-attachments/assets/7123f9d1-afeb-48d5-93fa-e750dec0ebba" width="70%" />
</p>
<div align="center">
<table>
<tbody>
<td align="center">
<a href="https://www.npmjs.com/package/cline" target="_blank"><strong>NPM</strong></a>
</td>
<td align="center">
<a href="https://discord.gg/cline" target="_blank"><strong>Discord</strong></a>
</td>
<td align="center">
<a href="https://www.reddit.com/r/cline/" target="_blank"><strong>r/cline</strong></a>
</td>
<td align="center">
<a href="https://github.com/cline/cline/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop" target="_blank"><strong>Feature Requests</strong></a>
</td>
<td align="center">
<a href="https://docs.cline.bot/getting-started/for-new-coders" target="_blank"><strong>Getting Started</strong></a>
</td>
</tbody>
</table>
</div>
Meet Cline, an AI assistant that lives in your terminal.
Cline can handle complex software development tasks step-by-step. With tools that let him create & edit files, explore large projects, use the browser, and execute terminal commands (after you grant permission), he can assist you in ways that go beyond code completion or tech support.
```bash
npm i -g cline
# cd into your project and run:
cline
```
> Move your mouse around under the Cline icon for a surprise!
---
<img align="right" width="340" src="https://github.com/user-attachments/assets/ceb74224-08aa-4b8b-a3e7-b438ac3d160a">
### Use any API and Model
Cline supports API providers like ChatGPT, Anthropic, OpenAI, Google Gemini, AWS Bedrock, Azure, GCP Vertex, Cerebras, Groq, and Moonshot. You can also configure any OpenAI compatible API, or use a local model through LM Studio/Ollama. If you're using a Cline Account, you'll always have access to the newest models as soon as they're available.
<!-- Transparent pixel to create line break after floating image -->
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
<img align="left" width="370" src="https://github.com/user-attachments/assets/cad091f6-6c0f-4e4b-97ea-a1ff67e39b9b">
### Stay in Control with Human-in-the-Loop
Cline asks for your approval before running commands, editing files, or taking any action. Review each step and approve or reject as you go—or enable auto-approve to let Cline work autonomously to completion.
<!-- Transparent pixel to create line break after floating image -->
<img width="2000" height="0" src="https://github.com/user-attachments/assets/cad091f6-6c0f-4e4b-97ea-a1ff67e39b9b"><br>
<img align="right" width="400" src="https://github.com/user-attachments/assets/4f264a0c-3802-49a7-8e5e-13d97beb659e">
### Plan & Act Modes
Toggle to Plan Mode to discuss implementation and architecture with Cline. He'll ask clarifying questions, explore your codebase, and present a plan for you to align on. Once you're satisfied, switch to Act Mode and let Cline execute the plan.
<!-- Transparent pixel to create line break after floating image -->
<img width="2000" height="0" src="https://github.com/user-attachments/assets/4f264a0c-3802-49a7-8e5e-13d97beb659e"><br>
## Enterprise
Get the same Cline experience with enterprise-grade controls: SSO (SAML/OIDC), global policies and configuration, observability with audit trails, private networking (VPC/private link), and self-hosted or on-prem deployments, and enterprise support. Learn more at our [enterprise page](https://cline.bot/enterprise) or [talk to us](https://cline.bot/contact-sales).
## License
[Apache 2.0 © 2026 Cline Bot Inc.](./LICENSE)
-21
View File
@@ -1,21 +0,0 @@
# IMPORTANT: `npm run postpublish` to update this file after publishing a new version of the package
class Cline < Formula
desc "Autonomous coding agent CLI - capable of creating/editing files, running commands, and more"
homepage "https://cline.bot"
url "https://registry.npmjs.org/cline/-/cline-2.0.0.tgz" # GET from https://registry.npmjs.org/cline/latest tarball URL
sha256 "65bae90401191aeeabfbbc0b315e816aea96742043ba85b90671bf5e19d0761e"
license "Apache-2.0"
depends_on "node@20"
depends_on "ripgrep"
def install
system "npm", "install", *std_npm_args(prefix: false)
bin.install_symlink Dir["#{libexec}/bin/*"]
end
test do
# Test that the binary exists and is executable
assert_match version.to_s, shell_output("#{bin}/cline --version")
end
end
-305
View File
@@ -1,305 +0,0 @@
import fs from "node:fs"
import path from "node:path"
import { fileURLToPath } from "node:url"
import dotenv from "dotenv"
import * as esbuild from "esbuild"
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const rootDir = path.resolve(__dirname, "..")
// Load .env from repo root
dotenv.config({ path: path.join(rootDir, ".env") })
const production = process.argv.includes("--production")
const watch = process.argv.includes("--watch")
/**
* Plugin to resolve path aliases from the parent project
*/
const aliasResolverPlugin: esbuild.Plugin = {
name: "alias-resolver",
setup(build) {
const aliases = {
"@": path.resolve(rootDir, "src"),
"@core": path.resolve(rootDir, "src/core"),
"@integrations": path.resolve(rootDir, "src/integrations"),
"@services": path.resolve(rootDir, "src/services"),
"@shared": path.resolve(rootDir, "src/shared"),
"@utils": path.resolve(rootDir, "src/utils"),
"@packages": path.resolve(rootDir, "src/packages"),
"@hosts": path.resolve(rootDir, "src/hosts"),
"@generated": path.resolve(rootDir, "src/generated"),
"@api": path.resolve(rootDir, "src/core/api"),
}
// For each alias entry, create a resolver
Object.entries(aliases).forEach(([alias, aliasPath]) => {
const aliasRegex = new RegExp(`^${alias}($|/.*)`)
build.onResolve({ filter: aliasRegex }, (args) => {
const importPath = args.path.replace(alias, aliasPath)
// First, check if the path exists as is
if (fs.existsSync(importPath)) {
const stats = fs.statSync(importPath)
if (stats.isDirectory()) {
// If it's a directory, try to find index files
const extensions = [".ts", ".tsx", ".js", ".jsx"]
for (const ext of extensions) {
const indexFile = path.join(importPath, `index${ext}`)
if (fs.existsSync(indexFile)) {
return { path: indexFile }
}
}
} else {
// It's a file that exists, so return it
return { path: importPath }
}
}
// If the path doesn't exist, try appending extensions
const extensions = [".ts", ".tsx", ".js", ".jsx"]
for (const ext of extensions) {
const pathWithExtension = `${importPath}${ext}`
if (fs.existsSync(pathWithExtension)) {
return { path: pathWithExtension }
}
}
// Handle .js -> .ts extension mapping (common in ESM TypeScript projects)
if (importPath.endsWith(".js")) {
const tsPath = importPath.replace(/\.js$/, ".ts")
if (fs.existsSync(tsPath)) {
return { path: tsPath }
}
const tsxPath = importPath.replace(/\.js$/, ".tsx")
if (fs.existsSync(tsxPath)) {
return { path: tsxPath }
}
}
// If nothing worked, return the original path and let esbuild handle the error
return { path: importPath }
})
})
},
}
/**
* Plugin to redirect vscode imports to our shim
*/
const vscodeStubPlugin: esbuild.Plugin = {
name: "vscode-stub",
setup(build) {
// Redirect 'vscode' imports to our shim
build.onResolve({ filter: /^vscode$/ }, () => {
return { path: path.join(__dirname, "src", "vscode-shim.ts") }
})
},
}
const esbuildProblemMatcherPlugin: esbuild.Plugin = {
name: "esbuild-problem-matcher",
setup(build) {
build.onStart(() => {
console.log("[cli esbuild] Build started...")
})
build.onEnd((result) => {
result.errors.forEach(({ text, location }) => {
console.error(`✘ [ERROR] ${text}`)
if (location) {
console.error(` ${location.file}:${location.line}:${location.column}:`)
}
})
console.log("[cli esbuild] Build finished")
})
},
}
// Plugin to stub out optional devtools module
const stubOptionalModulesPlugin: esbuild.Plugin = {
name: "stub-optional-modules",
setup(build) {
build.onResolve({ filter: /^react-devtools-core$/ }, () => {
return { path: path.join(__dirname, "src", "stub-devtools.js"), external: false }
})
},
}
const copyWasmFiles: esbuild.Plugin = {
name: "copy-wasm-files",
setup(build) {
build.onEnd(() => {
const destDir = path.join(__dirname, "dist")
// Ensure dist directory exists
if (!fs.existsSync(destDir)) {
fs.mkdirSync(destDir, { recursive: true })
}
// tree sitter
const sourceDir = path.join(rootDir, "node_modules", "web-tree-sitter")
// Copy tree-sitter.wasm
const treeSitterWasm = path.join(sourceDir, "tree-sitter.wasm")
if (fs.existsSync(treeSitterWasm)) {
fs.copyFileSync(treeSitterWasm, path.join(destDir, "tree-sitter.wasm"))
}
// Copy language-specific WASM files
const languageWasmDir = path.join(rootDir, "node_modules", "tree-sitter-wasms", "out")
const languages = [
"typescript",
"tsx",
"python",
"rust",
"javascript",
"go",
"cpp",
"c",
"c_sharp",
"ruby",
"java",
"php",
"swift",
"kotlin",
]
if (fs.existsSync(languageWasmDir)) {
languages.forEach((lang) => {
const filename = `tree-sitter-${lang}.wasm`
const sourcePath = path.join(languageWasmDir, filename)
if (fs.existsSync(sourcePath)) {
fs.copyFileSync(sourcePath, path.join(destDir, filename))
}
})
}
})
},
}
const buildEnvVars: Record<string, string> = {
"process.env.IS_STANDALONE": JSON.stringify("true"),
"process.env.IS_CLI": JSON.stringify("true"),
}
const buildTimeEnvs = [
"TELEMETRY_SERVICE_API_KEY",
"ERROR_SERVICE_API_KEY",
"ENABLE_ERROR_AUTOCAPTURE",
"POSTHOG_TELEMETRY_ENABLED",
"OTEL_TELEMETRY_ENABLED",
"OTEL_LOGS_EXPORTER",
"OTEL_METRICS_EXPORTER",
"OTEL_EXPORTER_OTLP_PROTOCOL",
"OTEL_EXPORTER_OTLP_ENDPOINT",
"OTEL_EXPORTER_OTLP_HEADERS",
"OTEL_METRIC_EXPORT_INTERVAL",
"CLINE_ENVIRONMENT",
]
buildTimeEnvs.forEach((envVar) => {
if (process.env[envVar]) {
console.log(`[cli esbuild] ${envVar} env var is set`)
buildEnvVars[`process.env.${envVar}`] = JSON.stringify(process.env[envVar])
}
})
if (production) {
buildEnvVars["process.env.IS_DEV"] = "false"
}
// Shared build options
const sharedOptions: Partial<esbuild.BuildOptions> = {
bundle: true,
minify: production,
sourcemap: !production,
logLevel: "silent",
define: buildEnvVars,
tsconfig: path.join(__dirname, "tsconfig.json"),
plugins: [copyWasmFiles, aliasResolverPlugin, vscodeStubPlugin, stubOptionalModulesPlugin, esbuildProblemMatcherPlugin],
format: "esm",
sourcesContent: false,
platform: "node",
target: "node20",
// These modules need to load files from the module directory at runtime
external: [
"@grpc/reflection",
"grpc-health-check",
"better-sqlite3",
"ink",
"ink-spinner",
"ink-picture",
"react",
"aws4fetch",
"pino",
"pino-roll",
"@vscode/ripgrep", // Uses __dirname to locate the binary
],
supported: { "top-level-await": true },
}
// CLI executable configuration
const cliConfig: esbuild.BuildOptions = {
...sharedOptions,
entryPoints: [path.join(__dirname, "src", "index.ts")],
outfile: path.join(__dirname, "dist", "cli.mjs"),
banner: {
js: `#!/usr/bin/env node
// Suppress all Node.js warnings (deprecation, experimental, etc.)
process.emitWarning = () => {};
import { createRequire as _createRequire } from 'module';
import { fileURLToPath as _fileURLToPath } from 'url';
import { dirname as _dirname } from 'path';
const require = _createRequire(import.meta.url);
const __filename = _fileURLToPath(import.meta.url);
const __dirname = _dirname(__filename);`,
},
}
// Library configuration for programmatic use
const libConfig: esbuild.BuildOptions = {
...sharedOptions,
entryPoints: [path.join(__dirname, "src", "exports.ts")],
outfile: path.join(__dirname, "dist", "lib.mjs"),
banner: {
js: `// Cline Library - Programmatic API
import { createRequire as _createRequire } from 'module';
import { fileURLToPath as _fileURLToPath } from 'url';
import { dirname as _dirname } from 'path';
const require = _createRequire(import.meta.url);
const __filename = _fileURLToPath(import.meta.url);
const __dirname = _dirname(__filename);`,
},
}
async function main() {
if (watch) {
// In watch mode, only watch the CLI (primary use case for development)
const ctx = await esbuild.context(cliConfig)
await ctx.watch()
console.log("[cli] Watching for changes...")
} else {
// Build both CLI and library
console.log("[cli esbuild] Building CLI executable...")
const cliCtx = await esbuild.context(cliConfig)
await cliCtx.rebuild()
await cliCtx.dispose()
console.log("[cli esbuild] Building library bundle...")
const libCtx = await esbuild.context(libConfig)
await libCtx.rebuild()
await libCtx.dispose()
// Make the CLI output executable
const cliOutfile = path.join(__dirname, "dist", "cli.mjs")
if (fs.existsSync(cliOutfile)) {
fs.chmodSync(cliOutfile, "755")
}
}
}
main().catch((e) => {
console.error(e)
process.exit(1)
})
-412
View File
@@ -1,412 +0,0 @@
.\" Automatically generated by Pandoc 3.8.3
.\"
.TH "CLINE" "1" "January 2026" "Cline CLI 2.0" "User Commands"
.SH NAME
cline \- AI coding assistant in your terminal
.SH SYNOPSIS
\f[B]cline\f[R] [\f[I]prompt\f[R]] [\f[I]options\f[R]]
.PP
\f[B]cline\f[R] \f[I]command\f[R] [\f[I]options\f[R]]
[\f[I]arguments\f[R]]
.SH DESCRIPTION
\f[B]cline\f[R] is a command\-line interface for the Cline AI coding
assistant.
It provides the same powerful AI capabilities as the VS Code extension,
directly in your terminal.
.PP
Cline is an autonomous AI agent that can read, write, and execute code
across your projects.
He can create and edit files, run terminal commands, use a headless
browser, and more\(emall while asking for your approval before taking
actions.
.PP
The CLI supports both interactive mode (with a rich terminal UI) and
plain text mode (for piped input and scripted workflows).
.SH MODES OF OPERATION
\f[B]Interactive Mode\f[R] : When you run \f[B]cline\f[R] without
arguments, it launches an interactive welcome prompt with a rich
terminal UI.
You can type your task, view conversation history, and interact with
Cline in real\-time.
.PP
\f[B]Task Mode\f[R] : Run \f[B]cline \(lqprompt\(rq\f[R] or \f[B]cline
task \(lqprompt\(rq\f[R] to immediately start a task.
If stdin is a TTY, you\(cqll see the interactive UI.
If stdin is piped or output is redirected, the CLI automatically
switches to plain text mode.
.PP
\f[B]Plain Text Mode\f[R] : Activated automatically when stdin is piped,
output is redirected, or \f[B]\-\-json\f[R]/\f[B]\-\-yolo\f[R] flags are
used.
Outputs clean text without the Ink UI, suitable for scripting and CI/CD
pipelines.
.SH AGENT BEHAVIOR
Cline operates in two primary modes:
.PP
\f[B]ACT MODE\f[R] : Cline actively uses tools to accomplish tasks.
He can read files, write code, execute commands, use a headless browser,
and more.
This is the default mode for task execution.
.PP
\f[B]PLAN MODE\f[R] : Cline gathers information and creates a detailed
plan before implementation.
He explores the codebase, asks clarifying questions, and presents a
strategy for user approval before switching to ACT MODE.
.SH COMMANDS
.SS task (alias: t)
Run a new task with a prompt.
.PP
\f[B]cline task\f[R] \f[I]prompt\f[R] [\f[I]options\f[R]]
.PP
\f[B]cline t\f[R] \f[I]prompt\f[R] [\f[I]options\f[R]] : Create and run
a new task.
Options:
.PP
\f[B]\-a\f[R], \f[B]\-\-act\f[R] : Run in act mode (default)
.PP
\f[B]\-p\f[R], \f[B]\-\-plan\f[R] : Run in plan mode
.PP
\f[B]\-y\f[R], \f[B]\-\-yolo\f[R] : Enable yolo/yes mode (auto\-approve
all actions, output in plain mode, exit process automatically when task
complete)
.PP
\f[B]\-m\f[R], \f[B]\-\-model\f[R] \f[I]model\f[R] : Model to use for
the task
.PP
\f[B]\-i\f[R], \f[B]\-\-images\f[R] \f[I]paths\&...\f[R] : Image file
paths to include with the task
.PP
\f[B]\-v\f[R], \f[B]\-\-verbose\f[R] : Show verbose output including
reasoning
.PP
\f[B]\-c\f[R], \f[B]\-\-cwd\f[R] \f[I]path\f[R] : Working directory for
the task
.PP
\f[B]\-\-config\f[R] \f[I]path\f[R] : Path to Cline configuration
directory
.PP
\f[B]\-\-thinking\f[R] : Enable extended thinking (1024 token budget)
.PP
\f[B]\-\-json\f[R] : Output messages as JSON instead of styled text
.PP
\f[B]\-T\f[R], \f[B]\-\-taskId\f[R] \f[I]id\f[R] : Resume an existing
task by ID.
The prompt argument becomes an optional follow\-up message.
.SS history (alias: h)
List task history with pagination.
.PP
\f[B]cline history\f[R] [\f[I]options\f[R]]
.PP
\f[B]cline h\f[R] [\f[I]options\f[R]] : Display previous tasks.
Options:
.PP
\f[B]\-n\f[R], \f[B]\-\-limit\f[R] \f[I]number\f[R] : Number of tasks to
show (default: 10)
.PP
\f[B]\-p\f[R], \f[B]\-\-page\f[R] \f[I]number\f[R] : Page number,
1\-based (default: 1)
.PP
\f[B]\-\-config\f[R] \f[I]path\f[R] : Path to Cline configuration
directory
.SS config
Show current configuration.
.PP
\f[B]cline config\f[R] [\f[I]options\f[R]] : Display global and
workspace state.
Options:
.PP
\f[B]\-\-config\f[R] \f[I]path\f[R] : Path to Cline configuration
directory
.SS auth
Authenticate a provider and configure the model.
.PP
\f[B]cline auth\f[R] [\f[I]options\f[R]] : Launch interactive
authentication wizard, or use quick setup flags.
Options:
.PP
\f[B]\-p\f[R], \f[B]\-\-provider\f[R] \f[I]id\f[R] : Provider ID for
quick setup (e.g., openai\-native, anthropic, openrouter, moonshot)
.PP
\f[B]\-k\f[R], \f[B]\-\-apikey\f[R] \f[I]key\f[R] : API key for the
provider
.PP
\f[B]\-m\f[R], \f[B]\-\-modelid\f[R] \f[I]id\f[R] : Model ID to
configure (e.g., gpt\-4o, claude\-sonnet\-4\-5\-20250929, kimi\-k2.5)
.PP
\f[B]\-b\f[R], \f[B]\-\-baseurl\f[R] \f[I]url\f[R] : Base URL (optional,
for OpenAI\-compatible providers)
.PP
\f[B]\-v\f[R], \f[B]\-\-verbose\f[R] : Show verbose output
.PP
\f[B]\-c\f[R], \f[B]\-\-cwd\f[R] \f[I]path\f[R] : Working directory
.PP
\f[B]\-\-config\f[R] \f[I]path\f[R] : Path to Cline configuration
directory
.SS update
Check for updates and install if available.
.PP
\f[B]cline update\f[R] [\f[I]options\f[R]] : Check npm for newer
versions.
Options:
.PP
\f[B]\-v\f[R], \f[B]\-\-verbose\f[R] : Show verbose output
.SS version
Show the CLI version number.
.PP
\f[B]cline version\f[R]
.SS dev
Developer tools and utilities.
.PP
\f[B]cline dev log\f[R] : Open the log file for debugging.
.SH DEFAULT COMMAND OPTIONS
When running \f[B]cline\f[R] with just a prompt (no subcommand), these
options are available:
.PP
\f[B]\-a\f[R], \f[B]\-\-act\f[R] : Run in act mode (default)
.PP
\f[B]\-p\f[R], \f[B]\-\-plan\f[R] : Run in plan mode
.PP
\f[B]\-y\f[R], \f[B]\-\-yolo\f[R] : Enable yolo mode (auto\-approve all
actions).
Also forces plain text output mode.
.PP
\f[B]\-m\f[R], \f[B]\-\-model\f[R] \f[I]model\f[R] : Model to use for
the task
.PP
\f[B]\-v\f[R], \f[B]\-\-verbose\f[R] : Show verbose output
.PP
\f[B]\-c\f[R], \f[B]\-\-cwd\f[R] \f[I]path\f[R] : Working directory
.PP
\f[B]\-\-config\f[R] \f[I]path\f[R] : Configuration directory
.PP
\f[B]\-\-thinking\f[R] : Enable extended thinking (1024 token budget)
.PP
\f[B]\-\-json\f[R] : Output messages as JSON instead of styled text.
Forces plain text mode.
.PP
\f[B]\-T\f[R], \f[B]\-\-taskId\f[R] \f[I]id\f[R] : Resume an existing
task by ID instead of starting a new one.
The prompt becomes an optional follow\-up message.
.SH JSON OUTPUT FORMAT
When using \f[B]\-\-json\f[R], each message is output as a JSON object
with these fields:
.PP
\f[B]Required fields:\f[R]
.IP \(bu 2
\f[B]type\f[R]: \(lqask\(rq or \(lqsay\(rq
.IP \(bu 2
\f[B]text\f[R]: message text
.IP \(bu 2
\f[B]ts\f[R]: Unix epoch timestamp in milliseconds
.PP
\f[B]Optional fields:\f[R]
.IP \(bu 2
\f[B]reasoning\f[R]: reasoning text
.IP \(bu 2
\f[B]say\f[R]: say subtype (when type is \(lqsay\(rq)
.IP \(bu 2
\f[B]ask\f[R]: ask subtype (when type is \(lqask\(rq)
.IP \(bu 2
\f[B]partial\f[R]: streaming flag
.IP \(bu 2
\f[B]images\f[R]: list of image URIs
.IP \(bu 2
\f[B]files\f[R]: list of file paths
.SH EXAMPLES
.SS Basic Usage
.IP
.EX
\f[I]# Launch interactive mode\f[R]
cline
\f[I]# Run a task directly\f[R]
cline \(dqCreate a hello world function in Python\(dq
\f[I]# Run with verbose output and extended thinking\f[R]
cline \-v \-\-thinking \(dqAnalyze this codebase architecture\(dq
.EE
.SS Mode Selection
.IP
.EX
\f[I]# Run in plan mode (gather info before acting)\f[R]
cline \-p \(dqDesign a REST API for user management\(dq
\f[I]# Run in act mode with auto\-approval (yolo)\f[R]
cline \-y \(dqFix the typo in README.md\(dq
.EE
.SS Using Specific Models
.IP
.EX
\f[I]# Use a specific model\f[R]
cline \-m claude\-sonnet\-4\-5\-20250929 \(dqRefactor this function\(dq
\f[I]# Quick auth setup with model\f[R]
cline auth \-p anthropic \-k sk\-ant\-xxxxx \-m claude\-sonnet\-4\-5\-20250929
\f[I]# Quick auth setup for Moonshot\f[R]
cline auth \-p moonshot \-k sk\-xxxxx \-m kimi\-k2.5
.EE
.SS Including Images
.IP
.EX
\f[I]# Include images with explicit flag\f[R]
cline task \-i screenshot.png diagram.jpg \(dqFix the UI based on these images\(dq
\f[I]# Or use inline image references in the prompt\f[R]
cline \(dqFix the layout shown in \(at./screenshot.png\(dq
.EE
.SS Piped Input
.IP
.EX
\f[I]# Pipe file contents to Cline\f[R]
cat README.md \f[B]|\f[R] cline \(dqSummarize this document\(dq
\f[I]# Pipe with additional prompt\f[R]
echo \(dqfunction add(a, b) { return a + b }\(dq \f[B]|\f[R] cline \(dqAdd TypeScript types to this\(dq
\f[I]# Combine piped input with a prompt\f[R]
git diff \f[B]|\f[R] cline \(dqReview these changes and suggest improvements\(dq
.EE
.SS Scripting and Automation
.IP
.EX
\f[I]# JSON output for parsing\f[R]
cline \-\-json \(dqWhat files are in this directory?\(dq \f[B]|\f[R] jq \(aq.text\(aq
\f[I]# Yolo mode for automated workflows (auto\-approves all actions), forces plain text output\f[R]
cline \-y \(dqRun the test suite and fix any failures\(dq
.EE
.SS Task History
.IP
.EX
\f[I]# List recent tasks\f[R]
cline history
\f[I]# Show more tasks with pagination\f[R]
cline history \-n 20 \-p 2
.EE
.SS Resuming Tasks
.IP
.EX
\f[I]# Resume a task by ID (get IDs from cline history)\f[R]
cline \-T abc123def
\f[I]# Resume a task with a follow\-up message\f[R]
cline \-T abc123def \(dqNow add unit tests for the changes\(dq
\f[I]# Resume in plan mode to review before continuing\f[R]
cline \-T abc123def \-p \(dqWhat\(aqs left to do?\(dq
\f[I]# Resume with yolo mode for automated continuation\f[R]
cline \-T abc123def \-y \(dqContinue with the implementation\(dq
.EE
.SS Authentication
.IP
.EX
\f[I]# Interactive authentication wizard\f[R]
cline auth
\f[I]# Quick setup for Anthropic\f[R]
cline auth \-p anthropic \-k sk\-ant\-api\-xxxxx
\f[I]# Quick setup for OpenAI\f[R]
cline auth \-p openai\-native \-k sk\-xxxxx \-m gpt\-4o
\f[I]# Quick setup for Moonshot\f[R]
cline auth \-p moonshot \-k sk\-xxxxx \-m kimi\-k2.5
\f[I]# OpenAI\-compatible provider with custom base URL\f[R]
cline auth \-p openai \-k your\-api\-key \-b https://api.example.com/v1
.EE
.SH ENVIRONMENT
\f[B]CLINE_DIR\f[R] : Override the default configuration directory.
When set, Cline stores all data in this directory instead of
\f[CR]\(ti/.cline/data/\f[R].
.PP
\f[B]CLINE_COMMAND_PERMISSIONS\f[R] : JSON configuration for restricting
which shell commands Cline can execute.
When set, commands are validated against allow/deny patternks before
execution.
When not set, all commands are allowed.
.PP
Format:
\f[CR]{\(dqallow\(dq: [\(dqpattern1\(dq, \(dqpattern2\(dq], \(dqdeny\(dq: [\(dqpattern3\(dq], \(dqallowRedirects\(dq: true}\f[R]
.PP
\f[B]Fields:\f[R]
.IP \(bu 2
\f[B]allow\f[R] (array of strings): Glob patterns for allowed commands.
If specified, only matching commands are permitted.
Uses \f[CR]*\f[R] to match any characters and \f[CR]?\f[R] to match a
single character.
Setting allow on anything will deny all others.
.IP \(bu 2
\f[B]deny\f[R] (array of strings): Glob patterns for denied commands.
Deny rules take precedence over allow rules.
.IP \(bu 2
\f[B]allowRedirects\f[R] (boolean): Whether to allow shell redirects
(\f[CR]>\f[R], \f[CR]>>\f[R], \f[CR]<\f[R], etc.).
Defaults to false.
.PP
\f[B]Rule evaluation:\f[R]
.IP "1." 3
Check for dangerous characters (backticks outside single quotes,
unquoted newlines)
.IP "2." 3
Parse command into segments split by operators (\f[CR]&&\f[R],
\f[CR]||\f[R], \f[CR]|\f[R], \f[CR];\f[R])
.IP "3." 3
If redirects detected and \f[CR]allowRedirects\f[R] is not true, command
is denied
.IP "4." 3
Each segment is validated against deny rules first, then allow rules
.IP "5." 3
Subshell contents (\f[CR]$(...)\f[R] and \f[CR](...)\f[R]) are
recursively validated
.IP "6." 3
All segments must pass for the command to be allowed
.PP
\f[B]Examples:\f[R]
.IP
.EX
\f[I]# Allow only npm and git commands.\f[R]
export CLINE_COMMAND_PERMISSIONS=\(aq{\(dqallow\(dq: [\(dqnpm *\(dq, \(dqgit *\(dq]}\(aq
\f[I]# Allow development commands but deny dangerous ones. Deny not strictly required here since allow is set.\f[R]
export CLINE_COMMAND_PERMISSIONS=\(aq{\(dqallow\(dq: [\(dqnpm *\(dq, \(dqgit *\(dq, \(dqnode *\(dq], \(dqdeny\(dq: [\(dqrm \-rf *\(dq, \(dqsudo *\(dq]}\(aq
\f[I]# Allow file operations with redirects\f[R]
export CLINE_COMMAND_PERMISSIONS=\(aq{\(dqallow\(dq: [\(dqcat *\(dq, \(dqecho *\(dq], \(dqallowRedirects\(dq: true}\(aq
.EE
.SH CONFIGURATION FILES
.IP
.EX
\(ti/.cline/
├── data/ # Default configuration directory
│ ├── globalState.json # Global settings and state
│ ├── secrets.json # API keys and secrets (stored securely)
│ ├── workspace/ # Workspace\-specific state
│ └── tasks/ # Task history and conversation data
└── log/ # Log files for debugging
.EE
.PP
View logs with \f[CR]cline dev log\f[R].
.SH BUGS
Report bugs at: \c
.UR https://github.com/cline/cline/issues
.UE \c
.PP
For real\-time help, join the Discord community at: \c
.UR https://discord.gg/cline
.UE \c
.SH SEE ALSO
Full documentation: \c
.UR https://docs.cline.bot
.UE \c
.PP
VS Code extension: \c
.UR https://marketplace.visualstudio.com/items?itemName=saoudrizwan.claude-dev
.UE \c
.SH AUTHORS
Cline is developed by Cline Bot Inc.\ and the open source community.
.SH COPYRIGHT
Copyright © 2025 Cline Bot Inc.\ Licensed under the Apache License 2.0.
-369
View File
@@ -1,369 +0,0 @@
---
title: CLINE
section: 1
header: User Commands
footer: Cline CLI 2.0
date: January 2026
---
# NAME
cline - AI coding assistant in your terminal
# SYNOPSIS
**cline** [*prompt*] [*options*]
**cline** *command* [*options*] [*arguments*]
# DESCRIPTION
**cline** is a command-line interface for the Cline AI coding assistant. It provides the same powerful AI capabilities as the VS Code extension, directly in your terminal.
Cline is an autonomous AI agent that can read, write, and execute code across your projects. He can create and edit files, run terminal commands, use a headless browser, and more—all while asking for your approval before taking actions.
The CLI supports both interactive mode (with a rich terminal UI) and plain text mode (for piped input and scripted workflows).
# MODES OF OPERATION
**Interactive Mode** : When you run **cline** without arguments, it launches an interactive welcome prompt with a rich terminal UI. You can type your task, view conversation history, and interact with Cline in real-time.
**Task Mode** : Run **cline "prompt"** or **cline task "prompt"** to immediately start a task. If stdin is a TTY, you'll see the interactive UI. If stdin is piped or output is redirected, the CLI automatically switches to plain text mode.
**Plain Text Mode** : Activated automatically when stdin is piped, output is redirected, or **\--json**/**\--yolo** flags are used. Outputs clean text without the Ink UI, suitable for scripting and CI/CD pipelines.
# AGENT BEHAVIOR
Cline operates in two primary modes:
**ACT MODE** : Cline actively uses tools to accomplish tasks. He can read files, write code, execute commands, use a headless browser, and more. This is the default mode for task execution.
**PLAN MODE** : Cline gathers information and creates a detailed plan before implementation. He explores the codebase, asks clarifying questions, and presents a strategy for user approval before switching to ACT MODE.
# COMMANDS
## task (alias: t)
Run a new task with a prompt.
**cline task** *prompt* [*options*]
**cline t** *prompt* [*options*] : Create and run a new task. Options:
**-a**, **\--act** : Run in act mode (default)
**-p**, **\--plan** : Run in plan mode
**-y**, **\--yolo** : Enable yolo/yes mode (auto-approve all actions, output in plain mode, exit process automatically when task complete)
**-t**, **\--timeout** *seconds* : Optional timeout in seconds. Only applied when explicitly provided.
**-m**, **\--model** *model* : Model to use for the task
**-i**, **\--images** *paths...* : Image file paths to include with the task
**-v**, **\--verbose** : Show verbose output including reasoning
**-c**, **\--cwd** *path* : Working directory for the task
**\--config** *path* : Path to Cline configuration directory
**\--thinking** : Enable extended thinking (1024 token budget)
**\--json** : Output messages as JSON instead of styled text
**-T**, **\--taskId** *id* : Resume an existing task by ID. The prompt argument becomes an optional follow-up message.
## history (alias: h)
List task history with pagination.
**cline history** [*options*]
**cline h** [*options*] : Display previous tasks. Options:
**-n**, **\--limit** *number* : Number of tasks to show (default: 10)
**-p**, **\--page** *number* : Page number, 1-based (default: 1)
**\--config** *path* : Path to Cline configuration directory
## config
Show current configuration.
**cline config** [*options*] : Display global and workspace state. Options:
**\--config** *path* : Path to Cline configuration directory
## auth
Authenticate a provider and configure the model.
**cline auth** [*options*] : Launch interactive authentication wizard, or use quick setup flags. Options:
**-p**, **\--provider** *id* : Provider ID for quick setup (e.g., openai-native, anthropic, openrouter)
**-k**, **\--apikey** *key* : API key for the provider
**-m**, **\--modelid** *id* : Model ID to configure (e.g., gpt-4o, claude-sonnet-4-5-20250929)
**-b**, **\--baseurl** *url* : Base URL (optional, for OpenAI-compatible providers)
**-v**, **\--verbose** : Show verbose output
**-c**, **\--cwd** *path* : Working directory
**\--config** *path* : Path to Cline configuration directory
## update
Check for updates and install if available.
**cline update** [*options*] : Check npm for newer versions. Options:
**-v**, **\--verbose** : Show verbose output
## version
Show the CLI version number.
**cline version**
## dev
Developer tools and utilities.
**cline dev log** : Open the log file for debugging.
# DEFAULT COMMAND OPTIONS
When running **cline** with just a prompt (no subcommand), these options are available:
**-a**, **\--act** : Run in act mode (default)
**-p**, **\--plan** : Run in plan mode
**-y**, **\--yolo** : Enable yolo mode (auto-approve all actions). Also forces plain text output mode.
**-t**, **\--timeout** *seconds* : Optional timeout in seconds. Only applied when explicitly provided.
**-m**, **\--model** *model* : Model to use for the task
**-v**, **\--verbose** : Show verbose output
**-c**, **\--cwd** *path* : Working directory
**\--config** *path* : Configuration directory
**\--thinking** : Enable extended thinking (1024 token budget)
**\--json** : Output messages as JSON instead of styled text. Forces plain text mode.
**-T**, **\--taskId** *id* : Resume an existing task by ID instead of starting a new one. The prompt becomes an optional follow-up message.
**\--continue** : Resume the most recent task from the current working directory instead of starting a new one.
# JSON OUTPUT FORMAT
When using **\--json**, each message is output as a JSON object with these fields:
**Required fields:**
- **type**: "ask" or "say"
- **text**: message text
- **ts**: Unix epoch timestamp in milliseconds
**Optional fields:**
- **reasoning**: reasoning text
- **say**: say subtype (when type is "say")
- **ask**: ask subtype (when type is "ask")
- **partial**: streaming flag
- **images**: list of image URIs
- **files**: list of file paths
# EXAMPLES
## Basic Usage
```bash
# Launch interactive mode
cline
# Run a task directly
cline "Create a hello world function in Python"
# Run with verbose output and extended thinking
cline -v --thinking "Analyze this codebase architecture"
```
## Mode Selection
```bash
# Run in plan mode (gather info before acting)
cline -p "Design a REST API for user management"
# Run in act mode with auto-approval (yolo)
cline -y "Fix the typo in README.md"
```
## Using Specific Models
```bash
# Use a specific model
cline -m claude-sonnet-4-5-20250929 "Refactor this function"
# Quick auth setup with model
cline auth -p anthropic -k sk-ant-xxxxx -m claude-sonnet-4-5-20250929
```
## Including Images
```bash
# Include images with explicit flag
cline task -i screenshot.png diagram.jpg "Fix the UI based on these images"
# Or use inline image references in the prompt
cline "Fix the layout shown in @./screenshot.png"
```
## Piped Input
```bash
# Pipe file contents to Cline
cat README.md | cline "Summarize this document"
# Pipe with additional prompt
echo "function add(a, b) { return a + b }" | cline "Add TypeScript types to this"
# Combine piped input with a prompt
git diff | cline "Review these changes and suggest improvements"
```
## Scripting and Automation
```bash
# JSON output for parsing
cline --json "What files are in this directory?" | jq '.text'
# Yolo mode for automated workflows (auto-approves all actions), forces plain text output
cline -y "Run the test suite and fix any failures"
```
## Task History
```bash
# List recent tasks
cline history
# Show more tasks with pagination
cline history -n 20 -p 2
```
## Resuming Tasks
```bash
# Resume a task by ID (get IDs from cline history)
cline -T abc123def
# Resume a task with a follow-up message
cline -T abc123def "Now add unit tests for the changes"
# Resume the most recent task from the current directory
cline --continue
# Resume in plan mode to review before continuing
cline -T abc123def -p "What's left to do?"
# Resume with yolo mode for automated continuation
cline -T abc123def -y "Continue with the implementation"
```
## Authentication
```bash
# Interactive authentication wizard
cline auth
# Quick setup for Anthropic
cline auth -p anthropic -k sk-ant-api-xxxxx
# Quick setup for OpenAI
cline auth -p openai-native -k sk-xxxxx -m gpt-4o
# OpenAI-compatible provider with custom base URL
cline auth -p openai -k your-api-key -b https://api.example.com/v1
```
# ENVIRONMENT
**CLINE_DIR** : Override the default configuration directory. When set, Cline stores all data in this directory instead of `~/.cline/data/`.
**CLINE_COMMAND_PERMISSIONS** : JSON configuration for restricting which shell commands Cline can execute. When set, commands are validated against allow/deny patternks before execution. When not set, all commands are allowed.
Format: `{"allow": ["pattern1", "pattern2"], "deny": ["pattern3"], "allowRedirects": true}`
**Fields:**
- **allow** (array of strings): Glob patterns for allowed commands. If specified, only matching commands are permitted. Uses `*` to match any characters and `?` to match a single character. Setting allow on anything will deny all others.
- **deny** (array of strings): Glob patterns for denied commands. Deny rules take precedence over allow rules.
- **allowRedirects** (boolean): Whether to allow shell redirects (`>`, `>>`, `<`, etc.). Defaults to false.
**Rule evaluation:**
1. Check for dangerous characters (backticks outside single quotes, unquoted newlines)
2. Parse command into segments split by operators (`&&`, `||`, `|`, `;`)
3. If redirects detected and `allowRedirects` is not true, command is denied
4. Each segment is validated against deny rules first, then allow rules
5. Subshell contents (`$(...)` and `(...)`) are recursively validated
6. All segments must pass for the command to be allowed
**Examples:**
```bash
# Allow only npm and git commands.
export CLINE_COMMAND_PERMISSIONS='{"allow": ["npm *", "git *"]}'
# Allow development commands but deny dangerous ones. Deny not strictly required here since allow is set.
export CLINE_COMMAND_PERMISSIONS='{"allow": ["npm *", "git *", "node *"], "deny": ["rm -rf *", "sudo *"]}'
# Allow file operations with redirects
export CLINE_COMMAND_PERMISSIONS='{"allow": ["cat *", "echo *"], "allowRedirects": true}'
```
# CONFIGURATION FILES
```
~/.cline/
├── data/ # Default configuration directory
│ ├── globalState.json # Global settings and state
│ ├── secrets.json # API keys and secrets (stored securely)
│ ├── workspace/ # Workspace-specific state
│ └── tasks/ # Task history and conversation data
└── log/ # Log files for debugging
```
View logs with `cline dev log`.
# BUGS
Report bugs at: <https://github.com/cline/cline/issues>
For real-time help, join the Discord community at: <https://discord.gg/cline>
# SEE ALSO
Full documentation: <https://docs.cline.bot>
VS Code extension: <https://marketplace.visualstudio.com/items?itemName=saoudrizwan.claude-dev>
# AUTHORS
Cline is developed by Cline Bot Inc. and the open source community.
# COPYRIGHT
Copyright © 2025 Cline Bot Inc. Licensed under the Apache License 2.0.
-101
View File
@@ -1,101 +0,0 @@
{
"name": "cline",
"version": "2.11.0",
"description": "Autonomous coding agent CLI - capable of creating/editing files, running commands, using the browser, and more",
"main": "dist/lib.mjs",
"types": "dist/lib.d.ts",
"bin": {
"cline": "./dist/cli.mjs"
},
"exports": {
".": {
"import": "./dist/lib.mjs",
"types": "./dist/lib.d.ts"
}
},
"os": [
"darwin",
"linux",
"win32"
],
"cpu": [
"x64",
"arm64"
],
"man": "./man/cline.1",
"type": "module",
"engines": {
"node": ">=20.0.0"
},
"scripts": {
"package:brew": "npx tsx ./scripts/update-brew-formula.mts",
"package": "npm pack --pack-destination ./dist",
"build": "npm run typecheck && npx tsx esbuild.mts && npm run build:types",
"build:production": "npm run typecheck && npx tsx esbuild.mts --production && npm run build:types",
"build:types": "(npx tsc -p tsconfig.lib.json || true) && cp dist/types/cli/src/exports.d.ts dist/lib.d.ts && mkdir -p dist/agent && cp dist/types/cli/src/agent/ClineAgent.d.ts dist/types/cli/src/agent/ClineSessionEmitter.d.ts dist/types/cli/src/agent/public-types.d.ts dist/agent/ && rm -rf dist/types",
"watch": "npx tsx esbuild.mts --watch",
"dev": "IS_DEV=true && npm run link && npm run watch ; npm run unlink",
"clean": "rimraf dist",
"typecheck": "npx tsc --noEmit",
"link": "npm run build && npm link",
"unlink": "npm unlink -g cline",
"test": "vitest",
"test:run": "vitest run"
},
"keywords": [
"cline",
"claude",
"dev",
"mcp",
"openrouter",
"coding",
"agent",
"autonomous",
"chatgpt",
"sonnet",
"ai",
"llama",
"cli"
],
"author": {
"name": "Cline Bot Inc."
},
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "https://github.com/cline/cline"
},
"homepage": "https://cline.bot",
"bugs": {
"url": "https://github.com/cline/cline/issues"
},
"devDependencies": {
"@types/marked": "^5.0.2",
"@types/node": "20.x",
"@types/prompts": "^2.4.9",
"@types/react": "^19.2.9",
"dotenv": "^16.4.5",
"esbuild": "^0.25.0",
"ink-testing-library": "^4.0.0",
"rimraf": "^6.0.1",
"typescript": "^5.4.5",
"vitest": "^4.0.17"
},
"dependencies": {
"@agentclientprotocol/sdk": "^0.13.1",
"@vscode/ripgrep": "^1.15.9",
"aws4fetch": "^1.0.20",
"chalk": "^5.3.0",
"commander": "^12.1.0",
"ink": "npm:@jrichman/ink@6.4.7",
"ink-picture": "^1.3.3",
"ink-spinner": "^5.0.0",
"marked": "^17.0.3",
"nanoid": "^5.1.6",
"ora": "^8.0.1",
"pino": "^10.0.0",
"pino-roll": "^4.0.0",
"prompts": "^2.4.2",
"react": "^19.2.3"
}
}
-81
View File
@@ -1,81 +0,0 @@
#!/usr/bin/env node
import { execSync } from "node:child_process"
import { createHash } from "node:crypto"
import { readFile, unlink, writeFile } from "node:fs/promises"
import { dirname, join } from "node:path"
import { fileURLToPath } from "node:url"
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
const CLI_DIR = join(__dirname, "..")
const FORMULA_PATH = join(CLI_DIR, "cline.rb")
interface PackageJson {
version: string
}
async function getLocalVersion(): Promise<string> {
const packageJson = JSON.parse(await readFile(join(CLI_DIR, "package.json"), "utf-8")) as PackageJson
return packageJson.version
}
async function packAndGetSHA256(version: string): Promise<string> {
console.log("Packing local package...")
execSync("npm run package", { cwd: CLI_DIR, stdio: "inherit" })
const tarballPath = join(CLI_DIR, "dist", `cline-cli-${version}.tgz`)
console.log(`Computing SHA256 for ${tarballPath}...`)
const buffer = await readFile(tarballPath)
const sha256 = createHash("sha256").update(buffer).digest("hex")
// Clean up the tarball
await unlink(tarballPath)
return sha256
}
async function updateFormula(version: string, sha256: string) {
console.log("Updating Homebrew formula...")
let formula = await readFile(FORMULA_PATH, "utf-8")
const tarballUrl = `https://registry.npmjs.org/cline/-/cline-${version}.tgz`
// Update URL - matches pattern like: url "https://registry.npmjs.org/cline/-/cline-1.0.10.tgz"
formula = formula.replace(/url "https:\/\/registry\.npmjs\.org\/cline\/-\/cline-[\d.]+\.tgz"/, `url "${tarballUrl}"`)
// Update SHA256
formula = formula.replace(/sha256 "[a-f0-9]+"/, `sha256 "${sha256}"`)
await writeFile(FORMULA_PATH, formula, "utf-8")
}
async function main() {
try {
const version = await getLocalVersion()
console.log(`\nLocal version: ${version}`)
const sha256 = await packAndGetSHA256(version)
console.log(`SHA256: ${sha256}`)
const tarballUrl = `https://registry.npmjs.org/cline/-/cline-${version}.tgz`
console.log(`Tarball URL: ${tarballUrl}`)
await updateFormula(version, sha256)
console.log("\n✓ Homebrew formula updated successfully!")
console.log("\nNext steps:")
console.log("1. Review the changes in cline.rb")
console.log("2. Test locally: brew install --build-from-source ./cline.rb")
console.log("3. Commit and push to your homebrew tap repository")
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
console.error(`\n✗ Error: ${errorMessage}\n`)
process.exit(1)
}
}
main()
-248
View File
@@ -1,248 +0,0 @@
/**
* ACP-based implementation of DiffViewProvider that uses the ACP client's
* filesystem capabilities for reading and writing files.
*
* This provider attempts to use the ACP client's fs/read_text_file and
* fs/write_text_file methods when available, falling back to the
* FileEditProvider's local filesystem implementation otherwise.
*
* @module acp
*/
import type * as acp from "@agentclientprotocol/sdk"
import { workspaceResolver } from "@core/workspace"
import { createDirectoriesForFile } from "@utils/fs"
import { getCwd } from "@utils/path"
import * as fs from "fs/promises"
import * as iconv from "iconv-lite"
import { HostProvider } from "@/hosts/host-provider"
import { FileEditProvider } from "@/integrations/editor/FileEditProvider"
import { detectEncoding } from "@/integrations/misc/extract-text"
import type { FileDiagnostics } from "@/shared/proto/index.cline"
import { Logger } from "@/shared/services/Logger"
/**
* A function that resolves the current session ID.
* This is used by ACPDiffViewProvider to get the session ID at runtime,
* since the provider may be created before a session exists.
*/
export type SessionIdResolver = () => string | undefined
/**
* A DiffViewProvider implementation that uses the ACP client's filesystem
* capabilities when available, with fallback to local filesystem operations.
*
* This class extends FileEditProvider and overrides the file I/O methods to
* use the ACP protocol's fs/read_text_file and fs/write_text_file requests
* when the client supports these capabilities. This allows the editor (client)
* to handle file operations, which enables features like:
* - Reading unsaved editor state
* - Tracking file modifications in the editor
* - Proper integration with the client's undo/redo stack
*/
export class ACPDiffViewProvider extends FileEditProvider {
private readonly connection: acp.AgentSideConnection
private readonly clientCapabilities: acp.ClientCapabilities | undefined
private readonly sessionIdResolver: SessionIdResolver
/**
* Creates a new ACPDiffViewProvider.
*
* @param connection - The ACP agent-side connection for making requests
* @param clientCapabilities - The client's advertised capabilities
* @param sessionIdResolver - A function that returns the current session ID
*/
constructor(
connection: acp.AgentSideConnection,
clientCapabilities: acp.ClientCapabilities | undefined,
sessionIdResolver: SessionIdResolver,
) {
super()
this.connection = connection
this.clientCapabilities = clientCapabilities
this.sessionIdResolver = sessionIdResolver
}
/**
* Gets the current session ID, or throws if no session is active.
*/
private getSessionId(): string {
const sessionId = this.sessionIdResolver()
if (!sessionId) {
throw new Error("No active ACP session. Cannot perform file operation.")
}
return sessionId
}
/**
* Check if the client supports file read operations.
*/
private canReadFile(): boolean {
return this.clientCapabilities?.fs?.readTextFile === true
}
/**
* Check if the client supports file write operations.
*/
private canWriteFile(): boolean {
return this.clientCapabilities?.fs?.writeTextFile === true
}
/**
* Opens a file for editing, using ACP fs capabilities when available.
*
* If the client supports fs/read_text_file, this method will read the file
* content via the ACP connection, which may include unsaved editor state.
* Otherwise, it falls back to the FileEditProvider's local fs implementation.
*/
override async open(relPath: string, options?: { displayPath?: string }): Promise<void> {
// If we can't read files via ACP, fall back to FileEditProvider
if (!this.canReadFile()) {
Logger.debug("[ACPDiffViewProvider] Client does not support fs.readTextFile, falling back to local fs")
return super.open(relPath, options)
}
// Set up state - this replicates the DiffViewProvider.open() logic
// but uses ACP for file reading instead of local fs
this.isEditing = true
const cwd = await getCwd()
const absolutePathResolved = workspaceResolver.resolveWorkspacePath(cwd, relPath, "ACPDiffViewProvider.open.absolutePath")
this.absolutePath = typeof absolutePathResolved === "string" ? absolutePathResolved : absolutePathResolved.absolutePath
this.relPath = options?.displayPath ?? relPath
const fileExists = this.editType === "modify"
// Read file content
if (fileExists) {
// Try to save any dirty state in the editor first
try {
await HostProvider.workspace.saveOpenDocumentIfDirty({
filePath: this.absolutePath!,
})
} catch {
// Ignore errors - the host may not support this
}
// Read file content via ACP
try {
Logger.debug("[ACPDiffViewProvider] Reading file via ACP:", this.absolutePath)
const response = await this.connection.readTextFile({
sessionId: this.getSessionId(),
path: this.absolutePath!,
})
this.originalContent = response.content
// ACP always returns UTF-8 text content
this.fileEncoding = "utf8"
Logger.debug("[ACPDiffViewProvider] Read file successfully, length:", response.content.length)
} catch (error) {
// If ACP read fails, fall back to local fs
Logger.debug("[ACPDiffViewProvider] ACP read failed, falling back to local fs:", error)
const fileBuffer = await fs.readFile(this.absolutePath!)
this.fileEncoding = await detectEncoding(fileBuffer)
this.originalContent = iconv.decode(fileBuffer, this.fileEncoding)
}
} else {
this.originalContent = ""
this.fileEncoding = "utf8"
}
// Create directories for new files
const createdDirs = await createDirectoriesForFile(this.absolutePath!)
// Store for potential cleanup - access via the private field workaround
;(this as any).createdDirs = createdDirs
// Make sure the file exists before we proceed
if (!fileExists) {
// For new files, write via ACP if possible, otherwise local fs
if (this.canWriteFile()) {
try {
await this.connection.writeTextFile({
sessionId: this.getSessionId(),
path: this.absolutePath!,
content: "",
})
} catch {
// Fall back to local fs
await fs.writeFile(this.absolutePath!, "")
}
} else {
await fs.writeFile(this.absolutePath!, "")
}
}
// Get diagnostics before editing
let preDiagnostics: FileDiagnostics[] = []
try {
preDiagnostics = (await HostProvider.workspace.getDiagnostics({})).fileDiagnostics
} catch {
preDiagnostics = []
}
;(this as any).preDiagnostics = preDiagnostics
// Call the parent's openDiffEditor to set up in-memory document content
await this.openDiffEditor()
await this.scrollEditorToLine(0)
;(this as any).streamedLines = []
}
/**
* Scrolls the editor to a specific line.
* No-op for file-based providers, but needed for protected access.
*/
protected override async scrollEditorToLine(_line: number): Promise<void> {
// No-op: No visual editor to scroll
}
/**
* Opens the diff editor.
*/
protected override async openDiffEditor(): Promise<void> {
// Set up in-memory document content from the original content
// no-op: No visual editor to open
}
/**
* Saves the document content, using ACP fs capabilities when available.
*
* If the client supports fs/write_text_file, this method will write the file
* content via the ACP connection. Otherwise, it falls back to the
* FileEditProvider's local fs implementation.
*/
protected override async saveDocument(): Promise<Boolean> {
// If we can't write files via ACP, fall back to FileEditProvider
if (!this.canWriteFile()) {
Logger.debug("[ACPDiffViewProvider] Client does not support fs.writeTextFile, falling back to local fs")
return super.saveDocument()
}
const content = await this.getContent()
if (!this.absolutePath || content === undefined) {
return false
}
try {
Logger.debug("[ACPDiffViewProvider] Writing file via ACP:", {
path: this.absolutePath,
contentLength: content.length,
})
await this.connection.writeTextFile({
sessionId: this.getSessionId(),
path: this.absolutePath,
content: content,
})
Logger.debug("[ACPDiffViewProvider] Write file successfully")
return true
} catch (error) {
// If ACP write fails, fall back to local fs
Logger.debug("[ACPDiffViewProvider] ACP write failed, falling back to local fs:", error)
return super.saveDocument()
}
}
}
-408
View File
@@ -1,408 +0,0 @@
/**
* ACP Host Bridge Client Provider
*
* Implements HostBridgeClientProvider for ACP mode, providing stub implementations
* of the 4 required service clients. These clients conform to the interfaces in
* host-bridge-client-types.ts and will use ACP connection capabilities where applicable.
*
* @module acp
*/
import type * as acp from "@agentclientprotocol/sdk"
import type {
DiffServiceClientInterface,
EnvServiceClientInterface,
WindowServiceClientInterface,
WorkspaceServiceClientInterface,
} from "@generated/hosts/host-bridge-client-types"
import type { HostBridgeClientProvider, StreamingCallbacks } from "@hosts/host-provider-types"
import * as proto from "@shared/proto/index"
import { ClineClient } from "@/shared/cline"
import { Logger } from "@/shared/services/Logger"
/**
* Function type that resolves the current session ID.
* Returns undefined if no session is active.
*/
export type SessionIdResolver = () => string | undefined
/**
* Function type that resolves the current working directory.
* Returns undefined if no cwd is available (will fall back to process.cwd()).
*/
export type CwdResolver = () => string | undefined
/**
* ACP implementation of DiffService client.
*
* Handles diff operations for the ACP environment. Most operations are stubs
* that will be implemented in the next phase using ACP extension methods or
* the fs capabilities (readTextFile/writeTextFile).
*/
class ACPDiffServiceClient implements DiffServiceClientInterface {
async openDiff(_request: proto.host.OpenDiffRequest): Promise<proto.host.OpenDiffResponse> {
// Next phase: Could use ACP client capabilities to open a diff view in the editor.
// This would involve sending an ACP extension notification/request to the client
// to display a side-by-side diff of the original vs modified content.
Logger.debug("[ACPDiffServiceClient] openDiff called (stub)")
return proto.host.OpenDiffResponse.create({})
}
async getDocumentText(request: proto.host.GetDocumentTextRequest): Promise<proto.host.GetDocumentTextResponse> {
// Next phase: Use connection.readTextFile if clientCapabilities.fs.readTextFile is available.
// This would read the current document content from the editor, including any unsaved changes.
// For now, return empty content.
Logger.debug("[ACPDiffServiceClient] getDocumentText called (stub)", { diffId: request.diffId })
return proto.host.GetDocumentTextResponse.create({ content: "" })
}
async replaceText(_request: proto.host.ReplaceTextRequest): Promise<proto.host.ReplaceTextResponse> {
// Next phase: Use connection.writeTextFile if clientCapabilities.fs.writeTextFile is available.
// This would replace text in the document at the specified range.
Logger.debug("[ACPDiffServiceClient] replaceText called (stub)")
return proto.host.ReplaceTextResponse.create({})
}
async scrollDiff(_request: proto.host.ScrollDiffRequest): Promise<proto.host.ScrollDiffResponse> {
// Next phase: Send ACP extension notification to scroll the diff view to a specific line.
// No visual editor in ACP mode by default, so this is a no-op.
Logger.debug("[ACPDiffServiceClient] scrollDiff called (stub)")
return proto.host.ScrollDiffResponse.create({})
}
async truncateDocument(_request: proto.host.TruncateDocumentRequest): Promise<proto.host.TruncateDocumentResponse> {
// Next phase: Read file using readTextFile, truncate content, write back using writeTextFile.
// This is used to truncate a document to a specific line count.
Logger.debug("[ACPDiffServiceClient] truncateDocument called (stub)")
return proto.host.TruncateDocumentResponse.create({})
}
async saveDocument(_request: proto.host.SaveDocumentRequest): Promise<proto.host.SaveDocumentResponse> {
// Next phase: Use connection.writeTextFile to persist the document to disk.
// This saves the current document content to the file system.
Logger.debug("[ACPDiffServiceClient] saveDocument called (stub)")
return proto.host.SaveDocumentResponse.create({})
}
async closeAllDiffs(_request: proto.host.CloseAllDiffsRequest): Promise<proto.host.CloseAllDiffsResponse> {
// Next phase: Send ACP extension notification to close all diff views in the editor.
// No visual diff views in ACP mode by default, so this is a no-op.
Logger.debug("[ACPDiffServiceClient] closeAllDiffs called (stub)")
return proto.host.CloseAllDiffsResponse.create({})
}
async openMultiFileDiff(_request: proto.host.OpenMultiFileDiffRequest): Promise<proto.host.OpenMultiFileDiffResponse> {
// Next phase: Send ACP extension notification to open a multi-file diff view.
// This would display changes across multiple files in the editor.
Logger.debug("[ACPDiffServiceClient] openMultiFileDiff called (stub)")
return proto.host.OpenMultiFileDiffResponse.create({})
}
}
/**
* ACP implementation of EnvService client.
*
* Handles environment operations like clipboard access, version info, and telemetry.
* Most operations are stubs that will be implemented using ACP extension methods.
*/
class ACPEnvServiceClient implements EnvServiceClientInterface {
private readonly version: string
constructor(_clientCapabilities: acp.ClientCapabilities | undefined, _sessionIdResolver: SessionIdResolver, version: string) {
this.version = version
}
async debugLog(request: proto.cline.StringRequest): Promise<proto.cline.Empty> {
Logger.debug(request.value)
return proto.cline.Empty.create()
}
async clipboardWriteText(_request: proto.cline.StringRequest): Promise<proto.cline.Empty> {
Logger.debug("[ACPEnvServiceClient] clipboardWriteText called (stub)")
return proto.cline.Empty.create()
}
async clipboardReadText(_request: proto.cline.EmptyRequest): Promise<proto.cline.String> {
Logger.debug("[ACPEnvServiceClient] clipboardReadText called (stub)")
return proto.cline.String.create({ value: "" })
}
async getHostVersion(_request: proto.cline.EmptyRequest): Promise<proto.host.GetHostVersionResponse> {
// Return version info for the ACP agent.
return proto.host.GetHostVersionResponse.create({
version: this.version,
platform: "Cline ACP Agent",
clineType: ClineClient.Cli,
})
}
async getIdeRedirectUri(_request: proto.cline.EmptyRequest): Promise<proto.cline.String> {
Logger.debug("[ACPEnvServiceClient] getIdeRedirectUri called (stub)")
return proto.cline.String.create({ value: "" })
}
async getTelemetrySettings(_request: proto.cline.EmptyRequest): Promise<proto.host.GetTelemetrySettingsResponse> {
// Return telemetry as disabled by default in ACP mode.
return proto.host.GetTelemetrySettingsResponse.create({
isEnabled: proto.host.Setting.DISABLED,
})
}
subscribeToTelemetrySettings(
_request: proto.cline.EmptyRequest,
callbacks: StreamingCallbacks<proto.host.TelemetrySettingsEvent>,
): () => void {
// Send initial telemetry settings (disabled) and return unsubscribe function.
callbacks.onResponse(
proto.host.TelemetrySettingsEvent.create({
isEnabled: proto.host.Setting.DISABLED,
}),
)
// Return no-op unsubscribe function
return () => {}
}
async shutdown(_request: proto.cline.EmptyRequest): Promise<proto.cline.Empty> {
// Next phase: Graceful ACP connection shutdown.
// This would cleanly close the ACP connection and release resources.
Logger.debug("[ACPEnvServiceClient] shutdown called (stub)")
return proto.cline.Empty.create()
}
async openExternal(request: proto.cline.StringRequest): Promise<proto.cline.Empty> {
const url = request.value || ""
if (url) {
Logger.debug(`[ACPEnvServiceClient] openExternal: ${url}`)
const { openUrlInBrowser } = await import("../utils/browser")
await openUrlInBrowser(url)
}
return proto.cline.Empty.create()
}
}
/**
* ACP implementation of WindowService client.
*
* Handles window/UI operations like showing documents, dialogs, and messages.
* Most operations are stubs that will be implemented using ACP extension methods.
*/
class ACPWindowServiceClient implements WindowServiceClientInterface {
constructor(_clientCapabilities: acp.ClientCapabilities | undefined, _sessionIdResolver: SessionIdResolver) {}
async showTextDocument(request: proto.host.ShowTextDocumentRequest): Promise<proto.host.TextEditorInfo> {
// Next phase: Send ACP extension request to open document in the editor.
// This would tell the ACP client to open the specified file.
Logger.debug("[ACPWindowServiceClient] showTextDocument called (stub)", { path: request.path })
return proto.host.TextEditorInfo.create({
documentPath: request.path,
})
}
async showOpenDialogue(_request: proto.host.ShowOpenDialogueRequest): Promise<proto.host.SelectedResources> {
// Next phase: Send ACP extension request for file picker dialog.
// This would display a file open dialog in the ACP client.
Logger.debug("[ACPWindowServiceClient] showOpenDialogue called (stub)")
return proto.host.SelectedResources.create({ paths: [] })
}
async showMessage(request: proto.host.ShowMessageRequest): Promise<proto.host.SelectedResponse> {
// Next phase: Send ACP extension notification to show message in the editor.
// This would display an information/warning/error message to the user.
Logger.debug("[ACPWindowServiceClient] showMessage called (stub)", {
message: request.message,
type: request.type,
})
return proto.host.SelectedResponse.create({})
}
async showInputBox(_request: proto.host.ShowInputBoxRequest): Promise<proto.host.ShowInputBoxResponse> {
// Next phase: Send ACP extension request for input dialog.
// This would display an input box for user text entry.
Logger.debug("[ACPWindowServiceClient] showInputBox called (stub)")
return proto.host.ShowInputBoxResponse.create({ response: "" })
}
async showSaveDialog(_request: proto.host.ShowSaveDialogRequest): Promise<proto.host.ShowSaveDialogResponse> {
// Next phase: Send ACP extension request for save dialog.
// This would display a file save dialog in the ACP client.
Logger.debug("[ACPWindowServiceClient] showSaveDialog called (stub)")
return proto.host.ShowSaveDialogResponse.create({ selectedPath: "" })
}
async openFile(request: proto.host.OpenFileRequest): Promise<proto.host.OpenFileResponse> {
// Next phase: Send ACP extension request to open file in the editor.
// This would open the specified file in the ACP client's editor.
Logger.debug("[ACPWindowServiceClient] openFile called (stub)", { filePath: request.filePath })
return proto.host.OpenFileResponse.create({})
}
async openSettings(_request: proto.host.OpenSettingsRequest): Promise<proto.host.OpenSettingsResponse> {
// Next phase: Send ACP extension request to open settings panel.
// This would open the settings/preferences in the ACP client.
Logger.debug("[ACPWindowServiceClient] openSettings called (stub)")
return proto.host.OpenSettingsResponse.create({})
}
async getOpenTabs(_request: proto.host.GetOpenTabsRequest): Promise<proto.host.GetOpenTabsResponse> {
// Next phase: Send ACP extension request to list open tabs/documents.
// This would return a list of currently open files in the editor.
Logger.debug("[ACPWindowServiceClient] getOpenTabs called (stub)")
return proto.host.GetOpenTabsResponse.create({ paths: [] })
}
async getVisibleTabs(_request: proto.host.GetVisibleTabsRequest): Promise<proto.host.GetVisibleTabsResponse> {
// Next phase: Send ACP extension request to list visible tabs.
// This would return a list of visible tabs/panes in the editor.
Logger.debug("[ACPWindowServiceClient] getVisibleTabs called (stub)")
return proto.host.GetVisibleTabsResponse.create({ paths: [] })
}
async getActiveEditor(_request: proto.host.GetActiveEditorRequest): Promise<proto.host.GetActiveEditorResponse> {
// Next phase: Send ACP extension request to get active editor info.
// This would return information about the currently focused editor.
Logger.debug("[ACPWindowServiceClient] getActiveEditor called (stub)")
return proto.host.GetActiveEditorResponse.create({})
}
}
/**
* ACP implementation of WorkspaceService client.
*
* Handles workspace operations like getting paths, diagnostics, and terminal commands.
* Uses the cwdResolver to get the current working directory, falling back to process.cwd().
*/
class ACPWorkspaceServiceClient implements WorkspaceServiceClientInterface {
private readonly _clientCapabilities: acp.ClientCapabilities | undefined
private readonly cwdResolver: CwdResolver
constructor(
clientCapabilities: acp.ClientCapabilities | undefined,
_sessionIdResolver: SessionIdResolver,
cwdResolver: CwdResolver,
) {
this._clientCapabilities = clientCapabilities
this.cwdResolver = cwdResolver
}
/**
* Get the current working directory, using the resolver if available,
* otherwise falling back to process.cwd().
*/
private getCwd(): string {
return this.cwdResolver() ?? process.cwd()
}
async getWorkspacePaths(_request: proto.host.GetWorkspacePathsRequest): Promise<proto.host.GetWorkspacePathsResponse> {
// Return the current working directory from the resolver.
const cwd = this.getCwd()
Logger.debug("[ACPWorkspaceServiceClient] getWorkspacePaths called", { cwd })
return proto.host.GetWorkspacePathsResponse.create({
paths: [cwd],
})
}
async saveOpenDocumentIfDirty(
_request: proto.host.SaveOpenDocumentIfDirtyRequest,
): Promise<proto.host.SaveOpenDocumentIfDirtyResponse> {
// Next phase: Use ACP extension or fs.writeTextFile to save dirty documents.
// This would save any unsaved changes in the specified document.
Logger.debug("[ACPWorkspaceServiceClient] saveOpenDocumentIfDirty called (stub)")
return proto.host.SaveOpenDocumentIfDirtyResponse.create({})
}
async getDiagnostics(_request: proto.host.GetDiagnosticsRequest): Promise<proto.host.GetDiagnosticsResponse> {
// Next phase: Send ACP extension request for diagnostics (errors, warnings).
// This would return linting/compilation errors from the ACP client.
Logger.debug("[ACPWorkspaceServiceClient] getDiagnostics called (stub)")
return proto.host.GetDiagnosticsResponse.create({ fileDiagnostics: [] })
}
async openProblemsPanel(_request: proto.host.OpenProblemsPanelRequest): Promise<proto.host.OpenProblemsPanelResponse> {
// Next phase: Send ACP extension notification to open the problems panel.
// This would show the diagnostics/problems view in the editor.
Logger.debug("[ACPWorkspaceServiceClient] openProblemsPanel called (stub)")
return proto.host.OpenProblemsPanelResponse.create({})
}
async openInFileExplorerPanel(
request: proto.host.OpenInFileExplorerPanelRequest,
): Promise<proto.host.OpenInFileExplorerPanelResponse> {
// Next phase: Send ACP extension notification to reveal file in explorer.
// This would highlight/reveal the specified path in the file tree.
Logger.debug("[ACPWorkspaceServiceClient] openInFileExplorerPanel called (stub)", { path: request.path })
return proto.host.OpenInFileExplorerPanelResponse.create({})
}
async openClineSidebarPanel(
_request: proto.host.OpenClineSidebarPanelRequest,
): Promise<proto.host.OpenClineSidebarPanelResponse> {
// Next phase: Send ACP extension notification to open Cline sidebar.
// This would show the Cline panel/sidebar in the editor.
Logger.debug("[ACPWorkspaceServiceClient] openClineSidebarPanel called (stub)")
return proto.host.OpenClineSidebarPanelResponse.create({})
}
async openTerminalPanel(_request: proto.host.OpenTerminalRequest): Promise<proto.host.OpenTerminalResponse> {
// Next phase: Send ACP extension notification or use createTerminal capability.
// This would open/show the terminal panel in the editor.
Logger.debug("[ACPWorkspaceServiceClient] openTerminalPanel called (stub)")
return proto.host.OpenTerminalResponse.create({})
}
async executeCommandInTerminal(
request: proto.host.ExecuteCommandInTerminalRequest,
): Promise<proto.host.ExecuteCommandInTerminalResponse> {
// Next phase: Use connection.createTerminal if clientCapabilities.terminal is available.
// This would execute the specified command in a terminal via the ACP client.
// The ACP SDK provides createTerminal() which returns a TerminalHandle with
// methods like currentOutput(), waitForExit(), kill(), and release().
Logger.debug("[ACPWorkspaceServiceClient] executeCommandInTerminal called (stub)", {
command: request.command,
hasTerminalCapability: this._clientCapabilities?.terminal,
})
return proto.host.ExecuteCommandInTerminalResponse.create({})
}
async openFolder(request: proto.host.OpenFolderRequest): Promise<proto.host.OpenFolderResponse> {
// Next phase: Send ACP extension request to change workspace/folder.
// This would open a new folder/workspace in the ACP client.
Logger.debug("[ACPWorkspaceServiceClient] openFolder called (stub)", { path: request.path })
return proto.host.OpenFolderResponse.create({ success: true })
}
}
/**
* ACP Host Bridge Client Provider
*
* Provides the 4 service clients required by HostBridgeClientProvider interface,
* implemented for the ACP environment. Uses the ACP connection and client capabilities
* to delegate operations to the ACP client where possible.
*/
export class ACPHostBridgeClientProvider implements HostBridgeClientProvider {
workspaceClient: WorkspaceServiceClientInterface
envClient: EnvServiceClientInterface
windowClient: WindowServiceClientInterface
diffClient: DiffServiceClientInterface
/**
* Creates a new ACPHostBridgeClientProvider.
*
* @param connection - The ACP agent-side connection for making requests
* @param clientCapabilities - The client's advertised capabilities
* @param sessionIdResolver - Function that returns the current session ID
* @param cwdResolver - Function that returns the current working directory
* @param debug - Whether to enable debug logging
* @param version - Version string for getHostVersion (optional)
*/
constructor(
clientCapabilities: acp.ClientCapabilities | undefined,
sessionIdResolver: SessionIdResolver,
cwdResolver: CwdResolver,
version: string,
) {
this.workspaceClient = new ACPWorkspaceServiceClient(clientCapabilities, sessionIdResolver, cwdResolver)
this.envClient = new ACPEnvServiceClient(clientCapabilities, sessionIdResolver, version)
this.windowClient = new ACPWindowServiceClient(clientCapabilities, sessionIdResolver)
this.diffClient = new ACPDiffServiceClient()
}
}
-141
View File
@@ -1,141 +0,0 @@
/**
* AcpAgent - Thin wrapper that bridges stdio connection to ClineAgent.
*
* This class wraps the ClineAgent and connects it to an ACP AgentSideConnection
* for stdio-based communication. It:
* - Wires up the permission handler to call connection.requestPermission()
* - Subscribes to ClineAgent session events and forwards them to connection.sessionUpdate()
* - Delegates all acp.Agent methods to the internal ClineAgent
*
* For programmatic usage without stdio, use ClineAgent directly.
*
* @module acp
*/
import type * as acp from "@agentclientprotocol/sdk"
import { Logger } from "@/shared/services/Logger.js"
import { ClineAgent } from "../agent/ClineAgent.js"
import { type AcpAgentOptions, type SessionUpdateType } from "../agent/types.js"
/**
* ACP Agent wrapper that bridges stdio connection to ClineAgent.
*
* This is the class used by runAcpMode() for stdio-based ACP communication.
* It creates an internal ClineAgent and wires up the connection for:
* - Permission requests (via connection.requestPermission)
* - Session updates (via connection.sessionUpdate)
*/
export class AcpAgent implements acp.Agent {
private readonly connection: acp.AgentSideConnection
private readonly clineAgent: ClineAgent
/** Track which sessions we've subscribed to for event forwarding */
private readonly subscribedSessions: Set<string> = new Set()
constructor(connection: acp.AgentSideConnection, options: AcpAgentOptions) {
this.connection = connection
// Create the internal ClineAgent
this.clineAgent = new ClineAgent(options)
// Wire up the permission handler to use the connection
this.clineAgent.setPermissionHandler(async (request) => {
try {
Logger.debug("[AcpAgent] Forwarding permission request to connection")
return await this.connection.requestPermission({
sessionId: request.sessionId,
toolCall: request.toolCall,
options: request.options,
})
} catch (error) {
Logger.debug("[AcpAgent] Error requesting permission:", error)
return { outcome: { outcome: "cancelled" } }
}
})
}
/**
* Subscribe to session events and forward them to the connection.
*/
private subscribeToSessionEvents(sessionId: string): void {
if (this.subscribedSessions.has(sessionId)) {
return
}
const emitter = this.clineAgent.emitterForSession(sessionId)
// Forward session update by adding the sessionUpdate discriminator
const forwardSessionUpdate = <K extends SessionUpdateType>(eventName: K) => {
emitter.on(eventName, (payload: Record<string, unknown>) => {
const update = {
sessionUpdate: eventName,
...payload,
} as acp.SessionUpdate
this.connection.sessionUpdate({ sessionId, update }).catch((error) => {
Logger.error(`[AcpAgent] Error forwarding ${eventName}:`, error)
})
})
}
// Forward all standard session updates
forwardSessionUpdate("agent_message_chunk")
forwardSessionUpdate("agent_thought_chunk")
forwardSessionUpdate("tool_call")
forwardSessionUpdate("tool_call_update")
forwardSessionUpdate("available_commands_update")
forwardSessionUpdate("plan")
forwardSessionUpdate("current_mode_update")
forwardSessionUpdate("user_message_chunk")
forwardSessionUpdate("config_option_update")
forwardSessionUpdate("session_info_update")
// Handle errors specially (not part of ACP SessionUpdate)
emitter.on("error", (error) => {
Logger.error("[AcpAgent] Session error:", error)
})
this.subscribedSessions.add(sessionId)
}
// ============================================================
// acp.Agent Interface Implementation - Delegate to ClineAgent
// ============================================================
async initialize(params: acp.InitializeRequest): Promise<acp.InitializeResponse> {
return await this.clineAgent.initialize(params, this.connection)
}
async newSession(params: acp.NewSessionRequest): Promise<acp.NewSessionResponse> {
const response = await this.clineAgent.newSession(params)
// Subscribe to events for this new session
this.subscribeToSessionEvents(response.sessionId)
return response
}
async prompt(params: acp.PromptRequest): Promise<acp.PromptResponse> {
// Ensure we're subscribed to this session's events
this.subscribeToSessionEvents(params.sessionId)
return this.clineAgent.prompt(params)
}
async cancel(params: acp.CancelNotification): Promise<void> {
return this.clineAgent.cancel(params)
}
async setSessionMode(params: acp.SetSessionModeRequest): Promise<acp.SetSessionModeResponse> {
return this.clineAgent.setSessionMode(params)
}
async unstable_setSessionModel(params: acp.SetSessionModelRequest): Promise<acp.SetSessionModelResponse> {
return this.clineAgent.unstable_setSessionModel(params)
}
async authenticate(params: acp.AuthenticateRequest): Promise<acp.AuthenticateResponse> {
return this.clineAgent.authenticate(params)
}
async shutdown(): Promise<void> {
this.subscribedSessions.clear()
return this.clineAgent.shutdown()
}
}
File diff suppressed because it is too large Load Diff
-141
View File
@@ -1,141 +0,0 @@
/**
* Entry point for ACP (Agent Client Protocol) mode.
*
* When the CLI is invoked with `--acp`, this module sets up the ACP connection
* and runs Cline as an ACP-compliant agent communicating over stdio.
*
* This module exports:
* - `ClineAgent` - Decoupled agent for programmatic use (no stdio dependency)
* - `AcpAgent` - Thin wrapper that bridges stdio connection to ClineAgent
* - `ClineSessionEmitter` - Typed EventEmitter for per-session events
* - `runAcpMode` - Function to run Cline in stdio-based ACP mode
*
* @module acp
*/
import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk"
import { Logger } from "@/shared/services/Logger"
import { AcpAgent } from "./AcpAgent.js"
import { nodeToWebReadable, nodeToWebWritable } from "./streamUtils.js"
// Re-export classes for programmatic use
export { ClineAgent } from "../agent/ClineAgent.js"
export { ClineSessionEmitter } from "../agent/ClineSessionEmitter.js"
export type {
AcpAgentOptions,
AcpSessionState,
ClineAgentOptions,
ClineSessionEvents,
PermissionHandler,
} from "../agent/types.js"
export { AcpAgent } from "./AcpAgent.js"
/** Original console methods for restoration if needed */
const originalConsole = {
log: console.log,
info: console.info,
warn: console.warn,
debug: console.debug,
error: console.error,
}
/**
* Redirect all console output to stderr.
*
* In ACP mode, stdout is reserved exclusively for JSON-RPC communication.
* All logging must go to stderr to avoid corrupting the protocol stream.
*/
function redirectConsoleToStderr(): void {
console.log = (...args) => console.error(...args)
console.info = (...args) => console.error(...args)
console.warn = (...args) => console.error(...args)
console.debug = (...args) => console.error(...args)
// console.error already goes to stderr
}
/**
* Restore console methods to their original behavior.
*/
export function restoreConsole(): void {
console.log = originalConsole.log
console.info = originalConsole.info
console.warn = originalConsole.warn
console.debug = originalConsole.debug
console.error = originalConsole.error
}
export interface AcpModeOptions {
/** Path to Cline configuration directory */
config?: string
/** Working directory (default: process.cwd()) */
cwd?: string
/** Additional runtime hooks directory */
hooksDir?: string
/** Enable verbose/debug logging to stderr */
verbose?: boolean
}
/**
* Run Cline in ACP mode.
*
* This function:
* 1. Redirects console output to stderr (stdout reserved for JSON-RPC)
* 2. Sets up the ndJsonStream for stdio communication
* 3. Creates the AgentSideConnection with our AcpAgent factory
* 4. Initializes the CLI infrastructure (StateManager, Controller, etc.)
* 5. Keeps the process alive until the connection closes
*
* @param options - Configuration options for ACP mode
*/
export async function runAcpMode(options: AcpModeOptions = {}): Promise<void> {
redirectConsoleToStderr()
const outputStream = nodeToWebWritable(process.stdout)
const inputStream = nodeToWebReadable(process.stdin)
const stream = ndJsonStream(outputStream, inputStream)
let agent: AcpAgent | null = null
new AgentSideConnection((conn) => {
agent = new AcpAgent(conn, {
debug: Boolean(options.verbose),
hooksDir: options.hooksDir,
})
return agent
}, stream)
let isShuttingDown = false
const shutdown = async () => {
if (isShuttingDown) {
// Force exit on second signal
process.exit(1)
}
isShuttingDown = true
try {
await agent?.shutdown()
restoreConsole()
} catch (error) {
Logger.error("[ACP] Error during shutdown:", error)
}
process.exit(0)
}
process.on("SIGINT", shutdown)
process.on("SIGTERM", shutdown)
// Keep the process alive
// The ndJsonStream will handle stdin events automatically.
// We need to ensure the process doesn't exit while waiting for input.
process.stdin.resume()
// Handle stdin end (client disconnected)
process.stdin.on("end", shutdown)
// Handle stdin errors
process.stdin.on("error", async (error) => {
Logger.error("[ACP] stdin error:", error)
await shutdown()
})
Logger.info("[ACP] Process is now listening for ACP requests on stdin")
}
-54
View File
@@ -1,54 +0,0 @@
/**
* Stream conversion utilities for ACP mode.
*
* The ACP SDK's ndJsonStream function expects Web Streams (ReadableStream/WritableStream),
* but Node.js provides its own stream types. These utilities convert between them.
*
* @module acp/streamUtils
*/
import type { Readable, Writable } from "node:stream"
/**
* Convert a Node.js Writable stream to a Web WritableStream.
*
* Used to convert process.stdout for ACP output.
*
* @param nodeStream - Node.js Writable stream (e.g., process.stdout)
* @returns Web WritableStream compatible with ndJsonStream
*/
export function nodeToWebWritable(nodeStream: Writable): WritableStream<Uint8Array> {
return new WritableStream<Uint8Array>({
write(chunk) {
return new Promise<void>((resolve, reject) => {
nodeStream.write(Buffer.from(chunk), (err) => {
if (err) {
reject(err)
} else {
resolve()
}
})
})
},
})
}
/**
* Convert a Node.js Readable stream to a Web ReadableStream.
*
* Used to convert process.stdin for ACP input.
*
* @param nodeStream - Node.js Readable stream (e.g., process.stdin)
* @returns Web ReadableStream compatible with ndJsonStream
*/
export function nodeToWebReadable(nodeStream: Readable): ReadableStream<Uint8Array> {
return new ReadableStream<Uint8Array>({
start(controller) {
nodeStream.on("data", (chunk: Buffer) => {
controller.enqueue(new Uint8Array(chunk))
})
nodeStream.on("end", () => controller.close())
nodeStream.on("error", (err) => controller.error(err))
},
})
}
File diff suppressed because it is too large Load Diff
-274
View File
@@ -1,274 +0,0 @@
/**
* Tests for ClineSessionEmitter - Typed EventEmitter for per-session ACP events.
*/
import { beforeEach, describe, expect, it, vi } from "vitest"
import { ClineSessionEmitter } from "./ClineSessionEmitter.js"
import type { SessionUpdatePayload } from "./types.js"
describe("ClineSessionEmitter", () => {
let emitter: ClineSessionEmitter
beforeEach(() => {
emitter = new ClineSessionEmitter()
})
describe("on/emit", () => {
it("should emit and receive agent_message_chunk events", () => {
const listener = vi.fn()
const payload: SessionUpdatePayload<"agent_message_chunk"> = {
content: { type: "text", text: "Hello, world!" },
}
emitter.on("agent_message_chunk", listener)
emitter.emit("agent_message_chunk", payload)
expect(listener).toHaveBeenCalledTimes(1)
expect(listener).toHaveBeenCalledWith(payload)
})
it("should emit and receive agent_thought_chunk events", () => {
const listener = vi.fn()
const payload: SessionUpdatePayload<"agent_thought_chunk"> = {
content: { type: "text", text: "Thinking..." },
}
emitter.on("agent_thought_chunk", listener)
emitter.emit("agent_thought_chunk", payload)
expect(listener).toHaveBeenCalledTimes(1)
expect(listener).toHaveBeenCalledWith(payload)
})
it("should emit and receive tool_call events", () => {
const listener = vi.fn()
const payload: SessionUpdatePayload<"tool_call"> = {
toolCallId: "test-tool-call-id",
title: "Test Tool Call",
status: "in_progress",
}
emitter.on("tool_call", listener)
emitter.emit("tool_call", payload)
expect(listener).toHaveBeenCalledTimes(1)
expect(listener).toHaveBeenCalledWith(payload)
})
it("should emit and receive tool_call_update events", () => {
const listener = vi.fn()
const payload: SessionUpdatePayload<"tool_call_update"> = {
toolCallId: "test-tool-call-id",
status: "completed",
rawOutput: { result: "success" },
}
emitter.on("tool_call_update", listener)
emitter.emit("tool_call_update", payload)
expect(listener).toHaveBeenCalledTimes(1)
expect(listener).toHaveBeenCalledWith(payload)
})
it("should emit and receive available_commands_update events", () => {
const listener = vi.fn()
const payload: SessionUpdatePayload<"available_commands_update"> = {
availableCommands: [{ name: "test", description: "Test command" }],
}
emitter.on("available_commands_update", listener)
emitter.emit("available_commands_update", payload)
expect(listener).toHaveBeenCalledTimes(1)
expect(listener).toHaveBeenCalledWith(payload)
})
it("should emit and receive current_mode_update events", () => {
const listener = vi.fn()
const payload: SessionUpdatePayload<"current_mode_update"> = {
currentModeId: "act",
}
emitter.on("current_mode_update", listener)
emitter.emit("current_mode_update", payload)
expect(listener).toHaveBeenCalledTimes(1)
expect(listener).toHaveBeenCalledWith(payload)
})
it("should emit and receive plan events", () => {
const listener = vi.fn()
const payload: SessionUpdatePayload<"plan"> = {
entries: [{ content: "Step 1", status: "pending", priority: "high" }],
}
emitter.on("plan", listener)
emitter.emit("plan", payload)
expect(listener).toHaveBeenCalledTimes(1)
expect(listener).toHaveBeenCalledWith(payload)
})
it("should emit and receive error events", () => {
const listener = vi.fn()
const error = new Error("Test error")
emitter.on("error", listener)
emitter.emit("error", error)
expect(listener).toHaveBeenCalledTimes(1)
expect(listener).toHaveBeenCalledWith(error)
})
})
describe("multiple listeners", () => {
it("should support multiple listeners for the same event", () => {
const listener1 = vi.fn()
const listener2 = vi.fn()
const payload: SessionUpdatePayload<"agent_message_chunk"> = {
content: { type: "text", text: "Hello" },
}
emitter.on("agent_message_chunk", listener1)
emitter.on("agent_message_chunk", listener2)
emitter.emit("agent_message_chunk", payload)
expect(listener1).toHaveBeenCalledTimes(1)
expect(listener2).toHaveBeenCalledTimes(1)
})
it("should call listeners in order of registration", () => {
const order: number[] = []
const listener1 = vi.fn(() => order.push(1))
const listener2 = vi.fn(() => order.push(2))
const payload: SessionUpdatePayload<"agent_message_chunk"> = {
content: { type: "text", text: "Hello" },
}
emitter.on("agent_message_chunk", listener1)
emitter.on("agent_message_chunk", listener2)
emitter.emit("agent_message_chunk", payload)
expect(order).toEqual([1, 2])
})
})
describe("off", () => {
it("should remove a specific listener", () => {
const listener = vi.fn()
const payload: SessionUpdatePayload<"agent_message_chunk"> = {
content: { type: "text", text: "Hello" },
}
emitter.on("agent_message_chunk", listener)
emitter.off("agent_message_chunk", listener)
emitter.emit("agent_message_chunk", payload)
expect(listener).not.toHaveBeenCalled()
})
it("should only remove the specified listener", () => {
const listener1 = vi.fn()
const listener2 = vi.fn()
const payload: SessionUpdatePayload<"agent_message_chunk"> = {
content: { type: "text", text: "Hello" },
}
emitter.on("agent_message_chunk", listener1)
emitter.on("agent_message_chunk", listener2)
emitter.off("agent_message_chunk", listener1)
emitter.emit("agent_message_chunk", payload)
expect(listener1).not.toHaveBeenCalled()
expect(listener2).toHaveBeenCalledTimes(1)
})
})
describe("once", () => {
it("should only call the listener once", () => {
const listener = vi.fn()
const payload: SessionUpdatePayload<"agent_message_chunk"> = {
content: { type: "text", text: "Hello" },
}
emitter.once("agent_message_chunk", listener)
emitter.emit("agent_message_chunk", payload)
emitter.emit("agent_message_chunk", payload)
expect(listener).toHaveBeenCalledTimes(1)
})
})
describe("removeAllListeners", () => {
it("should remove all listeners for a specific event", () => {
const listener1 = vi.fn()
const listener2 = vi.fn()
const payload: SessionUpdatePayload<"agent_message_chunk"> = {
content: { type: "text", text: "Hello" },
}
emitter.on("agent_message_chunk", listener1)
emitter.on("agent_message_chunk", listener2)
emitter.removeAllListeners("agent_message_chunk")
emitter.emit("agent_message_chunk", payload)
expect(listener1).not.toHaveBeenCalled()
expect(listener2).not.toHaveBeenCalled()
})
it("should remove all listeners when no event is specified", () => {
const listener1 = vi.fn()
const listener2 = vi.fn()
emitter.on("agent_message_chunk", listener1)
emitter.on("tool_call", listener2)
emitter.removeAllListeners()
emitter.emit("agent_message_chunk", { content: { type: "text", text: "Hello" } })
emitter.emit("tool_call", { toolCallId: "test", title: "Test" })
expect(listener1).not.toHaveBeenCalled()
expect(listener2).not.toHaveBeenCalled()
})
})
describe("listenerCount", () => {
it("should return the correct number of listeners", () => {
const listener1 = vi.fn()
const listener2 = vi.fn()
expect(emitter.listenerCount("agent_message_chunk")).toBe(0)
emitter.on("agent_message_chunk", listener1)
expect(emitter.listenerCount("agent_message_chunk")).toBe(1)
emitter.on("agent_message_chunk", listener2)
expect(emitter.listenerCount("agent_message_chunk")).toBe(2)
emitter.off("agent_message_chunk", listener1)
expect(emitter.listenerCount("agent_message_chunk")).toBe(1)
})
})
describe("chaining", () => {
it("should support method chaining", () => {
const listener = vi.fn()
const result = emitter.on("agent_message_chunk", listener).on("error", vi.fn()).off("error", vi.fn())
expect(result).toBe(emitter)
})
})
describe("emit return value", () => {
it("should return true when there are listeners", () => {
emitter.on("agent_message_chunk", vi.fn())
const result = emitter.emit("agent_message_chunk", { content: { type: "text", text: "Hello" } })
expect(result).toBe(true)
})
it("should return false when there are no listeners", () => {
const result = emitter.emit("agent_message_chunk", { content: { type: "text", text: "Hello" } })
expect(result).toBe(false)
})
})
})
-114
View File
@@ -1,114 +0,0 @@
/**
* Typed EventEmitter for per-session ACP events.
*
* This class provides a type-safe wrapper around Node's EventEmitter
* for emitting and subscribing to session-specific ACP events.
*
* @module acp
*/
import { EventEmitter } from "events"
import type { ClineSessionEvents } from "./public-types.js"
/**
* Type-safe EventEmitter for ClineAgent session events.
*
* Each session has its own emitter instance, allowing consumers to
* subscribe to events for specific sessions without filtering.
*
* @example
* ```typescript
* const agent = new ClineAgent({ version: "1.0.0" })
* const session = await agent.newSession({ cwd: "/path/to/project" })
*
* // Subscribe to session events
* agent.session(session.sessionId).on("agent_message_chunk", (content) => {
* console.log("Agent says:", content.text)
* })
*
* agent.session(session.sessionId).on("tool_call", (toolCall) => {
* console.log("Tool called:", toolCall.toolName)
* })
* ```
*/
export class ClineSessionEmitter {
private readonly emitter: EventEmitter
constructor() {
this.emitter = new EventEmitter()
// Increase max listeners since we may have many event types
this.emitter.setMaxListeners(20)
}
/**
* Subscribe to a session event.
*
* @param event - The event name to subscribe to
* @param listener - The callback function to invoke when the event is emitted
* @returns This emitter instance for chaining
*/
on<K extends keyof ClineSessionEvents>(event: K, listener: ClineSessionEvents[K]): this {
this.emitter.on(event, listener as (...args: unknown[]) => void)
return this
}
/**
* Subscribe to a session event for a single invocation.
*
* @param event - The event name to subscribe to
* @param listener - The callback function to invoke when the event is emitted
* @returns This emitter instance for chaining
*/
once<K extends keyof ClineSessionEvents>(event: K, listener: ClineSessionEvents[K]): this {
this.emitter.once(event, listener as (...args: unknown[]) => void)
return this
}
/**
* Unsubscribe from a session event.
*
* @param event - The event name to unsubscribe from
* @param listener - The callback function to remove
* @returns This emitter instance for chaining
*/
off<K extends keyof ClineSessionEvents>(event: K, listener: ClineSessionEvents[K]): this {
this.emitter.off(event, listener as (...args: unknown[]) => void)
return this
}
/**
* Emit a session event.
*
* @param event - The event name to emit
* @param args - The arguments to pass to the event listeners
* @returns True if the event had listeners, false otherwise
*/
emit<K extends keyof ClineSessionEvents>(event: K, ...args: Parameters<ClineSessionEvents[K]>): boolean {
return this.emitter.emit(event, ...args)
}
/**
* Remove all listeners for a specific event or all events.
*
* @param event - Optional event name to remove listeners for
* @returns This emitter instance for chaining
*/
removeAllListeners<K extends keyof ClineSessionEvents>(event?: K): this {
if (event) {
this.emitter.removeAllListeners(event)
} else {
this.emitter.removeAllListeners()
}
return this
}
/**
* Get the number of listeners for a specific event.
*
* @param event - The event name to count listeners for
* @returns The number of listeners
*/
listenerCount<K extends keyof ClineSessionEvents>(event: K): number {
return this.emitter.listenerCount(event)
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-356
View File
@@ -1,356 +0,0 @@
/**
* Permission handling for ACP integration.
*
* This module handles the translation between ACP permission requests/responses
* and Cline's internal permission system. It maps ClineAsk types to appropriate
* ACP permission options and translates user responses back to Cline's format.
*
* @module acp/permissionHandler
*/
import type * as acp from "@agentclientprotocol/sdk"
import type { ClineAsk } from "@shared/ExtensionMessage"
import type { ClineAskResponse } from "@shared/WebviewMessage"
import { Logger } from "@/shared/services/Logger.js"
import type { AcpSessionState, ClinePermissionOption } from "./types.js"
/**
* Standard permission options for operations that support "always allow".
* Used for commands, tools, and MCP server operations.
*/
const STANDARD_PERMISSION_OPTIONS: ClinePermissionOption[] = [
{ kind: "allow_once", optionId: "allow_once", name: "Allow Once" },
{ kind: "allow_always", optionId: "allow_always", name: "Always Allow" },
{ kind: "reject_once", optionId: "reject_once", name: "Reject" },
]
/**
* Permission options for operations that don't support "always allow".
* Used for browser actions and other one-time operations.
*/
const RESTRICTED_PERMISSION_OPTIONS: ClinePermissionOption[] = [
{ kind: "allow_once", optionId: "allow_once", name: "Allow Once" },
{ kind: "reject_once", optionId: "reject_once", name: "Reject" },
]
/**
* Mapping of ClineAsk types to their permission option sets.
*/
const ASK_TYPE_PERMISSION_MAP: Partial<Record<ClineAsk, ClinePermissionOption[]>> = {
// Commands support "always allow" for auto-approval
command: STANDARD_PERMISSION_OPTIONS,
// Tool operations support "always allow"
tool: STANDARD_PERMISSION_OPTIONS,
// MCP server operations support "always allow"
use_mcp_server: STANDARD_PERMISSION_OPTIONS,
// Browser actions are one-time, no "always allow"
browser_action_launch: RESTRICTED_PERMISSION_OPTIONS,
// Command output continuation - simple allow/reject
command_output: RESTRICTED_PERMISSION_OPTIONS,
}
/**
* ClineAsk types that require permission handling.
* Other ask types (like followup, plan_mode_respond) don't need permission UI.
*/
const PERMISSION_REQUIRING_ASK_TYPES: Set<ClineAsk> = new Set([
"command",
"tool",
"browser_action_launch",
"use_mcp_server",
"command_output",
])
/**
* Result of handling a permission response.
*/
export interface PermissionHandlerResult {
/** Cline's internal response type */
response: ClineAskResponse
/** Optional text to pass with the response */
text?: string
/** Whether "always allow" was selected (for auto-approval tracking) */
alwaysAllow?: boolean
/** Whether the request was cancelled */
cancelled?: boolean
}
/**
* Check if a ClineAsk type requires permission handling.
*
* @param askType - The ClineAsk type to check
* @returns True if the ask type requires permission UI
*/
export function requiresPermission(askType: ClineAsk): boolean {
return PERMISSION_REQUIRING_ASK_TYPES.has(askType)
}
/**
* Get the appropriate permission options for a ClineAsk type.
*
* @param askType - The ClineAsk type
* @returns Array of permission options, or undefined if the ask type doesn't require permission
*/
export function getPermissionOptionsForAskType(askType: ClineAsk): acp.PermissionOption[] | undefined {
const options = ASK_TYPE_PERMISSION_MAP[askType]
if (!options) {
return undefined
}
// Convert to ACP PermissionOption format
return options.map((opt) => ({
kind: opt.kind,
optionId: opt.optionId,
name: opt.name,
}))
}
/**
* Handle an ACP permission response and translate it to Cline's format.
*
* @param response - The ACP permission response from the client
* @param askType - The original ClineAsk type that triggered the permission request
* @returns The translated result for Cline's handleWebviewAskResponse
*/
export function handlePermissionResponse(response: acp.RequestPermissionResponse, askType: ClineAsk): PermissionHandlerResult {
// Check if cancelled
if (response.outcome.outcome === "cancelled") {
return {
response: "noButtonClicked",
cancelled: true,
}
}
// Get the selected option ID
const optionId = response.outcome.optionId
// Translate the option to Cline's response format
switch (optionId) {
case "allow_once":
return {
response: "yesButtonClicked",
alwaysAllow: false,
}
case "allow_always":
return {
response: "yesButtonClicked",
alwaysAllow: true,
}
case "reject_once":
case "reject_always":
return {
response: "noButtonClicked",
alwaysAllow: false,
}
default:
// Unknown option ID - treat as rejection for safety
Logger.error(`[permissionHandler] Unknown permission option: ${optionId}`)
return {
response: "noButtonClicked",
}
}
}
/**
* Create a permission request for an ACP tool call.
*
* @param toolCall - The ACP tool call that needs permission
* @param askType - The Cline ask type
* @returns The permission request options, or null if no permission needed
*/
export function createPermissionRequest(
toolCall: acp.ToolCall,
askType: ClineAsk,
): { toolCall: acp.ToolCall; options: acp.PermissionOption[] } | null {
const options = getPermissionOptionsForAskType(askType)
if (!options) {
return null
}
return {
toolCall,
options,
}
}
/**
* Track "always allow" decisions for auto-approval.
* This maintains a set of tool/command patterns that have been auto-approved.
*/
export class AutoApprovalTracker {
/** Set of auto-approved command prefixes */
private autoApprovedCommands: Set<string> = new Set()
/** Set of auto-approved tool names */
private autoApprovedTools: Set<string> = new Set()
/** Set of auto-approved MCP servers */
private autoApprovedMcpServers: Set<string> = new Set()
/**
* Record an "always allow" decision for a permission request.
*
* @param askType - The Cline ask type that was auto-approved
* @param identifier - The identifier for the operation (command, tool name, etc.)
*/
recordAlwaysAllow(askType: ClineAsk, identifier: string): void {
switch (askType) {
case "command":
// Store the first word of the command as the key
const commandPrefix = identifier.split(" ")[0]
this.autoApprovedCommands.add(commandPrefix)
break
case "tool":
this.autoApprovedTools.add(identifier)
break
case "use_mcp_server":
this.autoApprovedMcpServers.add(identifier)
break
}
}
/**
* Check if an operation has been auto-approved.
*
* @param askType - The Cline ask type
* @param identifier - The identifier for the operation
* @returns True if the operation was previously auto-approved
*/
isAutoApproved(askType: ClineAsk, identifier: string): boolean {
switch (askType) {
case "command":
const commandPrefix = identifier.split(" ")[0]
return this.autoApprovedCommands.has(commandPrefix)
case "tool":
return this.autoApprovedTools.has(identifier)
case "use_mcp_server":
return this.autoApprovedMcpServers.has(identifier)
default:
return false
}
}
/**
* Clear all auto-approval records.
*/
clear(): void {
this.autoApprovedCommands.clear()
this.autoApprovedTools.clear()
this.autoApprovedMcpServers.clear()
}
}
/**
* Process a pending permission request for a session.
*
* This function coordinates the permission flow:
* 1. Checks if the operation is already auto-approved
* 2. If not, requests permission from the ACP client
* 3. Tracks "always allow" decisions
* 4. Returns the translated result for Cline
*
* @param requestPermission - Function to request permission from the ACP client
* @param sessionId - The session ID
* @param toolCall - The tool call requiring permission
* @param askType - The Cline ask type
* @param identifier - Identifier for auto-approval tracking
* @param autoApprovalTracker - The auto-approval tracker
* @returns The permission handler result
*/
export async function processPermissionRequest(
requestPermission: (
sessionId: string,
toolCall: acp.ToolCall,
options: acp.PermissionOption[],
) => Promise<acp.RequestPermissionResponse>,
sessionId: string,
toolCall: acp.ToolCall,
askType: ClineAsk,
identifier: string,
autoApprovalTracker?: AutoApprovalTracker,
): Promise<PermissionHandlerResult> {
// Check if already auto-approved
if (autoApprovalTracker?.isAutoApproved(askType, identifier)) {
return {
response: "yesButtonClicked",
alwaysAllow: true,
}
}
// Get permission options for this ask type
const options = getPermissionOptionsForAskType(askType)
if (!options) {
// No permission options defined - allow by default
return {
response: "yesButtonClicked",
}
}
// Request permission from the ACP client
const response = await requestPermission(sessionId, toolCall, options)
// Handle the response
const result = handlePermissionResponse(response, askType)
// Track "always allow" decisions
if (result.alwaysAllow && autoApprovalTracker) {
autoApprovalTracker.recordAlwaysAllow(askType, identifier)
}
return result
}
/**
* Get the identifier for auto-approval tracking from a tool call.
*
* @param toolCall - The ACP tool call
* @param askType - The Cline ask type
* @returns The identifier string for auto-approval tracking
*/
export function getAutoApprovalIdentifier(toolCall: acp.ToolCall, askType: ClineAsk): string {
const rawInput = toolCall.rawInput as Record<string, unknown> | undefined
switch (askType) {
case "command":
return (rawInput?.command as string) || toolCall.title
case "tool":
// Try to get tool name from raw input or title
return (rawInput?.tool as string) || toolCall.title
case "use_mcp_server":
return (rawInput?.serverName as string) || toolCall.title
default:
return toolCall.toolCallId
}
}
/**
* Update the session state's pending tool call after permission is handled.
*
* @param sessionState - The session state to update
* @param toolCallId - The tool call ID that was handled
* @param approved - Whether the permission was approved
*/
export function updateSessionStateAfterPermission(sessionState: AcpSessionState, toolCallId: string, approved: boolean): void {
// Remove from pending tool calls
sessionState.pendingToolCalls.delete(toolCallId)
// Clear current tool call ID if it matches
if (sessionState.currentToolCallId === toolCallId && !approved) {
sessionState.currentToolCallId = undefined
}
}
-258
View File
@@ -1,258 +0,0 @@
/**
* Public types for the Cline library API.
*
* This file contains types that are safe to export to library consumers.
* It must NOT import any internal types (Controller, StateManager, etc.)
* to keep the generated declaration files clean.
*
* Internal-only extensions of these types live in ./types.ts.
*/
import type * as acp from "@agentclientprotocol/sdk"
// ============================================================
// Session Update Type Utilities
// ============================================================
/**
* Different types of updates that can be sent during session processing.
*
* These updates provide real-time feedback about the agent's progress.
*
* See protocol docs: [Agent Reports Output](https://agentclientprotocol.com/protocol/prompt-turn#3-agent-reports-output)
*/
export type SessionUpdateType = acp.SessionUpdate["sessionUpdate"]
/**
* Different types of update payloads that can be sent during session processing.
*
* Each update type has a corresponding payload structure defined in the ACP SessionUpdate union.
*/
export type SessionUpdatePayload<T extends SessionUpdateType> = Omit<
Extract<acp.SessionUpdate, { sessionUpdate: T }>,
"sessionUpdate"
>
// ============================================================
// Permission Handler Callback Types
// ============================================================
/**
* Handler function for permission requests.
* Called when the agent needs permission for a tool call.
* The handler should present the request to the user and call resolve() with their response.
*/
export type PermissionHandler = (request: acp.RequestPermissionRequest) => Promise<acp.RequestPermissionResponse>
// ============================================================
// Session Event Emitter Types
// ============================================================
/**
* Maps ACP SessionUpdate types to their event listener signatures.
* Uses the sessionUpdate discriminator to derive event names and payload types.
*/
export type ClineSessionEvents = {
[K in SessionUpdateType]: (payload: SessionUpdatePayload<K>) => void
} & {
/** Error event for session-level errors (not part of ACP SessionUpdate) */
error: (error: Error) => void
}
// ============================================================
// ClineAgent Options
// ============================================================
/**
* Options for creating a ClineAgent instance.
*/
export interface ClineAgentOptions {
/** Whether debug logging is enabled */
debug?: boolean
/** Cline Config Directory (defaults to ~/.cline) */
clineDir?: string
/** Additional runtime hooks directory */
hooksDir?: string
}
/**
* Options for creating an ACP agent instance.
*/
export interface AcpAgentOptions {
/** Whether debug logging is enabled */
debug?: boolean
/** Additional runtime hooks directory */
hooksDir?: string
}
// ============================================================
// Session Types
// ============================================================
export type SessionID = string
/**
* Extended session data stored by Cline for ACP sessions.
*/
export interface ClineAcpSession {
/** Unique session ID */
sessionId: SessionID
/** Working directory for the session */
cwd: string
/** Current mode (plan/act) */
mode: "plan" | "act"
/** MCP servers passed from the client */
mcpServers: acp.McpServer[]
/** Timestamp when session was created */
createdAt: number
/** Timestamp of last activity */
lastActivityAt: number
/** Whether this session was loaded from history (needs resume on first prompt) */
isLoadedFromHistory?: boolean
/** Model ID override for plan mode (format: "provider/modelId") */
planModeModelId?: string
/** Model ID override for act mode (format: "provider/modelId") */
actModeModelId?: string
}
/**
* Lifecycle status of an ACP session.
*
* Represents the state machine:
* Idle → Processing → Idle (normal completion)
* Idle → Processing → Cancelled (cancellation, then back to Idle on next prompt)
*/
export enum AcpSessionStatus {
/** Session is idle, waiting for a prompt */
Idle = "idle",
/** Session is actively processing a prompt */
Processing = "processing",
/** Session processing was cancelled */
Cancelled = "cancelled",
}
/**
* State tracking for an active ACP session within Cline.
*/
export interface AcpSessionState {
/** Session ID */
sessionId: SessionID
/** Current lifecycle status of the session */
status: AcpSessionStatus
/** Current tool call ID being executed (if any) */
currentToolCallId?: string
/** Accumulated tool calls for permission batching */
pendingToolCalls: Map<string, acp.ToolCall>
}
// ============================================================
// Agent Capabilities
// ============================================================
/**
* Cline-specific agent capabilities extending the ACP base capabilities.
*/
export interface ClineAgentCapabilities {
/** Support for loading sessions from disk */
loadSession: boolean
/** Prompt capabilities for the agent */
promptCapabilities: {
/** Support for image inputs */
image: boolean
/** Support for audio inputs */
audio: boolean
/** Support for embedded context (file resources) */
embeddedContext: boolean
}
/** MCP server passthrough capabilities */
mcpCapabilities: {
/** Support for HTTP MCP servers */
http: boolean
/** Support for SSE MCP servers */
sse: boolean
}
}
/**
* Cline agent info for ACP initialization response.
*/
export interface ClineAgentInfo {
name: "cline"
title: "Cline"
version: string
}
// ============================================================
// Permission Options
// ============================================================
/**
* Permission option as presented to the ACP client.
*/
export interface ClinePermissionOption {
kind: acp.PermissionOptionKind
name: string
optionId: string
}
// ============================================================
// Message Translation
// ============================================================
/**
* Result of translating a Cline message to ACP session update(s).
* A single Cline message may produce multiple ACP updates.
*/
export interface TranslatedMessage {
/** The session updates to send */
updates: acp.SessionUpdate[]
/** Whether this message requires a permission request */
requiresPermission?: boolean
/** Permission request details if required */
permissionRequest?: Omit<acp.RequestPermissionRequest, "sessionId">
/** The toolCallId that was created/used (for tracking across streaming updates) */
toolCallId?: string
}
// ============================================================
// Re-exported ACP Types
// ============================================================
export type {
Agent,
AgentSideConnection,
AudioContent,
CancelNotification,
ClientCapabilities,
ContentBlock,
ImageContent,
InitializeRequest,
InitializeResponse,
LoadSessionRequest,
LoadSessionResponse,
McpServer,
ModelInfo,
NewSessionRequest,
NewSessionResponse,
PermissionOption,
PermissionOptionKind,
PromptRequest,
PromptResponse,
RequestPermissionRequest,
RequestPermissionResponse,
SessionConfigOption,
SessionModelState,
SessionNotification,
SessionUpdate,
SetSessionConfigOptionRequest,
SetSessionConfigOptionResponse,
SetSessionModelRequest,
SetSessionModelResponse,
SetSessionModeRequest,
SetSessionModeResponse,
StopReason,
TextContent,
ToolCall,
ToolCallStatus,
ToolCallUpdate,
ToolKind,
} from "@agentclientprotocol/sdk"
-68
View File
@@ -1,68 +0,0 @@
/**
* Internal types for ACP integration with Cline CLI.
*
* This file re-exports all public types from ./public-types.ts and adds
* internal-only Types that reference core modules (Controller, etc.).
*
* Library consumers should never import from this file directly — they
* get the public types via the library entrypoint (exports.ts).
*/
export type {
Agent,
AgentSideConnection,
AudioContent,
CancelNotification,
ContentBlock,
ImageContent,
InitializeRequest,
InitializeResponse,
LoadSessionRequest,
LoadSessionResponse,
McpServer,
ModelInfo,
NewSessionRequest,
NewSessionResponse,
PermissionOption,
PermissionOptionKind,
PromptRequest,
PromptResponse,
ReadTextFileRequest,
ReadTextFileResponse,
RequestPermissionRequest,
RequestPermissionResponse,
SessionConfigOption,
SessionModelState,
SessionNotification,
SessionUpdate,
SetSessionConfigOptionRequest,
SetSessionConfigOptionResponse,
SetSessionModelRequest,
SetSessionModelResponse,
SetSessionModeRequest,
SetSessionModeResponse,
StopReason,
TextContent,
ToolCall,
ToolCallStatus,
ToolCallUpdate,
ToolKind,
WriteTextFileRequest,
WriteTextFileResponse,
} from "@agentclientprotocol/sdk"
export type {
AcpAgentOptions,
AcpSessionState,
ClineAgentCapabilities,
ClineAgentInfo,
ClineAgentOptions,
ClinePermissionOption,
ClineSessionEvents,
PermissionHandler,
SessionUpdatePayload,
SessionUpdateType,
TranslatedMessage,
} from "./public-types.js"
export { AcpSessionStatus } from "./public-types.js"
-194
View File
@@ -1,194 +0,0 @@
/**
* Account info view component
* Shows current provider, and for Cline provider: credit balance and organization name
*/
import { Box, Text } from "ink"
import React, { useCallback, useEffect, useState } from "react"
import { Controller } from "@/core/controller"
import { StateManager } from "@/core/storage/StateManager"
import { ClineAccountService } from "@/services/account/ClineAccountService"
import { AuthService, ClineAccountOrganization } from "@/services/auth/AuthService"
import { LoadingSpinner } from "./Spinner"
interface AccountInfoViewProps {
controller: Controller
}
/**
* Capitalize provider name for display
*/
function capitalize(str: string): string {
return str
.split("-")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ")
}
/**
* Format balance as currency (balance is in microcredits, divide by 10000)
*/
function formatBalance(balance: number | null): string {
if (balance === null || balance === undefined) {
return "..."
}
return `$${(balance / 1000000).toFixed(2)}`
}
export const AccountInfoView: React.FC<AccountInfoViewProps> = React.memo(({ controller }) => {
const [provider, setProvider] = useState<string | null>(null)
const [balance, setBalance] = useState<number | null>(null)
const [organization, setOrganization] = useState<ClineAccountOrganization | null>(null)
const [email, setEmail] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const fetchAccountInfo = useCallback(async () => {
try {
setIsLoading(true)
setError(null)
// Get current provider from state
const stateManager = StateManager.get()
const mode = stateManager.getGlobalSettingsKey("mode") as string
const providerKey = mode === "act" ? "actModeApiProvider" : "planModeApiProvider"
const currentProvider = stateManager.getGlobalSettingsKey(providerKey) as string
setProvider(currentProvider || "cline")
// If using Cline provider, fetch additional info
if (currentProvider === "cline") {
const authService = AuthService.getInstance(controller)
// Wait for auth to be restored - poll until we have auth info or timeout
let authInfo = authService.getInfo()
let attempts = 0
const maxAttempts = 20 // 2 seconds max
while (!authInfo?.user?.uid && attempts < maxAttempts) {
await new Promise((resolve) => setTimeout(resolve, 100))
authInfo = authService.getInfo()
attempts++
}
// Get user info
if (authInfo?.user?.email) {
setEmail(authInfo.user.email)
} else {
// User not logged in to Cline
setEmail(null)
setIsLoading(false)
return
}
// Get organization info
const organizations = authService.getUserOrganizations()
if (organizations) {
const activeOrg = organizations.find((org) => org.active)
if (activeOrg) {
setOrganization(activeOrg)
}
}
// Fetch credit balance
try {
const accountService = ClineAccountService.getInstance()
const activeOrgId = authService.getActiveOrganizationId()
if (activeOrgId) {
// Fetch organization balance
const orgBalance = await accountService.fetchOrganizationCreditsRPC(activeOrgId)
if (orgBalance?.balance !== undefined) {
setBalance(orgBalance.balance)
}
} else {
// Fetch personal balance
const balanceData = await accountService.fetchBalanceRPC()
if (balanceData?.balance !== undefined) {
setBalance(balanceData.balance)
}
}
} catch {
// Balance fetch failed, but we can still show other info
// Don't log to console as it pollutes CLI output
}
}
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to load account info")
} finally {
setIsLoading(false)
}
}, [controller])
useEffect(() => {
fetchAccountInfo()
}, [fetchAccountInfo])
if (isLoading) {
return (
<Box>
<LoadingSpinner />
<Text color="gray"> Loading account info...</Text>
</Box>
)
}
if (error) {
return (
<Box>
<Text color="red">Error: {error}</Text>
</Box>
)
}
// If not using Cline provider, just show the provider name
if (provider !== "cline") {
return (
<Box>
<Text color="gray">Provider: </Text>
<Text color="cyan">{capitalize(provider || "Not configured")}</Text>
</Box>
)
}
// Cline provider but not logged in
if (!email) {
return (
<Box>
<Text color="gray">Provider: </Text>
<Text color="cyan">Cline</Text>
<Text color="gray"> </Text>
<Text color="yellow">Not logged in (run 'cline auth' to sign in)</Text>
</Box>
)
}
// Cline provider - show full account info
return (
<Box flexDirection="column">
<Box>
<Text color="gray">Provider: </Text>
<Text color="cyan">Cline</Text>
{email && (
<Box>
<Text color="gray"> </Text>
<Text color="white">{email}</Text>
</Box>
)}
</Box>
<Box>
{organization ? (
<Box>
<Text color="gray">Organization: </Text>
<Text color="magenta">{organization.name}</Text>
</Box>
) : (
<Box>
<Text color="gray">Account: </Text>
<Text color="white">Personal</Text>
</Box>
)}
<Text color="gray"> Credits: </Text>
<Text color="green">{formatBalance(balance)}</Text>
</Box>
</Box>
)
})
-337
View File
@@ -1,337 +0,0 @@
/**
* Action buttons component for CLI
* Shows primary/secondary buttons above the input field
* Supports keyboard navigation (1/2 for buttons, arrows to navigate, esc to cancel)
*/
import type { ClineMessage } from "@shared/ExtensionMessage"
import { Box, Text } from "ink"
import React from "react"
import { COLORS } from "../constants/colors"
import { useTerminalSize } from "../hooks/useTerminalSize"
import { isFileSaveTool, parseToolFromMessage } from "../utils/tools"
/**
* Button action types that determine the behavior
*/
export type ButtonActionType =
| "approve" // Send yesButtonClicked
| "reject" // Send noButtonClicked
| "proceed" // Send messageResponse or yesButtonClicked
| "new_task" // Start a new task
| "cancel" // Cancel streaming
| "retry" // Retry the last action
/**
* Button configuration for different message states
*/
export interface ButtonConfig {
sendingDisabled: boolean
enableButtons: boolean
primaryText?: string
secondaryText?: string
primaryAction?: ButtonActionType
secondaryAction?: ButtonActionType
}
/**
* Centralized button state configurations based on task lifecycle
*/
const BUTTON_CONFIGS: Record<string, ButtonConfig> = {
// Error recovery states
api_req_failed: {
sendingDisabled: true,
enableButtons: true,
primaryText: "Retry",
secondaryText: "Start New Task",
primaryAction: "retry",
secondaryAction: "new_task",
},
mistake_limit_reached: {
sendingDisabled: false,
enableButtons: true,
primaryText: "Proceed Anyways",
secondaryText: "Start New Task",
primaryAction: "proceed",
secondaryAction: "new_task",
},
// Tool approval states
tool_approve: {
sendingDisabled: false,
enableButtons: true,
primaryText: "Approve",
secondaryText: "Reject",
primaryAction: "approve",
secondaryAction: "reject",
},
tool_save: {
sendingDisabled: false,
enableButtons: true,
primaryText: "Save",
secondaryText: "Reject",
primaryAction: "approve",
secondaryAction: "reject",
},
// Command execution states
command: {
sendingDisabled: false,
enableButtons: true,
primaryText: "Run Command",
secondaryText: "Reject",
primaryAction: "approve",
secondaryAction: "reject",
},
command_output: {
sendingDisabled: false,
enableButtons: true,
primaryText: "Proceed While Running",
secondaryText: undefined,
primaryAction: "proceed",
secondaryAction: undefined,
},
// Browser and external tool states
browser_action_launch: {
sendingDisabled: false,
enableButtons: true,
primaryText: "Approve",
secondaryText: "Reject",
primaryAction: "approve",
secondaryAction: "reject",
},
use_mcp_server: {
sendingDisabled: false,
enableButtons: true,
primaryText: "Approve",
secondaryText: "Reject",
primaryAction: "approve",
secondaryAction: "reject",
},
followup: {
sendingDisabled: false,
enableButtons: false,
primaryText: undefined,
secondaryText: undefined,
primaryAction: undefined,
secondaryAction: undefined,
},
plan_mode_respond: {
sendingDisabled: false,
enableButtons: false,
primaryText: undefined,
secondaryText: undefined,
primaryAction: undefined,
secondaryAction: undefined,
},
// Task lifecycle states
completion_result: {
sendingDisabled: false,
enableButtons: true,
primaryText: "Start New Task",
secondaryText: "Exit",
primaryAction: "new_task",
secondaryAction: "reject",
},
resume_task: {
sendingDisabled: false,
enableButtons: true,
primaryText: "Resume Task",
secondaryText: "Exit",
primaryAction: "proceed",
secondaryAction: "reject",
},
resume_completed_task: {
sendingDisabled: false,
enableButtons: true,
primaryText: "Start New Task",
secondaryText: "Exit",
primaryAction: "new_task",
secondaryAction: "reject",
},
new_task: {
sendingDisabled: false,
enableButtons: true,
primaryText: "Start New Task with Context",
secondaryText: "Exit",
primaryAction: "new_task",
secondaryAction: "reject",
},
// Streaming/partial states
partial: {
sendingDisabled: true,
enableButtons: true,
primaryText: undefined,
secondaryText: "Cancel",
primaryAction: undefined,
secondaryAction: "cancel",
},
// Default states
default: {
sendingDisabled: false,
enableButtons: false,
primaryText: undefined,
secondaryText: undefined,
primaryAction: undefined,
secondaryAction: undefined,
},
api_req_active: {
sendingDisabled: true,
enableButtons: true,
primaryText: undefined,
secondaryText: "Cancel",
primaryAction: undefined,
secondaryAction: "cancel",
},
}
const errorTypes = ["api_req_failed", "mistake_limit_reached"]
/**
* Get button configuration based on message type and state
*/
export function getButtonConfig(message: ClineMessage | undefined, isStreaming: boolean = false): ButtonConfig {
if (!message) {
return BUTTON_CONFIGS.default
}
const isError = message?.ask ? errorTypes.includes(message.ask) : false
// Special case: command_output should show "Proceed While Running" button even while streaming
if (message.type === "ask" && message.ask === "command_output") {
return BUTTON_CONFIGS.command_output
}
// Handle partial/streaming messages first
if (isStreaming && !isError) {
return BUTTON_CONFIGS.partial
}
// Handle ask messages (user interaction required)
if (message.type === "ask") {
switch (message.ask) {
// Error recovery states
case "api_req_failed":
return BUTTON_CONFIGS.api_req_failed
case "mistake_limit_reached":
return BUTTON_CONFIGS.mistake_limit_reached
// Tool approval (most common)
case "tool": {
const toolInfo = parseToolFromMessage(message.text)
if (toolInfo && isFileSaveTool(toolInfo.toolName)) {
return BUTTON_CONFIGS.tool_save
}
return BUTTON_CONFIGS.tool_approve
}
// Command execution
case "command":
return BUTTON_CONFIGS.command
case "command_output":
return BUTTON_CONFIGS.command_output
// Standard approvals
case "followup":
return BUTTON_CONFIGS.followup
case "browser_action_launch":
return BUTTON_CONFIGS.browser_action_launch
case "use_mcp_server":
return BUTTON_CONFIGS.use_mcp_server
case "plan_mode_respond":
return BUTTON_CONFIGS.plan_mode_respond
// Task lifecycle
case "completion_result":
return BUTTON_CONFIGS.completion_result
case "resume_task":
return BUTTON_CONFIGS.resume_task
case "resume_completed_task":
return BUTTON_CONFIGS.resume_completed_task
case "new_task":
return BUTTON_CONFIGS.new_task
default:
return BUTTON_CONFIGS.tool_approve
}
}
// Handle say messages
if (message.type === "say" && message.say === "api_req_started") {
return BUTTON_CONFIGS.api_req_active
}
if (message.type === "say" && message.say === "command_output") {
return BUTTON_CONFIGS.command_output
}
return BUTTON_CONFIGS.partial
}
interface ActionButtonsProps {
config: ButtonConfig
mode?: "act" | "plan"
}
/**
* Determine which buttons are actually visible based on config
* Cancel is hidden in the CLI (ThinkingIndicator handles that with esc)
*/
export function getVisibleButtons(config: ButtonConfig) {
const hiddenActions = ["cancel"]
const hasPrimary = !!config.primaryText && !hiddenActions.includes(config.primaryAction || "")
const hasSecondary = !!config.secondaryText && !hiddenActions.includes(config.secondaryAction || "")
return { hasPrimary, hasSecondary }
}
/**
* Action buttons component
* Shows primary and/or secondary buttons based on config
* Buttons take full width (one button = full, two buttons = half each)
* Does not show cancel-only buttons (ThinkingIndicator handles that with esc)
*/
export const ActionButtons: React.FC<ActionButtonsProps> = ({ config, mode = "act" }) => {
if (!config.enableButtons) {
return null
}
const { hasPrimary, hasSecondary } = getVisibleButtons(config)
if (!hasPrimary && !hasSecondary) {
return null
}
// Calculate button widths based on terminal width
const { columns: terminalWidth } = useTerminalSize()
const buttonCount = (hasPrimary ? 1 : 0) + (hasSecondary ? 1 : 0)
const gapWidth = buttonCount > 1 ? 1 : 0 // 1 char gap between buttons
const availableWidth = terminalWidth - 2 - gapWidth // 1 space padding on each side
const buttonWidth = Math.floor(availableWidth / buttonCount)
const modeColor = mode === "plan" ? "yellow" : COLORS.primaryBlue
const renderButton = (text: string, shortcut: string) => {
const label = ` ${text} (${shortcut}) `
const padding = Math.max(0, buttonWidth - label.length)
const leftPad = Math.floor(padding / 2)
const rightPad = padding - leftPad
const paddedLabel = " ".repeat(leftPad) + label + " ".repeat(rightPad)
return (
<Text backgroundColor={modeColor} color="black">
{paddedLabel}
</Text>
)
}
return (
<Box flexDirection="row" gap={1} marginLeft={1} width="100%">
{hasPrimary && renderButton(config.primaryText!, "1")}
{hasSecondary && renderButton(config.secondaryText!, hasPrimary ? "2" : "1")}
</Box>
)
}
-74
View File
@@ -1,74 +0,0 @@
/**
* Reusable API key input component
* Shows a password-masked input field for entering API keys
*/
import { Box, Text, useInput } from "ink"
import React from "react"
import { COLORS } from "../constants/colors"
import { useStdinContext } from "../context/StdinContext"
import { isEnterKey, isMouseEscapeSequence } from "../utils/input"
interface ApiKeyInputProps {
providerName: string
value: string
onChange: (value: string) => void
onSubmit: (value: string) => void
onCancel: () => void
isActive?: boolean
}
export const ApiKeyInput: React.FC<ApiKeyInputProps> = ({
providerName,
value,
onChange,
onSubmit,
onCancel,
isActive = true,
}) => {
const { isRawModeSupported } = useStdinContext()
useInput(
(input, key) => {
// Filter out mouse escape sequences
if (isMouseEscapeSequence(input)) {
return
}
if (key.escape) {
onCancel()
return
}
if (isEnterKey(input, key)) {
onSubmit(value)
return
}
if (key.backspace || key.delete) {
onChange(value.slice(0, -1))
return
}
if (input && !key.ctrl && !key.meta) {
onChange(value + input)
}
},
{ isActive: isRawModeSupported && isActive },
)
return (
<Box flexDirection="column">
<Text bold color={COLORS.primaryBlue}>
{providerName} API Key
</Text>
<Box marginTop={1}>
<Text color="gray">Paste your API key below</Text>
</Box>
<Box marginTop={1}>
<Text color="white">{"•".repeat(value.length)}</Text>
<Text inverse> </Text>
</Box>
<Box marginTop={1}>
<Text color="gray">Enter to save, Esc to cancel</Text>
</Box>
</Box>
)
}
@@ -1,136 +0,0 @@
import { Text } from "ink"
import { render } from "ink-testing-library"
import React from "react"
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
import { App } from "./App"
const CLEAR_SEQUENCE = "\x1b[2J\x1b[3J\x1b[H"
function setTerminalSize(columns: number, rows: number) {
Object.defineProperty(process.stdout, "columns", {
configurable: true,
writable: true,
value: columns,
})
Object.defineProperty(process.stdout, "rows", {
configurable: true,
writable: true,
value: rows,
})
}
function hasClearSequenceCall(calls: unknown[][]): boolean {
return calls.some((call) => call[0] === CLEAR_SEQUENCE)
}
vi.mock("./ChatView", () => ({
ChatView: ({ controller, initialPrompt, initialImages }: any) => {
React.useEffect(() => {
if (initialPrompt || (initialImages && initialImages.length > 0)) {
controller?.initTask(initialPrompt || "", initialImages)
}
}, [])
return React.createElement(Text, null, "ChatView")
},
}))
vi.mock("./TaskJsonView", () => ({
TaskJsonView: () => React.createElement(Text, null, "TaskJsonView"),
}))
vi.mock("./HistoryView", () => ({
HistoryView: () => React.createElement(Text, null, "HistoryView"),
}))
vi.mock("./ConfigView", () => ({
ConfigView: () => React.createElement(Text, null, "ConfigView"),
}))
vi.mock("./AuthView", () => ({
AuthView: () => React.createElement(Text, null, "AuthView"),
}))
vi.mock("../context/TaskContext", () => ({
TaskContextProvider: ({ children }: any) => children,
}))
vi.mock("../context/StdinContext", () => ({
StdinProvider: ({ children }: any) => children,
}))
describe("App startup prompt resize behavior", () => {
beforeEach(() => {
vi.useFakeTimers()
})
afterEach(() => {
vi.useRealTimers()
vi.restoreAllMocks()
delete (process.stdout as any).columns
delete (process.stdout as any).rows
})
it("does not replay initialPrompt after a width resize", async () => {
const initTask = vi.fn()
setTerminalSize(120, 40)
const writeSpy = vi.spyOn(process.stdout, "write").mockImplementation(((...args: any[]) => {
const callback = args.find((arg) => typeof arg === "function")
if (callback) {
callback()
}
return true
}) as any)
const { unmount } = render(
<App controller={{ initTask }} initialPrompt="hello" isRawModeSupported={true} view="welcome" />,
)
await vi.advanceTimersByTimeAsync(0)
expect(initTask).toHaveBeenCalledTimes(1)
writeSpy.mockClear()
setTerminalSize(121, 40)
process.stdout.emit("resize")
await vi.advanceTimersByTimeAsync(350)
await vi.advanceTimersByTimeAsync(0)
expect(initTask).toHaveBeenCalledTimes(1)
expect(hasClearSequenceCall(writeSpy.mock.calls as unknown[][])).toBe(true)
unmount()
})
it("does not remount on height-only resize", async () => {
const initTask = vi.fn()
setTerminalSize(120, 40)
const writeSpy = vi.spyOn(process.stdout, "write").mockImplementation(((...args: any[]) => {
const callback = args.find((arg) => typeof arg === "function")
if (callback) {
callback()
}
return true
}) as any)
const { unmount } = render(
<App controller={{ initTask }} initialPrompt="hello" isRawModeSupported={true} view="welcome" />,
)
await vi.advanceTimersByTimeAsync(0)
expect(initTask).toHaveBeenCalledTimes(1)
writeSpy.mockClear()
setTerminalSize(120, 45)
process.stdout.emit("resize")
await vi.advanceTimersByTimeAsync(350)
await vi.advanceTimersByTimeAsync(0)
expect(initTask).toHaveBeenCalledTimes(1)
expect(hasClearSequenceCall(writeSpy.mock.calls as unknown[][])).toBe(false)
unmount()
})
})
-125
View File
@@ -1,125 +0,0 @@
import { Text } from "ink"
import { render } from "ink-testing-library"
import React from "react"
import { beforeEach, describe, expect, it, vi } from "vitest"
import { App } from "./App"
// Mock the child components to isolate App routing logic
vi.mock("./ChatView", () => ({
ChatView: ({ taskId, controller }: any) =>
React.createElement(Text, null, `ChatView: ${taskId || "no-id"} controller=${controller ? "present" : "none"}`),
}))
vi.mock("./TaskJsonView", () => ({
TaskJsonView: ({ taskId, verbose }: any) =>
React.createElement(Text, null, `TaskJsonView: ${taskId || "no-id"} verbose=${String(verbose)}`),
}))
vi.mock("./HistoryView", () => ({
HistoryView: ({ items }: any) => React.createElement(Text, null, `HistoryView: ${items?.length || 0} items`),
}))
vi.mock("./ConfigView", () => ({
ConfigView: ({ dataDir }: any) => React.createElement(Text, null, `ConfigView: ${dataDir}`),
}))
vi.mock("./AuthView", () => ({
AuthView: ({ quickSetup }: any) => React.createElement(Text, null, `AuthView: ${quickSetup?.provider || "no-provider"}`),
}))
vi.mock("../context/TaskContext", () => ({
TaskContextProvider: ({ children }: any) => children,
}))
vi.mock("../context/StdinContext", () => ({
StdinProvider: ({ children }: any) => children,
}))
// Mock useTerminalSize to prevent EventEmitter memory leak warnings from resize listeners
vi.mock("../hooks/useTerminalSize", () => ({
useTerminalSize: () => ({ columns: 80, rows: 24, resizeKey: 0 }),
}))
describe("App", () => {
const mockController = {
dispose: vi.fn(),
stateManager: { flushPendingState: vi.fn() },
}
beforeEach(() => {
vi.clearAllMocks()
})
describe("view routing", () => {
it("should render ChatView when view is task", () => {
const { lastFrame } = render(<App controller={mockController} taskId="test-task" view="task" />)
expect(lastFrame()).toContain("ChatView")
expect(lastFrame()).toContain("test-task")
})
it("should render TaskJsonView when view is task with jsonOutput", () => {
const { lastFrame } = render(<App controller={mockController} jsonOutput={true} taskId="test-task" view="task" />)
expect(lastFrame()).toContain("TaskJsonView")
expect(lastFrame()).toContain("test-task")
})
it("should render HistoryView when view is history", () => {
const historyItems = [
{ id: "1", ts: Date.now(), task: "Task 1" },
{ id: "2", ts: Date.now(), task: "Task 2" },
]
const { lastFrame } = render(<App controller={mockController} historyItems={historyItems} view="history" />)
expect(lastFrame()).toContain("HistoryView")
expect(lastFrame()).toContain("2 items")
})
it("should render ConfigView when view is config", () => {
const { lastFrame } = render(
<App dataDir="/path/to/config" globalState={{ key: "value" }} view="config" workspaceState={{}} />,
)
expect(lastFrame()).toContain("ConfigView")
expect(lastFrame()).toContain("/path/to/config")
})
it("should render AuthView when view is auth", () => {
const { lastFrame } = render(<App controller={mockController} view="auth" />)
expect(lastFrame()).toContain("AuthView")
})
it("should render ChatView when view is welcome", () => {
const { lastFrame } = render(
<App controller={mockController} onWelcomeExit={() => {}} onWelcomeSubmit={() => {}} view="welcome" />,
)
expect(lastFrame()).toContain("ChatView")
})
})
describe("default props", () => {
it("should use default verbose=false with jsonOutput", () => {
const { lastFrame } = render(<App controller={mockController} jsonOutput={true} view="task" />)
expect(lastFrame()).toContain("verbose=false")
})
it("should use empty array for historyItems by default", () => {
const { lastFrame } = render(<App controller={mockController} view="history" />)
expect(lastFrame()).toContain("0 items")
})
})
describe("props passing", () => {
it("should pass verbose to TaskJsonView", () => {
const { lastFrame } = render(<App controller={mockController} jsonOutput={true} verbose={true} view="task" />)
expect(lastFrame()).toContain("verbose=true")
})
it("should pass taskId to ChatView", () => {
const { lastFrame } = render(<App controller={mockController} taskId="my-task-123" view="task" />)
expect(lastFrame()).toContain("my-task-123")
})
it("should pass controller to ChatView", () => {
const { lastFrame } = render(<App controller={mockController} view="task" />)
expect(lastFrame()).toContain("controller=present")
})
})
})
-288
View File
@@ -1,288 +0,0 @@
/**
* Main App component for Ink CLI
* Routes between different views (task, history, config)
*/
import { Box, useApp } from "ink"
import React, { ReactNode, useCallback, useEffect, useState } from "react"
import { StdinProvider } from "../context/StdinContext"
import { TaskContextProvider } from "../context/TaskContext"
import { useTerminalSize } from "../hooks/useTerminalSize"
import { AuthView } from "./AuthView"
import { ChatView } from "./ChatView"
import { ConfigView } from "./ConfigView"
import { ErrorBoundary } from "./ErrorBoundary"
import { HistoryView } from "./HistoryView"
import { TaskJsonView } from "./TaskJsonView"
export type ViewType = "task" | "history" | "config" | "auth" | "welcome"
interface HistoryPagination {
page: number
totalPages: number
totalCount: number
limit: number
}
interface HookInfo {
name: string
enabled: boolean
absolutePath: string
}
interface WorkspaceHooks {
workspaceName: string
hooks: HookInfo[]
}
interface SkillInfo {
name: string
description: string
path: string
enabled: boolean
}
interface AppProps {
view: ViewType
taskId?: string
controller?: any
// Output Style
verbose?: boolean
jsonOutput?: boolean
// Status Callbacks
onComplete?: () => void
onError?: () => void
// For history view
historyItems?: Array<{ id: string; ts: number; task?: string; totalCost?: number; modelId?: string }>
historyAllItems?: Array<{ id: string; ts: number; task?: string; totalCost?: number; modelId?: string }>
historyPagination?: HistoryPagination
onHistoryPageChange?: (page: number) => void
// For config view
dataDir?: string
globalState?: Record<string, any>
workspaceState?: Record<string, any>
// Rules toggles
globalClineRulesToggles?: Record<string, boolean>
localClineRulesToggles?: Record<string, boolean>
localCursorRulesToggles?: Record<string, boolean>
localWindsurfRulesToggles?: Record<string, boolean>
localAgentsRulesToggles?: Record<string, boolean>
onToggleRule?: (isGlobal: boolean, rulePath: string, enabled: boolean, ruleType: string) => void
// Workflow toggles
globalWorkflowToggles?: Record<string, boolean>
localWorkflowToggles?: Record<string, boolean>
onToggleWorkflow?: (isGlobal: boolean, workflowPath: string, enabled: boolean) => void
// Hooks
hooksEnabled?: boolean
globalHooks?: HookInfo[]
workspaceHooks?: WorkspaceHooks[]
onToggleHook?: (isGlobal: boolean, hookName: string, enabled: boolean, workspaceName?: string) => void
// Skills
skillsEnabled?: boolean
globalSkills?: SkillInfo[]
localSkills?: SkillInfo[]
onToggleSkill?: (isGlobal: boolean, skillPath: string, enabled: boolean) => void
// For welcome view
onWelcomeSubmit?: (prompt: string, imagePaths: string[]) => void
onWelcomeExit?: () => void
initialPrompt?: string
initialImages?: string[]
// Stdin support
isRawModeSupported?: boolean
}
export const App: React.FC<AppProps> = (props) => {
const { exit } = useApp()
return (
<ErrorBoundary exit={exit}>
<InternalApp {...props} />
</ErrorBoundary>
)
}
const InternalApp: React.FC<AppProps> = ({
view: initialView,
taskId,
verbose = false,
jsonOutput = false,
controller,
onComplete,
onError,
historyItems = [],
historyAllItems,
historyPagination,
onHistoryPageChange,
dataDir = "",
globalState = {},
workspaceState = {},
// Rules
globalClineRulesToggles,
localClineRulesToggles,
localCursorRulesToggles,
localWindsurfRulesToggles,
localAgentsRulesToggles,
onToggleRule,
// Workflows
globalWorkflowToggles,
localWorkflowToggles,
onToggleWorkflow,
// Hooks
hooksEnabled,
globalHooks,
workspaceHooks,
onToggleHook,
// Skills
skillsEnabled,
globalSkills,
localSkills,
onToggleSkill,
onWelcomeSubmit,
onWelcomeExit,
initialPrompt,
initialImages,
isRawModeSupported = true,
}) => {
const { resizeKey } = useTerminalSize()
const [currentView, setCurrentView] = useState<ViewType>(initialView)
const [selectedTaskId, setSelectedTaskId] = useState<string | undefined>(taskId)
const [pendingInitialPrompt, setPendingInitialPrompt] = useState<string | undefined>(initialPrompt)
const [pendingInitialImages, setPendingInitialImages] = useState<string[] | undefined>(initialImages)
useEffect(() => {
if (!pendingInitialPrompt && (!pendingInitialImages || pendingInitialImages.length === 0)) {
return
}
setPendingInitialPrompt(undefined)
setPendingInitialImages(undefined)
}, [pendingInitialPrompt, pendingInitialImages])
const handleSelectTask = useCallback((taskId: string) => {
setSelectedTaskId(taskId)
setCurrentView("task")
}, [])
const handleNavigateToWelcome = useCallback(() => {
setCurrentView("welcome")
}, [])
// Handle welcome submit when navigating internally (e.g., from auth -> welcome)
const _handleInternalWelcomeSubmit = useCallback(
async (prompt: string, imagePaths: string[]) => {
if (onWelcomeSubmit) {
// If external handler provided, use it
onWelcomeSubmit(prompt, imagePaths)
} else if (controller && prompt.trim()) {
// Otherwise, start a task directly via controller
setCurrentView("task")
// Convert image paths to data URLs if needed
const imageDataUrls =
imagePaths.length > 0
? await Promise.all(
imagePaths.map(async (p) => {
try {
const fs = await import("fs/promises")
const path = await import("path")
const data = await fs.readFile(p)
const ext = path.extname(p).toLowerCase().slice(1)
const mimeType = ext === "jpg" ? "jpeg" : ext
return `data:image/${mimeType};base64,${data.toString("base64")}`
} catch {
return null
}
}),
)
: []
const validImages = imageDataUrls.filter((img): img is string => img !== null)
await controller.initTask(prompt.trim(), validImages.length > 0 ? validImages : undefined)
}
},
[onWelcomeSubmit, controller],
)
let content: ReactNode
switch (currentView) {
case "history":
content = (
<HistoryView
allItems={historyAllItems}
controller={controller}
items={historyItems}
onPageChange={onHistoryPageChange}
onSelectTask={handleSelectTask}
pagination={historyPagination}
/>
)
break
case "config":
content = (
<ConfigView
dataDir={dataDir}
globalClineRulesToggles={globalClineRulesToggles}
globalHooks={globalHooks}
globalSkills={globalSkills}
globalState={globalState}
globalWorkflowToggles={globalWorkflowToggles}
hooksEnabled={hooksEnabled}
localAgentsRulesToggles={localAgentsRulesToggles}
localClineRulesToggles={localClineRulesToggles}
localCursorRulesToggles={localCursorRulesToggles}
localSkills={localSkills}
localWindsurfRulesToggles={localWindsurfRulesToggles}
localWorkflowToggles={localWorkflowToggles}
onToggleHook={onToggleHook}
onToggleRule={onToggleRule}
onToggleSkill={onToggleSkill}
onToggleWorkflow={onToggleWorkflow}
skillsEnabled={skillsEnabled}
workspaceHooks={workspaceHooks}
workspaceState={workspaceState}
/>
)
break
case "auth":
content = (
<AuthView
controller={controller}
onComplete={onComplete}
onError={onError}
onNavigateToWelcome={handleNavigateToWelcome}
/>
)
break
case "task":
case "welcome":
content = (
<TaskContextProvider controller={controller}>
{jsonOutput ? (
<TaskJsonView onComplete={onComplete} onError={onError} taskId={selectedTaskId} verbose={verbose} />
) : (
<ChatView
controller={controller}
initialImages={pendingInitialImages}
initialPrompt={pendingInitialPrompt}
onComplete={onComplete}
onError={onError}
onExit={onWelcomeExit}
taskId={selectedTaskId}
/>
)}
</TaskContextProvider>
)
break
default:
content = null
}
return (
<StdinProvider isRawModeSupported={isRawModeSupported}>
<Box key={resizeKey}>{content}</Box>
</StdinProvider>
)
}
File diff suppressed because it is too large Load Diff
-442
View File
@@ -1,442 +0,0 @@
/**
* User input prompt component
* Handles different types of user interactions (text input, confirmations, choices)
*/
import type { ClineAsk, ClineMessage } from "@shared/ExtensionMessage"
import { Box, Text, useApp, useInput } from "ink"
import React, { useCallback, useEffect, useRef, useState } from "react"
import { useStdinContext } from "../context/StdinContext"
import { useTaskController } from "../context/TaskContext"
import { useLastCompletedAskMessage } from "../hooks/useStateSubscriber"
import { isEnterKey, isMouseEscapeSequence } from "../utils/input"
import { jsonParseSafe } from "../utils/parser"
interface AskPromptProps {
onRespond?: (response: string) => void
}
type PromptType = "confirmation" | "text" | "options" | "plan_mode_text" | "completion" | "exit_confirmation" | "none"
function getPromptType(ask: ClineAsk, text: string): PromptType {
switch (ask) {
case "followup": {
const parts = jsonParseSafe(text, {
question: undefined as string | undefined,
options: undefined as string[] | undefined,
})
if (parts.options && parts.options.length > 0) {
return "options"
}
return "text"
}
case "plan_mode_respond": {
const parts = jsonParseSafe(text, {
question: undefined as string | undefined,
options: undefined as string[] | undefined,
})
if (parts.options && parts.options.length > 0) {
return "options"
}
// Plan mode without options - allow text input or toggle to Act mode
return "plan_mode_text"
}
case "completion_result":
// Task completed - allow follow-up question or exit
return "completion"
case "resume_task":
case "resume_completed_task":
return "exit_confirmation"
case "command":
case "tool":
case "browser_action_launch":
case "use_mcp_server":
return "confirmation"
default:
return "none"
}
}
export const AskPrompt: React.FC<AskPromptProps> = ({ onRespond }) => {
const { exit } = useApp()
const { isRawModeSupported } = useStdinContext()
const controller = useTaskController()
const lastAskMessage = useLastCompletedAskMessage()
const [textInput, setTextInput] = useState("")
const [responded, setResponded] = useState(false)
const lastAskTs = useRef<number | null>(null)
// Reset state when ask message changes
useEffect(() => {
if (lastAskMessage && lastAskMessage.ts !== lastAskTs.current) {
lastAskTs.current = lastAskMessage.ts
setTextInput("")
setResponded(false)
}
}, [lastAskMessage])
const sendResponse = useCallback(
async (responseType: string, text?: string) => {
if (responded || !controller?.task) {
return
}
setResponded(true)
try {
await controller.task.handleWebviewAskResponse(responseType, text)
onRespond?.(text || responseType)
} catch {
// Controller may be disposed
}
},
[controller, responded, onRespond],
)
const toggleToActMode = useCallback(async () => {
if (responded || !controller) {
return
}
setResponded(true)
try {
await controller.togglePlanActMode("act")
onRespond?.("Switched to Act mode")
} catch {
// Controller may be disposed
}
}, [controller, responded, onRespond])
// Handle keyboard input
useInput(
(input, key) => {
// Filter out mouse escape sequences
if (isMouseEscapeSequence(input)) {
return
}
if (!lastAskMessage || responded) {
return
}
const ask = lastAskMessage.ask as ClineAsk
const text = lastAskMessage.text || ""
const promptType = getPromptType(ask, text)
if (promptType === "confirmation" || promptType === "exit_confirmation") {
// y/n confirmation
if (input.toLowerCase() === "y") {
sendResponse("yesButtonClicked")
} else if (input.toLowerCase() === "n") {
if (promptType === "exit_confirmation") {
exit()
return
}
sendResponse("noButtonClicked")
}
} else if (promptType === "options") {
// Number selection for options, or free text input
const parts = jsonParseSafe(text, { options: [] as string[] })
if (isEnterKey(input, key)) {
// Submit free text on Enter
if (textInput.trim()) {
sendResponse("messageResponse", textInput.trim())
}
} else if (key.backspace || key.delete) {
setTextInput((prev) => prev.slice(0, -1))
} else if (input && !key.ctrl && !key.meta) {
// Check if it's a number for option selection (only when no text typed yet)
const num = Number.parseInt(input, 10)
if (textInput === "" && !Number.isNaN(num) && num >= 1 && num <= parts.options.length) {
const selectedOption = parts.options[num - 1]
sendResponse("messageResponse", selectedOption)
} else {
// Regular character input for free text
setTextInput((prev) => prev + input)
}
}
} else if (promptType === "text") {
// Text input mode
if (isEnterKey(input, key)) {
// Submit on Enter
if (textInput.trim()) {
sendResponse("messageResponse", textInput.trim())
}
} else if (key.backspace || key.delete) {
setTextInput((prev) => prev.slice(0, -1))
} else if (input && !key.ctrl && !key.meta) {
// Regular character input
setTextInput((prev) => prev + input)
}
} else if (promptType === "plan_mode_text") {
// Plan mode text input - allows text response or toggle to Act mode
if (isEnterKey(input, key)) {
// Submit on Enter
if (textInput.trim()) {
sendResponse("messageResponse", textInput.trim())
} else {
// Empty enter = switch to Act mode
toggleToActMode()
}
} else if (key.backspace || key.delete) {
setTextInput((prev) => prev.slice(0, -1))
} else if (input && !key.ctrl && !key.meta) {
// Regular character input
setTextInput((prev) => prev + input)
}
} else if (promptType === "completion") {
// Task completed - allow follow-up question or exit
if (isEnterKey(input, key)) {
if (textInput.trim()) {
// Send follow-up question
sendResponse("messageResponse", textInput.trim())
} else {
// Empty enter = confirm completion (exit)
sendResponse("yesButtonClicked")
}
} else if (key.backspace || key.delete) {
setTextInput((prev) => prev.slice(0, -1))
} else if (input && !key.ctrl && !key.meta) {
// Regular character input
setTextInput((prev) => prev + input)
}
}
},
{ isActive: isRawModeSupported && !!lastAskMessage && !responded },
)
if (!lastAskMessage || responded) {
return null
}
const ask = lastAskMessage.ask as ClineAsk
const text = lastAskMessage.text || ""
const promptType = getPromptType(ask, text)
const icon = getCliMessagePrefixIcon(lastAskMessage)
if (promptType === "none") {
return null
}
switch (ask) {
case "followup": {
const parts = jsonParseSafe(text, {
question: undefined as string | undefined,
options: undefined as string[] | undefined,
})
if (parts.options && parts.options.length > 0) {
return (
<Box flexDirection="column" marginTop={1}>
<Text color="cyan">Select an option (enter number):</Text>
{parts.options.map((opt, idx) => (
<Box key={idx} marginLeft={2}>
<Text>{`${idx + 1}. ${opt}`}</Text>
</Box>
))}
<Box marginTop={1}>
<Text>{icon} </Text>
<Text color="cyan">Or type: </Text>
<Text>{textInput}</Text>
<Text inverse> </Text>
</Box>
<Text color="gray">(Enter number to select, or type response + Enter)</Text>
</Box>
)
}
// Text input prompt
return (
<Box flexDirection="column" marginTop={1}>
<Box>
<Text>{icon} </Text>
<Text color="cyan">Reply: </Text>
<Text>{textInput}</Text>
<Text inverse> </Text>
</Box>
<Text color="gray">(Type your response and press Enter)</Text>
</Box>
)
}
case "plan_mode_respond": {
const parts = jsonParseSafe(text, {
question: undefined as string | undefined,
options: undefined as string[] | undefined,
})
if (parts.options && parts.options.length > 0) {
return (
<Box flexDirection="column" marginTop={1}>
<Text color="cyan">Select an option (enter number):</Text>
{parts.options.map((opt, idx) => (
<Box key={idx} marginLeft={2}>
<Text>{`${idx + 1}. ${opt}`}</Text>
</Box>
))}
<Box marginTop={1}>
<Text>{icon} </Text>
<Text color="cyan">Or type: </Text>
<Text>{textInput}</Text>
<Text inverse> </Text>
</Box>
<Text color="gray">(Enter number to select, or type response + Enter)</Text>
</Box>
)
}
// Plan mode text input - show option to switch to Act mode
return (
<Box flexDirection="column" marginTop={1}>
<Box>
<Text>{icon} </Text>
<Text color="cyan">Reply: </Text>
<Text>{textInput}</Text>
<Text inverse> </Text>
</Box>
<Text color="gray">(Type response + Enter, or just Enter to switch to Act mode)</Text>
</Box>
)
}
case "command":
return (
<Box flexDirection="column" marginTop={1}>
<Box>
<Text>{icon} </Text>
<Text color="yellow"> Execute this command? </Text>
<Text color="gray">(y/n)</Text>
</Box>
</Box>
)
case "tool":
return (
<Box flexDirection="column" marginTop={1}>
<Box>
<Text>{icon} </Text>
<Text color="blue"> Use this tool? </Text>
<Text color="gray">(y/n)</Text>
</Box>
</Box>
)
case "completion_result":
return (
<Box flexDirection="column" marginTop={1}>
<Box>
<Text>{icon} </Text>
<Text color="cyan">Follow-up: </Text>
<Text>{textInput}</Text>
<Text inverse> </Text>
</Box>
<Text color="gray">(Type follow-up question + Enter, or q to exit)</Text>
</Box>
)
case "resume_task":
case "resume_completed_task":
return (
<Box flexDirection="column" marginTop={1}>
<Box>
<Text>{icon} </Text>
<Text color="cyan"> Resume task? </Text>
<Text color="gray">(y/n)</Text>
</Box>
</Box>
)
case "browser_action_launch":
return (
<Box flexDirection="column" marginTop={1}>
<Box>
<Text>{icon} </Text>
<Text color="cyan"> Launch browser? </Text>
<Text color="gray">(y/n)</Text>
</Box>
</Box>
)
case "use_mcp_server":
return (
<Box flexDirection="column" marginTop={1}>
<Box>
<Text>{icon} </Text>
<Text color="cyan"> Use MCP server? </Text>
<Text color="gray">(y/n)</Text>
</Box>
</Box>
)
default:
return null
}
}
/**
* Get emoji icon for message type
*/
function getCliMessagePrefixIcon(message: ClineMessage): string {
if (message.type === "ask") {
switch (message.ask) {
case "followup":
return "❓"
case "command":
case "command_output":
return "⚙️"
case "tool":
return "🔧"
case "completion_result":
return "✅"
case "api_req_failed":
return "❌"
case "resume_task":
case "resume_completed_task":
return "▶️"
case "browser_action_launch":
return "🌐"
case "use_mcp_server":
return "🔌"
case "plan_mode_respond":
return "📋"
default:
return "❔"
}
}
switch (message.say) {
case "task":
return "📋"
case "error":
return "❌"
case "text":
return "💬"
case "reasoning":
return "🧠"
case "completion_result":
return "✅"
case "user_feedback":
return "👤"
case "command":
case "command_output":
return "⚙️"
case "tool":
return "🔧"
case "browser_action":
case "browser_action_launch":
case "browser_action_result":
return "🌐"
case "mcp_server_request_started":
case "mcp_server_response":
return "🔌"
case "api_req_started":
case "api_req_finished":
return "🔄"
case "checkpoint_created":
return "💾"
case "info":
return "️"
case "generate_explanation":
return "📝"
default:
return " "
}
}
-942
View File
@@ -1,942 +0,0 @@
/**
* Auth view component
* Handles interactive authentication and provider configuration
*/
import { Box, Text, useApp, useInput } from "ink"
import Spinner from "ink-spinner"
import React, { useCallback, useEffect, useMemo, useState } from "react"
import { refreshOcaModels } from "@/core/controller/models/refreshOcaModels"
import { StateManager } from "@/core/storage/StateManager"
import { openAiCodexOAuthManager } from "@/integrations/openai-codex/oauth"
import { AuthService } from "@/services/auth/AuthService"
import { openAiCodexDefaultModelId, openRouterDefaultModelId } from "@/shared/api"
import { StringRequest } from "@/shared/proto/cline/common"
import { openExternal } from "@/utils/env"
import { COLORS } from "../constants/colors"
import { useStdinContext } from "../context/StdinContext"
import { useClineFeaturedModels } from "../hooks/useClineFeaturedModels"
import { useOcaAuth } from "../hooks/useOcaAuth"
import { useScrollableList } from "../hooks/useScrollableList"
import { type DetectedSources, detectImportSources, type ImportSource } from "../utils/import-configs"
import { isEnterKey, isMouseEscapeSequence } from "../utils/input"
import { applyBedrockConfig, applyProviderConfig } from "../utils/provider-config"
import { useValidProviders } from "../utils/providers"
import { ApiKeyInput } from "./ApiKeyInput"
import { StaticRobotFrame } from "./AsciiMotionCli"
import { BedrockCustomModelFlow } from "./BedrockCustomModelFlow"
import { type BedrockConfig, BedrockSetup } from "./BedrockSetup"
import {
FeaturedModelPicker,
getFeaturedModelAtIndex,
getFeaturedModelMaxIndex,
isBrowseAllSelected,
} from "./FeaturedModelPicker"
import { ImportView } from "./ImportView"
import { CUSTOM_MODEL_ID, getDefaultModelId, hasModelPicker, ModelPicker } from "./ModelPicker"
import { OcaEmployeeCheck } from "./OcaEmployeeCheck"
import { getProviderLabel } from "./ProviderPicker"
type AuthStep =
| "menu"
| "provider"
| "apikey"
| "modelid"
| "baseurl"
| "saving"
| "success"
| "error"
| "cline_auth"
| "oca_employee_check"
| "oca_auth"
| "cline_model"
| "openai_codex_auth"
| "bedrock"
| "import"
| "bedrock_custom"
interface AuthViewProps {
controller: any
onComplete?: () => void
onError?: () => void
onNavigateToWelcome?: () => void
}
interface SelectItem {
label: string
value: string
}
/**
* Select component with keyboard navigation
*/
const Select: React.FC<{
items: SelectItem[]
onSelect: (value: string) => void
label?: string
}> = ({ items, onSelect, label }) => {
const { isRawModeSupported } = useStdinContext()
const [selectedIndex, setSelectedIndex] = useState(0)
useInput(
(input, key) => {
if (key.upArrow) {
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : items.length - 1))
} else if (key.downArrow) {
setSelectedIndex((prev) => (prev < items.length - 1 ? prev + 1 : 0))
} else if (isEnterKey(input, key)) {
onSelect(items[selectedIndex].value)
}
},
{ isActive: isRawModeSupported },
)
return (
<Box flexDirection="column">
{label && (
<Text bold color="cyan">
{label}
</Text>
)}
{items.map((item, index) => (
<Box key={item.value}>
<Text color={index === selectedIndex ? COLORS.primaryBlue : undefined}>
{index === selectedIndex ? " " : " "}
{item.label}
</Text>
</Box>
))}
<Text color="gray">(Use arrow keys to navigate, Enter to select)</Text>
</Box>
)
}
/**
* Text input component - minimal, just the input field
*/
const TextInput: React.FC<{
value: string
onChange: (value: string) => void
onSubmit: (value: string) => void
placeholder?: string
isPassword?: boolean
}> = ({ value, onChange, onSubmit, placeholder, isPassword }) => {
const { isRawModeSupported } = useStdinContext()
useInput(
(input, key) => {
// Filter out mouse escape sequences
if (isMouseEscapeSequence(input)) {
return
}
if (isEnterKey(input, key)) {
onSubmit(value)
} else if (key.backspace || key.delete) {
onChange(value.slice(0, -1))
} else if (input && !key.ctrl && !key.meta) {
onChange(value + input)
}
},
{ isActive: isRawModeSupported },
)
const displayValue = isPassword ? "•".repeat(value.length) : value
return (
<Box>
{!displayValue && placeholder ? (
<Text color="gray">e.g. {placeholder}</Text>
) : (
<Text color="white">{displayValue || ""}</Text>
)}
<Text inverse> </Text>
</Box>
)
}
export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onError, onNavigateToWelcome }) => {
const { exit } = useApp()
const providers = useValidProviders()
const [step, setStep] = useState<AuthStep>("menu")
const [selectedProvider, setSelectedProvider] = useState<string>(
StateManager.get().getApiConfiguration().actModeApiProvider ||
StateManager.get().getApiConfiguration().planModeApiProvider ||
"",
)
const [apiKey, setApiKey] = useState("")
const [modelId, setModelId] = useState("")
const [baseUrl, setBaseUrl] = useState("")
const [errorMessage, setErrorMessage] = useState("")
const [providerSearch, setProviderSearch] = useState("")
const [providerIndex, setProviderIndex] = useState(0)
const [clineModelIndex, setClineModelIndex] = useState(0)
const featuredModels = useClineFeaturedModels()
const [importSources, setImportSources] = useState<DetectedSources>({ codex: false, opencode: false })
const [importSource, setImportSource] = useState<ImportSource | null>(null)
const [bedrockConfig, setBedrockConfig] = useState<BedrockConfig | null>(null)
// OCA auth hook - enabled when step is oca_auth
const handleOcaAuthSuccess = useCallback(async () => {
await applyProviderConfig({ providerId: "oca", controller })
// Fetch OCA models from the API - this sets actModeOcaModelId/planModeOcaModelId in state
await refreshOcaModels(controller, StringRequest.create({ value: "" }))
const stateManager = StateManager.get()
stateManager.setGlobalState("welcomeViewCompleted", true)
await stateManager.flushPendingState()
setSelectedProvider("oca")
const actModelId = stateManager.getGlobalSettingsKey("actModeOcaModelId") || ""
setModelId(actModelId)
setStep("success")
}, [controller])
const handleOcaAuthError = useCallback((error: Error) => {
setErrorMessage(error.message)
setStep("error")
}, [])
const { startAuth: initiateOcaAuth } = useOcaAuth({
controller,
enabled: step === "oca_auth",
onSuccess: handleOcaAuthSuccess,
onError: handleOcaAuthError,
})
// Main menu items - conditionally include import options
const mainMenuItems: SelectItem[] = useMemo(() => {
const items: SelectItem[] = [{ label: "Sign in with Cline", value: "cline_auth" }]
// Add OpenAI Codex option for ChatGPT subscribers
items.push({ label: "Sign in with ChatGPT Subscription", value: "openai_codex_auth" })
// Add import options if detected
if (importSources.codex) {
items.push({ label: "Import from Codex CLI", value: "import_codex" })
}
if (importSources.opencode) {
items.push({ label: "Import from OpenCode", value: "import_opencode" })
}
items.push({ label: "Use your own API key", value: "configure_byo" })
items.push({ label: "Exit", value: "exit" })
return items
}, [importSources])
// Provider menu items - filtered by search (searches both ID and display name)
const providerItems: SelectItem[] = useMemo(() => {
const search = providerSearch.toLowerCase()
const filtered = providerSearch
? providers.filter((p) => p.toLowerCase().includes(search) || getProviderLabel(p).toLowerCase().includes(search))
: providers
return filtered.map((p: string) => ({
label: getProviderLabel(p),
value: p,
}))
}, [providers, providerSearch])
// Use shared scrollable list hook for provider windowing
const TOTAL_PROVIDER_ROWS = 8
const {
visibleStart: providerVisibleStart,
visibleCount: providerVisibleCount,
showTopIndicator: showProviderTopIndicator,
showBottomIndicator: showProviderBottomIndicator,
} = useScrollableList(providerItems.length, providerIndex, TOTAL_PROVIDER_ROWS)
const visibleProviderItems = useMemo(() => {
return providerItems.slice(providerVisibleStart, providerVisibleStart + providerVisibleCount)
}, [providerItems, providerVisibleStart, providerVisibleCount])
// Detect import sources on mount
useEffect(() => {
setImportSources(detectImportSources())
}, [])
// Reset provider index when search changes
// biome-ignore lint/correctness/useExhaustiveDependencies: we want to reset here
useEffect(() => {
setProviderIndex(0)
}, [providerSearch])
// Set default model when entering model step
useEffect(() => {
if (step === "modelid" && hasModelPicker(selectedProvider)) {
setModelId(getDefaultModelId(selectedProvider))
}
}, [step, selectedProvider])
// Subscribe to auth status updates when in cline_auth step
useEffect(() => {
if (step !== "cline_auth") {
return
}
let cancelled = false
// Create a streaming response handler that receives auth state updates
const responseHandler = async (authState: { user?: { email?: string } }, _isLast?: boolean) => {
if (cancelled) {
return
}
if (authState.user?.email) {
// Auth succeeded - save configuration and transition to model selection
await applyProviderConfig({ providerId: "cline", controller })
setSelectedProvider("cline")
setModelId(openRouterDefaultModelId)
setStep("cline_model")
}
}
// Subscribe to auth status updates
const authService = AuthService.getInstance(controller)
authService.subscribeToAuthStatusUpdate(controller, {}, responseHandler, `cli-auth-${Date.now()}`)
return () => {
cancelled = true
}
}, [step, controller])
// Start OpenAI Codex OAuth flow
const startOpenAiCodexAuth = useCallback(async () => {
try {
// Get the authorization URL and start the callback server
const authUrl = openAiCodexOAuthManager.startAuthorizationFlow()
// Open browser to authorization URL (uses cross-platform 'open' package)
await openExternal(authUrl)
// Wait for the callback
await openAiCodexOAuthManager.waitForCallback()
// Success - save configuration
await applyProviderConfig({ providerId: "openai-codex", controller })
const stateManager = StateManager.get()
stateManager.setGlobalState("welcomeViewCompleted", true)
await stateManager.flushPendingState()
setSelectedProvider("openai-codex")
setModelId(openAiCodexDefaultModelId)
setStep("success")
} catch (error) {
openAiCodexOAuthManager.cancelAuthorizationFlow()
setErrorMessage(error instanceof Error ? error.message : String(error))
setStep("error")
}
}, [])
// Start Cline auth flow
const startClineAuth = useCallback(async () => {
try {
setStep("cline_auth")
await AuthService.getInstance(controller).createAuthRequest()
} catch (error) {
setErrorMessage(error instanceof Error ? error.message : String(error))
setStep("error")
}
}, [controller])
const startOcaAuth = useCallback(() => {
setStep("oca_auth")
initiateOcaAuth()
}, [initiateOcaAuth])
const handleMainMenuSelect = useCallback(
(value: string) => {
if (value === "exit") {
exit()
onComplete?.()
} else if (value === "cline_auth") {
startClineAuth()
} else if (value === "openai_codex_auth") {
setStep("openai_codex_auth")
startOpenAiCodexAuth()
} else if (value === "configure_byo") {
setStep("provider")
} else if (value === "import_codex") {
setImportSource("codex")
setStep("import")
} else if (value === "import_opencode") {
setImportSource("opencode")
setStep("import")
}
},
[exit, onComplete, startClineAuth, startOpenAiCodexAuth],
)
const handleProviderSelect = useCallback(
(value: string) => {
setSelectedProvider(value)
if (value === "oca") {
// Show employee check screen before starting auth
setStep("oca_employee_check")
} else if (value === "openai-codex") {
setStep("openai_codex_auth")
startOpenAiCodexAuth()
} else if (value === "bedrock") {
setStep("bedrock")
} else {
setStep("apikey")
}
},
[startOcaAuth, startOpenAiCodexAuth],
)
const handleApiKeySubmit = useCallback(
(value: string) => {
if (!value.trim() || !selectedProvider) {
// Don't allow empty
return
}
// Store in local state - will be saved via StateManager in saveConfiguration
setApiKey(value)
setStep("modelid")
},
[selectedProvider],
)
// Save custom Bedrock ARN configuration with base model for capability detection
const saveCustomBedrockConfiguration = useCallback(
async (arn: string, baseModelId: string) => {
try {
if (!bedrockConfig) {
throw new Error("Bedrock configuration is missing")
}
await applyBedrockConfig({
bedrockConfig,
modelId: arn,
customModelBaseId: baseModelId,
controller,
})
const stateManager = StateManager.get()
stateManager.setGlobalState("welcomeViewCompleted", true)
await stateManager.flushPendingState()
setStep("success")
} catch (error) {
setErrorMessage(error instanceof Error ? error.message : String(error))
setStep("error")
}
},
[bedrockConfig, controller],
)
const saveConfiguration = useCallback(
async (model: string, base: string) => {
try {
if (selectedProvider === "bedrock" && bedrockConfig) {
await applyBedrockConfig({
bedrockConfig,
modelId: model,
controller,
})
} else {
await applyProviderConfig({
providerId: selectedProvider,
apiKey,
modelId: model,
baseUrl: base,
controller,
})
}
const stateManager = StateManager.get()
stateManager.setGlobalState("welcomeViewCompleted", true)
await stateManager.flushPendingState()
setStep("success")
} catch (error) {
setErrorMessage(error instanceof Error ? error.message : String(error))
setStep("error")
}
},
[selectedProvider, apiKey, bedrockConfig, controller],
)
const handleModelIdSubmit = useCallback(
(value: string) => {
// Intercept "Custom" selection for Bedrock — redirect to custom ARN input flow
if (value === CUSTOM_MODEL_ID && selectedProvider === "bedrock") {
setStep("bedrock_custom")
return
}
if (value.trim()) {
setModelId(value)
}
// Only show baseurl step for OpenAI-like providers
if (["openai", "openai-native"].includes(selectedProvider)) {
setStep("baseurl")
} else {
setStep("saving")
saveConfiguration(value, "")
}
},
[selectedProvider, saveConfiguration],
)
const handleBaseUrlSubmit = useCallback(
(value: string) => {
setBaseUrl(value)
setStep("saving")
saveConfiguration(modelId, value)
},
[modelId, saveConfiguration],
)
const handleClineModelSelect = useCallback(
(modelId: string) => {
setModelId(modelId)
setStep("saving")
saveConfiguration(modelId, "")
},
[saveConfiguration],
)
const handleBedrockComplete = useCallback((config: BedrockConfig) => {
setBedrockConfig(config)
setStep("modelid")
}, [])
const handleImportComplete = useCallback(() => {
setStep("success")
}, [])
const handleImportCancel = useCallback(() => {
setImportSource(null)
setStep("menu")
}, [])
// Auto-navigate to welcome after success (immediate)
// For quick setup mode (no onNavigateToWelcome), exit the Ink app
useEffect(() => {
if (step === "success") {
if (onNavigateToWelcome) {
onNavigateToWelcome()
} else {
// Quick setup mode - exit Ink app after successful configuration
// The cleanup handler in runInkApp will handle process exit
exit()
}
}
}, [step, onNavigateToWelcome, exit])
// Error screen menu items
const errorMenuItems: SelectItem[] = useMemo(() => {
const items: SelectItem[] = [{ label: "Try again", value: "retry" }]
if (onNavigateToWelcome) {
items.push({ label: "Start a task", value: "welcome" })
}
items.push({ label: "Exit", value: "exit" })
return items
}, [onNavigateToWelcome])
const handleErrorMenuSelect = useCallback(
(value: string) => {
if (value === "retry") {
// Reset state and go back to menu
setErrorMessage("")
setApiKey("")
setModelId("")
setBaseUrl("")
setSelectedProvider("")
setStep("menu")
} else if (value === "welcome") {
onNavigateToWelcome?.()
} else if (value === "exit") {
onError?.()
exit()
}
},
[onNavigateToWelcome, onError, exit],
)
// Handle going back to previous step
const goBack = useCallback(() => {
switch (step) {
case "provider":
setProviderSearch("")
setProviderIndex(0)
setStep("menu")
break
case "apikey":
setApiKey("")
setStep("provider")
break
case "modelid":
setModelId("")
// Go back to cline_model if we came from there (Cline provider)
if (selectedProvider === "cline") {
setStep("cline_model")
} else if (selectedProvider === "bedrock") {
// Bedrock skips the API key step — go back to Bedrock setup
setStep("bedrock")
} else {
setStep("apikey")
}
break
case "baseurl":
setBaseUrl("")
setStep("modelid")
break
case "oca_employee_check":
setStep("provider")
break
case "oca_auth":
setStep("oca_employee_check")
break
case "cline_auth":
setStep("menu")
break
case "openai_codex_auth":
openAiCodexOAuthManager.cancelAuthorizationFlow()
setStep("menu")
break
case "cline_model":
setClineModelIndex(0)
setStep("menu")
break
case "bedrock":
setBedrockConfig(null)
setStep("provider")
break
case "import":
setImportSource(null)
setStep("menu")
break
case "error":
setErrorMessage("")
setStep("menu")
break
// menu, saving, success - no back action
}
}, [step, selectedProvider])
// Render the auth box content based on current step
// Note: "menu" step is rendered separately in the main return for proper menuIndex tracking
const renderAuthContent = () => {
switch (step) {
case "provider": {
return (
<Box flexDirection="column">
<Text color="white">Select a provider</Text>
<Text> </Text>
<Box>
<Text color="gray">Search: </Text>
<Text color="white">{providerSearch}</Text>
<Text inverse> </Text>
</Box>
<Text> </Text>
{showProviderTopIndicator && <Text color="gray">... {providerVisibleStart} more above</Text>}
{visibleProviderItems.map((item, i) => {
const actualIndex = providerVisibleStart + i
return (
<Box key={item.value}>
<Text color={actualIndex === providerIndex ? COLORS.primaryBlue : undefined}>
{actualIndex === providerIndex ? " " : " "}
{item.label}
</Text>
</Box>
)
})}
{showProviderBottomIndicator && (
<Text color="gray">
... {providerItems.length - providerVisibleStart - providerVisibleCount} more below
</Text>
)}
{providerItems.length === 0 && <Text color="gray">No providers match "{providerSearch}"</Text>}
<Text> </Text>
<Text color="gray">Type to search, arrows to navigate, Enter to select, Esc to go back</Text>
</Box>
)
}
case "apikey":
return (
<ApiKeyInput
isActive={step === "apikey"}
onCancel={goBack}
onChange={setApiKey}
onSubmit={handleApiKeySubmit}
providerName={getProviderLabel(selectedProvider)}
value={apiKey}
/>
)
case "modelid":
// Show model picker for providers with static model lists
if (hasModelPicker(selectedProvider)) {
return (
<Box flexDirection="column">
<Text color="white">Select a model</Text>
<Text> </Text>
<ModelPicker
controller={controller}
isActive={step === "modelid"}
onChange={setModelId}
onSubmit={handleModelIdSubmit}
provider={selectedProvider}
/>
<Text> </Text>
<Text color="gray">Type to search, arrows to navigate, Enter to select, Esc to go back</Text>
</Box>
)
}
// Fall back to text input for providers without static model lists
return (
<Box flexDirection="column">
<Text color="white">Model ID</Text>
<Text> </Text>
<Text color="gray">e.g., claude-sonnet-4-6, gpt-4o</Text>
<Text> </Text>
<TextInput onChange={setModelId} onSubmit={handleModelIdSubmit} placeholder="model-id" value={modelId} />
<Text> </Text>
<Text color="gray">Enter to continue, Esc to go back</Text>
</Box>
)
case "baseurl":
return (
<Box flexDirection="column">
<Text color="white">Base URL (optional)</Text>
<Text> </Text>
<Text color="gray">For self-hosted or proxy endpoints</Text>
<Text> </Text>
<TextInput
onChange={setBaseUrl}
onSubmit={handleBaseUrlSubmit}
placeholder="https://api.example.com/v1"
value={baseUrl}
/>
<Text> </Text>
<Text color="gray">Enter to skip or continue, Esc to go back</Text>
</Box>
)
case "saving":
return (
<Box>
<Text color={COLORS.primaryBlue}>
<Spinner type="dots" />
</Text>
<Text color="white"> Saving configuration...</Text>
</Box>
)
case "oca_employee_check":
return <OcaEmployeeCheck isActive={step === "oca_employee_check"} onCancel={goBack} onSignIn={startOcaAuth} />
case "oca_auth":
case "cline_auth":
return (
<Box flexDirection="column">
<Box>
<Text color={COLORS.primaryBlue}>
<Spinner type="dots" />
</Text>
<Text color="white"> Waiting for browser sign-in...</Text>
</Box>
<Text> </Text>
<Text color="gray">Complete sign-in in your browser, then return here.</Text>
<Text> </Text>
<Text color="gray">Esc to cancel</Text>
</Box>
)
case "openai_codex_auth":
return (
<Box flexDirection="column">
<Box>
<Text color={COLORS.primaryBlue}>
<Spinner type="dots" />
</Text>
<Text color="white"> Waiting for ChatGPT sign-in...</Text>
</Box>
<Text> </Text>
<Text color="gray">Sign in with your ChatGPT account in the browser.</Text>
<Text color="gray">Requires ChatGPT Plus, Pro, or Team subscription.</Text>
<Text> </Text>
<Text color="gray">Esc to cancel</Text>
</Box>
)
case "cline_model": {
return (
<Box flexDirection="column">
<Text color="white">Choose a model</Text>
<Text> </Text>
<FeaturedModelPicker featuredModels={featuredModels} selectedIndex={clineModelIndex} />
</Box>
)
}
case "bedrock":
return (
<BedrockSetup
isActive={step === "bedrock"}
onCancel={() => {
setBedrockConfig(null)
setStep("provider")
}}
onComplete={handleBedrockComplete}
/>
)
case "bedrock_custom":
return (
<BedrockCustomModelFlow
isActive={step === "bedrock_custom"}
onCancel={() => setStep("modelid")}
onComplete={(arn, baseModelId) => {
setStep("saving")
saveCustomBedrockConfiguration(arn, baseModelId)
}}
/>
)
case "import":
if (!importSource) {
return null
}
return <ImportView onCancel={handleImportCancel} onComplete={handleImportComplete} source={importSource} />
case "error":
return (
<Box flexDirection="column">
<Text bold color="red">
Something went wrong
</Text>
<Text> </Text>
<Text color="yellow">{errorMessage}</Text>
<Text> </Text>
<Select items={errorMenuItems} onSelect={handleErrorMenuSelect} />
</Box>
)
default:
return null
}
}
// For menu step, we need to handle input at the top level
const { isRawModeSupported } = useStdinContext()
const [menuIndex, setMenuIndex] = useState(0)
// Steps that allow going back with escape (apikey handled by ApiKeyInput component)
// OcaEmployeeCheck handles its own escape key, so oca_employee_check is not in this list
const canGoBack = [
"provider",
"modelid",
"baseurl",
"cline_auth",
"oca_auth",
"cline_model",
"openai_codex_auth",
"bedrock",
"error",
].includes(step)
useInput(
(input, key) => {
// Handle escape to go back (except on menu)
if (key.escape && canGoBack) {
goBack()
return
}
if (step === "menu") {
if (key.upArrow) {
setMenuIndex((prev) => (prev > 0 ? prev - 1 : mainMenuItems.length - 1))
} else if (key.downArrow) {
setMenuIndex((prev) => (prev < mainMenuItems.length - 1 ? prev + 1 : 0))
} else if (isEnterKey(input, key)) {
handleMainMenuSelect(mainMenuItems[menuIndex].value)
}
} else if (step === "provider") {
if (key.upArrow) {
setProviderIndex((prev) => (prev > 0 ? prev - 1 : providerItems.length - 1))
} else if (key.downArrow) {
setProviderIndex((prev) => (prev < providerItems.length - 1 ? prev + 1 : 0))
} else if (isEnterKey(input, key)) {
if (providerItems[providerIndex]) {
handleProviderSelect(providerItems[providerIndex].value)
}
} else if (key.backspace || key.delete) {
setProviderSearch((prev) => prev.slice(0, -1))
} else if (input && !key.ctrl && !key.meta) {
setProviderSearch((prev) => prev + input)
}
} else if (step === "cline_model") {
const maxIndex = getFeaturedModelMaxIndex(featuredModels)
if (key.upArrow) {
setClineModelIndex((prev) => (prev > 0 ? prev - 1 : maxIndex))
} else if (key.downArrow) {
setClineModelIndex((prev) => (prev < maxIndex ? prev + 1 : 0))
} else if (isEnterKey(input, key)) {
if (isBrowseAllSelected(clineModelIndex, featuredModels)) {
setStep("modelid")
} else {
const selectedModel = getFeaturedModelAtIndex(clineModelIndex, featuredModels)
if (selectedModel) {
handleClineModelSelect(selectedModel.id)
}
}
}
}
// Note: modelid step input is handled by ModelPicker component
},
{ isActive: isRawModeSupported && (step === "menu" || step === "provider" || step === "cline_model" || canGoBack) },
)
return (
<Box flexDirection="column" paddingLeft={1} paddingRight={1} width="100%">
{/* Cline robot - centered */}
<StaticRobotFrame />
{/* Welcome text - centered */}
<Box justifyContent="center" marginTop={1}>
<Text bold color="white">
Welcome to Cline
</Text>
</Box>
{/* Auth box with border */}
<Box
borderColor="gray"
borderStyle="round"
flexDirection="column"
marginTop={1}
paddingBottom={1}
paddingLeft={2}
paddingRight={2}
paddingTop={1}>
{step === "menu" ? (
<Box flexDirection="column">
<Text color="gray">How would you like to get started?</Text>
<Text> </Text>
{mainMenuItems.map((item, index) => (
<Box key={item.value}>
<Text>
<Text color={index === menuIndex ? COLORS.primaryBlue : undefined}>
{index === menuIndex ? " " : " "}
{item.label}
</Text>
{item.value === "cline_auth" && <Text color="yellow"> (try Opus 4.6!)</Text>}
</Text>
</Box>
))}
<Text> </Text>
<Text color="gray">Use arrow keys, Enter to select</Text>
</Box>
) : (
renderAuthContent()
)}
</Box>
</Box>
)
}
@@ -1,112 +0,0 @@
/**
* Bedrock Custom Model Flow component
* Two-step flow: ARN/custom model ID input → base model selection for capability detection.
* Used by both AuthView (onboarding) and SettingsPanelContent (/settings).
*/
import { Box, Text, useInput } from "ink"
// biome-ignore lint/correctness/noUnusedImports: React is needed for JSX at runtime
import React, { useCallback, useState } from "react"
import { COLORS } from "../constants/colors"
import { useStdinContext } from "../context/StdinContext"
import { isEnterKey } from "../utils/input"
import { getModelList } from "./ModelPicker"
import { SearchableList } from "./SearchableList"
type FlowStep = "arn_input" | "base_model"
interface BedrockCustomModelFlowProps {
/** Whether this component should capture keyboard input */
isActive: boolean
/** Called when the user completes both steps (ARN + base model selection) */
onComplete: (arn: string, baseModelId: string) => void
/** Called when the user presses Escape on the first step (ARN input) */
onCancel: () => void
}
export const BedrockCustomModelFlow: React.FC<BedrockCustomModelFlowProps> = ({ isActive, onComplete, onCancel }) => {
const { isRawModeSupported } = useStdinContext()
const [step, setStep] = useState<FlowStep>("arn_input")
const [customArn, setCustomArn] = useState("")
const handleArnSubmit = useCallback(() => {
if (customArn.trim()) {
setStep("base_model")
}
}, [customArn])
const handleBaseModelCancel = useCallback(() => {
setStep("arn_input")
}, [])
useInput(
(input, key) => {
if (step === "arn_input") {
if (key.escape) {
onCancel()
} else if (isEnterKey(input, key)) {
handleArnSubmit()
} else if (key.backspace || key.delete) {
setCustomArn((prev) => prev.slice(0, -1))
} else if (input && !key.ctrl && !key.meta) {
setCustomArn((prev) => prev + input)
}
return
}
if (step === "base_model") {
if (key.escape) {
handleBaseModelCancel()
}
// Other input is handled by SearchableList
}
},
{ isActive: isActive && isRawModeSupported },
)
if (step === "arn_input") {
return (
<Box flexDirection="column">
<Text bold color={COLORS.primaryBlue}>
Custom Model ID
</Text>
<Box marginTop={1}>
<Text color="gray">Enter your Application Inference Profile ARN or custom model ID</Text>
</Box>
<Box marginTop={1}>
{customArn ? (
<Text color="white">{customArn}</Text>
) : (
<Text color="gray">e.g. arn:aws:bedrock:region:account:application-inference-profile/...</Text>
)}
<Text inverse> </Text>
</Box>
<Box marginTop={1}>
<Text color="gray">Enter to continue, Esc to go back</Text>
</Box>
</Box>
)
}
// step === "base_model"
return (
<Box flexDirection="column">
<Text bold color={COLORS.primaryBlue}>
Base Inference Model
</Text>
<Text color="gray">Select the base model your inference profile uses (for capability detection)</Text>
<Box marginTop={1}>
<SearchableList
isActive={isActive && step === "base_model"}
items={getModelList("bedrock").map((id) => ({ id, label: id }))}
onSelect={(item) => {
onComplete(customArn, item.id)
}}
/>
</Box>
<Box marginTop={1}>
<Text color="gray">Type to search, arrows to navigate, Enter to select, Esc to go back</Text>
</Box>
</Box>
)
}

Some files were not shown because too many files have changed in this diff Show More