mirror of
https://github.com/cline/cline.git
synced 2026-09-05 14:14:01 +08:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e9bef4253a | |||
| 2a1c3fd9ba | |||
| 5ac991902b | |||
| c40c7c098c | |||
| b62c7874f6 |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"cline": patch
|
||||
---
|
||||
|
||||
fix(cli): prevent hang when spawned without TTY
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix decimal input crash in OpenAI Compatible price fields (#8129)
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"cline": patch
|
||||
---
|
||||
|
||||
Supports rendering markdown table in chat view.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix: build complete handlers when upadting the api config
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Updating script documentation and removing unnecessary continue on error
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fixed missing provider from list
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
feat(skills): Make skills always enabled and remove feature toggle setting
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fixed Favorite Icon / Star from getting clipped in the task history view
|
||||
@@ -41,11 +41,11 @@ fi
|
||||
|
||||
# Install project dependencies
|
||||
echo "Installing dependencies..."
|
||||
npm run install:all
|
||||
bun run install:all
|
||||
|
||||
# Generate gRPC/protobuf types (required for TypeScript)
|
||||
echo "Generating proto types..."
|
||||
npm run protos
|
||||
bun run protos
|
||||
|
||||
echo ""
|
||||
echo "Session setup complete!"
|
||||
|
||||
@@ -147,29 +147,14 @@ When filling out the template:
|
||||
|
||||
### 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
|
||||
gh pr create --title "PR_TITLE" --body "PR_BODY" --base main
|
||||
```
|
||||
|
||||
**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.
|
||||
Alternatively, create as draft if the user wants review before marking ready:
|
||||
```bash
|
||||
gh pr create --title "PR_TITLE" --body "PR_BODY" --base main --draft
|
||||
```
|
||||
|
||||
## Post-Creation
|
||||
|
||||
|
||||
@@ -147,17 +147,6 @@ Required steps:
|
||||
|
||||
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()`.
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ Here, we use the common `StringRequest` and `KeyValuePair` types.
|
||||
|
||||
After editing a `.proto` file, regenerate the TypeScript code. From the project root, run:
|
||||
```bash
|
||||
npm run protos
|
||||
bun run protos
|
||||
```
|
||||
This command compiles all `.proto` files and outputs the generated code to `src/generated/` and `src/shared/`. Do not edit these generated files manually.
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ On the main branch, create a commit that updates:
|
||||
|
||||
Each changeset file in `.changeset/` corresponds to a PR. Read them to identify which ones belong to the commits you're hotfixing, then delete those files.
|
||||
|
||||
**Skip running `npm run install:all`** - the automation handles outdated lockfiles.
|
||||
**Skip running `bun run install:all`** - the automation handles outdated lockfiles.
|
||||
|
||||
Commit with message format: `v{VERSION} Release Notes (hotfix)`
|
||||
|
||||
|
||||
@@ -1,42 +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 = '''
|
||||
npm run compile && IS_DEV=true DEV_WORKSPACE_FOLDER="$(pwd)" CLINE_ENVIRONMENT=production code \
|
||||
--extensionDevelopmentPath="$(pwd)" \
|
||||
--disable-workspace-trust \
|
||||
--disable-extension saoudrizwan.claude-dev \
|
||||
--disable-extension saoudrizwan.cline-nightly \
|
||||
"$(pwd)"
|
||||
'''
|
||||
|
||||
[[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
|
||||
git checkout package-lock.json webview-ui/package-lock.json
|
||||
'''
|
||||
+3
-2
@@ -1,2 +1,3 @@
|
||||
/.github/ @saoudrizwan @arafatkatze @maxpaulus43 @candieduniverse
|
||||
/README.md @saoudrizwan @juanpflores
|
||||
/docs/
|
||||
/.github/ @saoudrizwan @garoth @sjf
|
||||
/README.md @saoudrizwan @nickbaumann98
|
||||
|
||||
@@ -59,8 +59,8 @@ We're not looking for exhaustive documentation - just evidence that you've thoug
|
||||
<!-- Put an 'x' in all boxes that apply -->
|
||||
|
||||
- [ ] 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)
|
||||
- [ ] Tests are passing (`bun test`) and code is formatted and linted (`bun run format && bun run lint`)
|
||||
- [ ] I have created a changeset using `bun run changeset` (required for user-facing changes)
|
||||
- [ ] I have reviewed [contributor guidelines](https://github.com/cline/cline/blob/main/CONTRIBUTING.md)
|
||||
|
||||
### Screenshots
|
||||
|
||||
@@ -62,9 +62,9 @@ class TestCoverage(unittest.TestCase):
|
||||
|
||||
# Use xvfb-run on Linux
|
||||
if sys.platform.startswith('linux'):
|
||||
cmd = f"cd {root_dir} && xvfb-run -a npm run test:coverage > {cls.extension_coverage_file} 2>&1"
|
||||
cmd = f"cd {root_dir} && xvfb-run -a bun run test:coverage > {cls.extension_coverage_file} 2>&1"
|
||||
else:
|
||||
cmd = f"cd {root_dir} && npm run test:coverage > {cls.extension_coverage_file} 2>&1"
|
||||
cmd = f"cd {root_dir} && bun run test:coverage > {cls.extension_coverage_file} 2>&1"
|
||||
|
||||
log("Running extension tests...")
|
||||
log(f"Command: {cmd}")
|
||||
@@ -73,7 +73,7 @@ class TestCoverage(unittest.TestCase):
|
||||
|
||||
# Run webview tests with coverage
|
||||
log("Running webview tests...")
|
||||
cmd = f"cd {webview_dir} && npm run test:coverage > {cls.webview_coverage_file} 2>&1"
|
||||
cmd = f"cd {webview_dir} && bun run test:coverage > {cls.webview_coverage_file} 2>&1"
|
||||
log(f"Command: {cmd}")
|
||||
result = subprocess.run(cmd, shell=True, check=False, capture_output=True, text=True)
|
||||
log(f"Webview tests exit code: {result.returncode}")
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
name: Claude Issue Triage
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
# Manual trigger for backfilling existing issues. Run from terminal:
|
||||
# gh workflow run claude-issue-triage.yml -f issue_number=1234
|
||||
# Or batch process:
|
||||
# gh issue list --state open --limit 10 --json number --jq '.[].number' | while read num; do
|
||||
# gh workflow run claude-issue-triage.yml -f issue_number=$num
|
||||
# sleep 60
|
||||
# done
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
issue_number:
|
||||
description: 'Issue number to triage'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
claude-issue-triage:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 120
|
||||
# SECURITY: These permissions are intentionally restrictive.
|
||||
# - contents: read -> Claude can read the codebase but CANNOT write/push any code
|
||||
# - issues: write -> Claude can comment and add labels (the only write access needed)
|
||||
# - pull-requests: read -> Claude can view PR context but CANNOT create PRs
|
||||
# This ensures that even if a malicious user attempts prompt injection via issue content,
|
||||
# Claude cannot modify repository code, create branches, or open PRs.
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: read
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Run Issue Response & Triage
|
||||
id: triage
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
allowed_non_write_users: "*"
|
||||
# Allow all tools - security is enforced by GitHub permissions above (contents: read, issues: write)
|
||||
claude_args: --model claude-opus-4-5-20251101 --allowedTools "Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch"
|
||||
prompt: |
|
||||
You're a GitHub issue first responder for the open source Cline repository.
|
||||
|
||||
**Issue:** #${{ github.event.issue.number || inputs.issue_number }}
|
||||
**Title:** ${{ github.event.issue.title || 'See issue details below' }}
|
||||
**Author:** @${{ github.event.issue.user.login || 'See issue details below' }}
|
||||
|
||||
## Your job
|
||||
|
||||
Investigate this issue thoroughly, then post a single helpful comment that helps the user and gives maintainers the context they need.
|
||||
|
||||
## Investigation
|
||||
|
||||
Start by reading the full issue:
|
||||
gh issue view ${{ github.event.issue.number || inputs.issue_number }}
|
||||
|
||||
### Search for duplicates and related issues
|
||||
|
||||
Search thoroughly for existing issues that match this one:
|
||||
gh issue list --search "<keywords from the issue>" --state all --limit 30
|
||||
gh issue list --search "<error messages>" --state all --limit 20
|
||||
gh issue list --search "<affected feature/component>" --state all --limit 20
|
||||
|
||||
For each relevant issue you find, read it including its comments:
|
||||
gh issue view <number> --comments
|
||||
|
||||
You're looking for:
|
||||
- **Duplicates**: Issues describing the same problem. Link to them and explain why you think they're duplicates. If closed, check how they were resolved - the solution might apply here.
|
||||
- **Related issues**: Similar problems or context that could help. Pull useful information from their comments (workarounds others found, debugging steps that helped, maintainer explanations). Link to them and explain the connection.
|
||||
|
||||
If there are closed issues with solutions, surface those solutions prominently - this might immediately solve the user's problem.
|
||||
|
||||
### Analyze recent changes (ALWAYS DO THIS)
|
||||
|
||||
Many issues are regressions from recent releases. **Always** check what changed recently:
|
||||
gh release list --limit 10
|
||||
gh pr list --state merged --limit 50 --json number,title,mergedAt,author,body
|
||||
|
||||
Look for PRs merged in the last few weeks that might correlate with the issue. If you find a likely connection:
|
||||
gh pr view <number>
|
||||
gh pr diff <number>
|
||||
git log --since="1 month ago" --oneline -- <relevant paths>
|
||||
git show <commit>
|
||||
|
||||
**Always include your findings in your comment:**
|
||||
- If you find a regression, call it out explicitly: which PR/commit likely caused it, who authored it, what changed, and suggest a fix direction if you can see one.
|
||||
- If you don't find anything related, still mention it: "I analyzed recent PRs and releases but didn't find any changes that seem related to this issue."
|
||||
|
||||
### Search the codebase
|
||||
|
||||
Find the relevant code:
|
||||
- Use grep/find to locate code related to the issue
|
||||
- Key areas: `src/api/` (providers/models), `src/core/prompts/` (tools/prompts), platform-specific code for VS Code vs JetBrains
|
||||
|
||||
### Find documentation
|
||||
|
||||
Cline docs are at **https://docs.cline.bot/** and built with Mintlify from the `docs/` directory.
|
||||
|
||||
The URL structure maps directly to the file structure:
|
||||
- `docs/getting-started/selecting-your-model.mdx` → https://docs.cline.bot/getting-started/selecting-your-model
|
||||
- `docs/troubleshooting.mdx` → https://docs.cline.bot/troubleshooting
|
||||
- Headings become anchors: `## Which Model` → `#which-model`
|
||||
|
||||
Search the `docs/` directory to find relevant documentation, then construct URLs to link users to:
|
||||
```bash
|
||||
ls docs/
|
||||
grep -r "keyword" docs/ --include="*.mdx" -l
|
||||
```
|
||||
|
||||
### Identify subject matter experts
|
||||
|
||||
For issues that clearly need engineering attention:
|
||||
git log --since="6 months ago" --format="%an" -- <relevant paths> | sort | uniq -c | sort -rn | head -5
|
||||
|
||||
Cross-reference with GitHub usernames. Include in your response (@mention, do NOT assign):
|
||||
|
||||
| SME | Reason |
|
||||
|-----|--------|
|
||||
| @username1 | Authored PR #X which modified this area |
|
||||
| @username2 | Primary contributor to affected file |
|
||||
|
||||
## Weak model detection
|
||||
|
||||
Many issues are caused by users running small or non-frontier models that don't tool-call reliably. Signs include:
|
||||
- Model failing to use tools correctly
|
||||
- Nonsensical or malformed responses
|
||||
- User is running a small/local model or older model version
|
||||
|
||||
If this looks like a weak model issue, kindly suggest they try reproducing with Claude Sonnet and report back if it persists. Link to https://docs.cline.bot/getting-started/selecting-your-model if helpful. Still label and triage normally.
|
||||
|
||||
## Your comment
|
||||
|
||||
Write a single comment as a helpful community member. Be conversational, not robotic. Include what's relevant:
|
||||
|
||||
- **Helpful response** - Answer their question, suggest a fix, provide a workaround. If you found solutions in related closed issues, surface those prominently.
|
||||
- **Duplicates and related issues** - Link to any you found and explain why they're duplicates/related. Summarize useful context from their comments.
|
||||
- **Regression analysis** - If this looks like a regression, explain what change likely caused it, link to the PR/commit, and tag the author.
|
||||
- **Clarifying questions** - If you need more info, ask specific questions. Don't ask for things already provided.
|
||||
- **SME table** - Include the table above if this needs engineering attention. Don't tag people for questions with obvious answers or weak-model issues.
|
||||
- **Context for maintainers** - Relevant code paths, what you found. Keep it concise.
|
||||
- **Docs links** - If there's relevant documentation, link to it naturally in your response as a recommendation (e.g., "For more details, check out [the Ollama setup guide](url)"). Do NOT add a "Sources" section at the end - integrate doc links into your response where they're helpful.
|
||||
- **Possible Duplicates section** - ALWAYS include a "Possible Duplicates" section at the end of your comment listing issues that might be duplicates so maintainers can quickly close if appropriate. If none found, say "No obvious duplicates found."
|
||||
|
||||
## Labels
|
||||
First, retrieve all available labels and read their descriptions to understand what each is for:
|
||||
gh label list --json name,description --limit 100
|
||||
|
||||
Then apply the appropriate labels based on your analysis. Only use labels from the list above—do not create new labels.
|
||||
gh issue edit ${{ github.event.issue.number || inputs.issue_number }} --add-label "label1,label2"
|
||||
|
||||
If your regression analysis found a likely culprit (a recent PR/commit that probably caused this issue), add the "Regression" label:
|
||||
gh issue edit ${{ github.event.issue.number || inputs.issue_number }} --add-label "Regression"
|
||||
|
||||
IMPORTANT: After posting your comment, add the "Bot Responded" label to indicate this issue has received an automated response:
|
||||
gh issue edit ${{ github.event.issue.number || inputs.issue_number }} --add-label "Bot Responded"
|
||||
|
||||
## Remember
|
||||
|
||||
- **This is a one-time automated response** - you will NOT see their reply or respond again. Never say things like "I can help you", "let me know", "once I have that info", or "I can give you more targeted help" - you won't be there to follow up. If you ask clarifying questions, frame them for the maintainers who will follow up, e.g., "If you can share X, that would help the maintainers diagnose this."
|
||||
- Don't be formulaic. Respond to what the issue actually needs.
|
||||
- Surface solutions from past issues - often the fastest path to helping.
|
||||
- Connecting regressions to specific changes is extremely valuable.
|
||||
- Link issues with #number so they're clickable.
|
||||
@@ -0,0 +1,284 @@
|
||||
name: Claude PR Review
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, ready_for_review]
|
||||
# Manual trigger for backfilling existing PRs. Run from terminal:
|
||||
# gh workflow run claude-pr-review.yml -f pr_number=1234
|
||||
# Or batch process open PRs:
|
||||
# gh pr list --state open --limit 10 --json number --jq '.[].number' | while read num; do
|
||||
# gh workflow run claude-pr-review.yml -f pr_number=$num
|
||||
# sleep 60
|
||||
# done
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: 'PR number to review'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
claude-pr-review:
|
||||
# Runs on PR opened/ready_for_review (skips drafts) or manual trigger for backfilling
|
||||
if: |
|
||||
(github.event_name == 'pull_request' && github.event.pull_request.draft == false) ||
|
||||
github.event_name == 'workflow_dispatch'
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 120
|
||||
|
||||
# SECURITY: These permissions are intentionally restrictive.
|
||||
# - contents: read -> Claude can read the codebase but CANNOT write/push any code
|
||||
# - pull-requests: write -> Claude can post reviews and inline suggestions
|
||||
# - issues: read -> Claude can search for related issues
|
||||
# NOTE: Even with pull-requests: write, Claude CANNOT merge PRs because branch protection
|
||||
# requires 1 approval from a Code Owner. The GITHUB_TOKEN cannot bypass this.
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: read
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Print HEAD commit
|
||||
run: |
|
||||
echo "HEAD is at: $(git rev-parse HEAD)"
|
||||
echo "Short: $(git rev-parse --short HEAD)"
|
||||
git log -1 --format="Commit: %H%nAuthor: %an <%ae>%nDate: %ad%nMessage: %s"
|
||||
|
||||
- name: Get PR number
|
||||
id: pr
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||||
echo "number=${{ inputs.pr_number }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "number=${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Run PR Review
|
||||
id: review
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
allowed_non_write_users: "*"
|
||||
claude_args: --model claude-opus-4-5-20251101 --allowedTools "Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch"
|
||||
prompt: |
|
||||
You're a GitHub PR reviewer for the open source Cline repository. Your goal is to give the PR author helpful feedback and give maintainers the context they need to review efficiently.
|
||||
|
||||
PR: #${{ steps.pr.outputs.number }}
|
||||
|
||||
## Gather context
|
||||
|
||||
```bash
|
||||
# Get full PR details
|
||||
gh pr view ${{ steps.pr.outputs.number }} --json number,title,body,author,createdAt,updatedAt,isDraft,labels,commits,files,additions,deletions,changedFiles,baseRefName,headRefName,mergeable,reviewDecision
|
||||
|
||||
# Get the diff
|
||||
gh pr diff ${{ steps.pr.outputs.number }}
|
||||
|
||||
# Check CI status
|
||||
gh pr checks ${{ steps.pr.outputs.number }}
|
||||
|
||||
# Get existing review comments (to understand context and your previous feedback)
|
||||
gh api repos/${{ github.repository }}/pulls/${{ steps.pr.outputs.number }}/comments --jq '.[] | {user: .user.login, body: .body, path: .path, created_at: .created_at}'
|
||||
|
||||
# Get conversation comments
|
||||
gh pr view ${{ steps.pr.outputs.number }} --comments
|
||||
```
|
||||
|
||||
If this is a re-review (workflow_dispatch event):
|
||||
Read your previous comments carefully. Understand what you asked for before.
|
||||
Check if new commits or comments address your previous feedback.
|
||||
|
||||
## Check contributing guidelines
|
||||
|
||||
Flag (but don't block) if:
|
||||
- Missing changeset - For user-facing changes, check if there's a `.changeset/` file:
|
||||
```bash
|
||||
gh pr diff ${{ steps.pr.outputs.number }} --name-only | grep '.changeset/' || echo "No changeset found"
|
||||
```
|
||||
If missing, ask them to run `npm run changeset`
|
||||
- Missing tests - New features should have tests
|
||||
|
||||
## Find related issues and PRs
|
||||
|
||||
Search thoroughly for context that might help with the review:
|
||||
|
||||
```bash
|
||||
# Find related issues for context
|
||||
gh issue list --search "<keywords from the PR>" --state all --limit 30
|
||||
gh issue list --search "<error messages or feature names>" --state all --limit 20
|
||||
|
||||
# Find similar PRs for reference
|
||||
gh pr list --search "<keywords>" --state all --limit 30
|
||||
```
|
||||
|
||||
For each relevant issue or PR you find, read it including comments:
|
||||
```bash
|
||||
gh issue view <number> --comments
|
||||
gh pr view <number> --comments
|
||||
```
|
||||
|
||||
Look for:
|
||||
- Open issues this PR might fix that weren't linked in the description
|
||||
- Similar PRs that went through review - what feedback did they get? What patterns did they follow?
|
||||
- Context from maintainer discussions that could inform your review
|
||||
|
||||
## Find subject matter experts
|
||||
|
||||
For files changed in this PR, find who knows the code best:
|
||||
```bash
|
||||
# Get files changed
|
||||
gh pr diff ${{ steps.pr.outputs.number }} --name-only
|
||||
|
||||
# For each relevant path, find contributors
|
||||
git log --since="6 months ago" --format="%an" -- <path> | sort | uniq -c | sort -rn | head -5
|
||||
```
|
||||
|
||||
Cross-reference git authors with GitHub usernames. Include an SME table in your response:
|
||||
|
||||
| SME | Reason |
|
||||
|-----|--------|
|
||||
| @username1 | Authored PR #X which modified this area |
|
||||
| @username2 | Primary contributor to affected file (15 commits in 6 months) |
|
||||
| @username3 | Reviewed similar PR #Y with extensive feedback |
|
||||
|
||||
## Deep code review
|
||||
|
||||
This is the most important part. Don't just look for syntax issues - understand what the PR is trying to achieve and whether the implementation is the right approach.
|
||||
|
||||
Step 1: Understand the intent
|
||||
Read the PR description and understand what the author is trying to accomplish. What problem are they solving? What feature are they adding?
|
||||
|
||||
Step 2: Form your own opinion first
|
||||
Before analyzing their code, think about how YOU would implement this feature or fix. What files would you touch? What patterns would you follow? What edge cases would you handle?
|
||||
|
||||
Step 3: Compare approaches
|
||||
Now look at their implementation. How does it compare to what you would have done?
|
||||
- Is their approach better in some ways? Note what they did well.
|
||||
- Is their approach missing something? Be specific about what and why.
|
||||
- Are there edge cases they haven't considered?
|
||||
- Does it follow the patterns established in similar parts of the codebase?
|
||||
|
||||
Step 4: Look at the bigger picture
|
||||
- What other files or systems does this change interact with?
|
||||
- Could this break anything else?
|
||||
- Is there additional work needed beyond this PR to complete the feature?
|
||||
- Does this fit well with the overall architecture?
|
||||
|
||||
Step 5: Find reference implementations
|
||||
Look for similar changes in the codebase:
|
||||
```bash
|
||||
git log --oneline --all --grep="<relevant keywords>" | head -20
|
||||
git log --oneline -- <similar files> | head -20
|
||||
```
|
||||
|
||||
If this is adding a new API provider, look at how other providers are implemented.
|
||||
If this is adding a new feature, look at how similar features were added.
|
||||
Note where their implementation aligns with or diverges from established patterns.
|
||||
|
||||
Step 6: Standard code review checks
|
||||
- DRY: Is there duplicated code that could be extracted?
|
||||
- Error handling: Are errors handled appropriately?
|
||||
- Security: Any injection risks, credential exposure, unsafe dependencies?
|
||||
- Performance: Any obvious inefficiencies, memory leaks, N+1 patterns?
|
||||
- Types: Is TypeScript used correctly? Any unsafe type assertions?
|
||||
- Naming: Are variables and functions named clearly?
|
||||
- Comments: Is complex logic explained? Are there outdated comments?
|
||||
|
||||
## Inline code suggestions
|
||||
|
||||
For specific code improvements, use GitHub's suggestion syntax via `gh api`.
|
||||
This creates suggestions the author can commit with one click.
|
||||
|
||||
Single-line suggestion:
|
||||
```bash
|
||||
gh api repos/${{ github.repository }}/pulls/${{ steps.pr.outputs.number }}/reviews \
|
||||
-X POST \
|
||||
-f commit_id="$(gh pr view ${{ steps.pr.outputs.number }} --json headRefOid -q .headRefOid)" \
|
||||
-f event="COMMENT" \
|
||||
-f body="" \
|
||||
-F comments='[
|
||||
{
|
||||
"path": "src/example.ts",
|
||||
"line": 42,
|
||||
"body": "Consider simplifying:\n\n```suggestion\nconst result = items.filter(Boolean);\n```"
|
||||
}
|
||||
]'
|
||||
```
|
||||
|
||||
Multi-line suggestion (replacing lines 40-45):
|
||||
```bash
|
||||
gh api repos/${{ github.repository }}/pulls/${{ steps.pr.outputs.number }}/reviews \
|
||||
-X POST \
|
||||
-f commit_id="$(gh pr view ${{ steps.pr.outputs.number }} --json headRefOid -q .headRefOid)" \
|
||||
-f event="COMMENT" \
|
||||
-f body="" \
|
||||
-F comments='[
|
||||
{
|
||||
"path": "src/example.ts",
|
||||
"start_line": 40,
|
||||
"line": 45,
|
||||
"body": "This can be simplified:\n\n```suggestion\nconst simplified = doThing();\n```"
|
||||
}
|
||||
]'
|
||||
```
|
||||
|
||||
Use inline suggestions for concrete improvements. Use regular comments for questions or broader feedback.
|
||||
|
||||
## Post your review
|
||||
|
||||
After your investigation, post a single helpful comment that helps the author and gives maintainers context.
|
||||
|
||||
Start by noting the commit hash you reviewed:
|
||||
```bash
|
||||
git rev-parse --short HEAD
|
||||
```
|
||||
Include this at the top of your comment: "Reviewed at commit: <short hash>"
|
||||
|
||||
Then thank them for their contribution. Be conversational, not robotic.
|
||||
|
||||
Include what's relevant:
|
||||
- In-depth explanation of what the PR does - Be comprehensive. A maintainer should be able to read this section and fully understand the author's intent, why they made the changes, how they implemented it, and what files/systems are affected. Don't just summarize - explain.
|
||||
- Related issues/PRs you found that provide useful context (link to them)
|
||||
- Your review findings (issues to address, suggestions, etc.)
|
||||
- Clear next steps for the author
|
||||
|
||||
Include a "For Maintainers" section with:
|
||||
- Anything else useful to help the maintainer resolve this PR
|
||||
- Related issues/PRs with context on why they're relevant
|
||||
- Open issues this PR might fix that weren't linked in the description
|
||||
- Your recommendation: merge as-is, needs changes, needs discussion, close, etc.
|
||||
- SME table - who should review this and why
|
||||
|
||||
For the SME table:
|
||||
| SME | Reason |
|
||||
|-----|--------|
|
||||
| @username | Primary contributor to affected files |
|
||||
|
||||
## Update labels
|
||||
|
||||
Add appropriate labels based on your analysis:
|
||||
```bash
|
||||
gh label list --json name,description --limit 100
|
||||
gh pr edit ${{ steps.pr.outputs.number }} --add-label "label1,label2"
|
||||
```
|
||||
|
||||
When done, add the reviewed label:
|
||||
```bash
|
||||
gh pr edit ${{ steps.pr.outputs.number }} --add-label "Bot Reviewed"
|
||||
```
|
||||
|
||||
## Remember
|
||||
|
||||
- This is a one-time automated response - you will NOT see their reply or respond again. Never say things like "let me know if you have questions", "I can help you with", or "feel free to ask" - you won't be there to follow up. Frame any questions for the maintainers who will follow up.
|
||||
- Be helpful and welcoming - Many contributors are new to the project
|
||||
- Be specific - Point to exact lines and suggest fixes, don't give vague feedback
|
||||
- Think deeply - Don't just surface-level review, understand the intent and evaluate the approach
|
||||
- Use inline suggestions - Make it easy for authors to accept changes
|
||||
- You're a first-pass reviewer - A human maintainer will do final approval
|
||||
@@ -0,0 +1,325 @@
|
||||
name: Cline PR Code Review
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
[opened, ready_for_review]
|
||||
# Manual trigger for backfilling existing PRs. Run from terminal:
|
||||
# gh workflow run cline-pr-review.yml -f pr_number=1234
|
||||
# Or batch process open PRs:
|
||||
# gh pr list --state open --limit 10 --json number --jq '.[].number' | while read num; do
|
||||
# gh workflow run cline-pr-review.yml -f pr_number=$num
|
||||
# sleep 60
|
||||
# done
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: "PR number to review"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
concurrency:
|
||||
group: pr-review-${{ github.event.pull_request.number || inputs.pr_number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
cline-pr-review:
|
||||
# Runs on PR opened/ready_for_review (skips drafts) or manual trigger for backfilling
|
||||
if: |
|
||||
(github.event_name == 'pull_request' && github.event.pull_request.draft == false) ||
|
||||
github.event_name == 'workflow_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
|
||||
# SECURITY: These permissions are intentionally restrictive.
|
||||
# - contents: read -> cline can read the codebase but CANNOT write/push any code
|
||||
# - pull-requests: write -> cline can post reviews and inline suggestions
|
||||
# - issues: read -> cline can search for related issues
|
||||
# NOTE: Even with pull-requests: write, cline CANNOT merge PRs because branch protection
|
||||
# requires 1 approval from a Code Owner. The GITHUB_TOKEN cannot bypass this.
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: read
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Print HEAD commit
|
||||
run: |
|
||||
echo "HEAD is at: $(git rev-parse HEAD)"
|
||||
echo "Short: $(git rev-parse --short HEAD)"
|
||||
git log -1 --format="Commit: %H%nAuthor: %an <%ae>%nDate: %ad%nMessage: %s"
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: "npm"
|
||||
|
||||
- name: Install and Verify Cline CLI
|
||||
run: |
|
||||
npm install -g cline
|
||||
cline version # verify installation
|
||||
|
||||
- name: Configure Cline with Anthropic
|
||||
run: |
|
||||
npx cline auth --provider anthropic \
|
||||
--apikey "${{ secrets.ANTHROPIC_API_KEY }}" \
|
||||
--modelid claude-opus-4-5-20251101
|
||||
|
||||
- name: Get PR number
|
||||
id: pr
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||||
echo "number=${{ inputs.pr_number }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "number=${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Review PR with Cline
|
||||
env:
|
||||
PR_NUMBER: ${{ steps.pr.outputs.number }}
|
||||
GITHUB_REPO: ${{ github.repository }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
CLINE_COMMAND_PERMISSIONS: |
|
||||
{
|
||||
"allow": [
|
||||
"gh pr diff *",
|
||||
"gh pr view *",
|
||||
"gh pr checks *",
|
||||
"gh pr list *",
|
||||
"gh label list *",
|
||||
"gh issue list *",
|
||||
"gh issue view *",
|
||||
"git log *",
|
||||
"gh pr comment ${{ steps.pr.outputs.number }} *",
|
||||
"gh pr edit ${{ steps.pr.outputs.number }} *",
|
||||
"gh api repos/${{ github.repository }}/pulls/${{ steps.pr.outputs.number }}/comments *",
|
||||
"gh api repos/${{ github.repository }}/pulls/${{ steps.pr.outputs.number }}/reviews *"
|
||||
]
|
||||
}
|
||||
run: |
|
||||
npx cline --yolo 'You'\''re a GitHub PR reviewer for the open source Cline repository. Your goal is to give the PR author helpful feedback and give maintainers the context they need to review efficiently.
|
||||
|
||||
PR: #'"${PR_NUMBER}"'
|
||||
|
||||
## Gather context
|
||||
|
||||
```bash
|
||||
# Get full PR details
|
||||
gh pr view '"${PR_NUMBER}"' --json number,title,body,author,createdAt,updatedAt,isDraft,labels,commits,files,additions,deletions,changedFiles,baseRefName,headRefName,mergeable,reviewDecision
|
||||
|
||||
# Get the diff
|
||||
gh pr diff '"${PR_NUMBER}"'
|
||||
|
||||
# Check CI status
|
||||
gh pr checks '"${PR_NUMBER}"'
|
||||
|
||||
# Get existing review comments (to understand context and your previous feedback)
|
||||
gh api repos/'"${GITHUB_REPO}"'/pulls/'"${PR_NUMBER}"'/comments --jq '\''.[] | {user: .user.login, body: .body, path: .path, created_at: .created_at}'\''
|
||||
|
||||
# Get conversation comments
|
||||
gh pr view '"${PR_NUMBER}"' --comments
|
||||
```
|
||||
|
||||
If this is a re-review (workflow_dispatch event):
|
||||
Read your previous comments carefully. Understand what you asked for before.
|
||||
Check if new commits or comments address your previous feedback.
|
||||
|
||||
## Check contributing guidelines
|
||||
|
||||
Flag (but don'\''t block) if:
|
||||
- Missing changeset - For user-facing changes, check if there'\''s a `.changeset/` file:
|
||||
```bash
|
||||
gh pr diff '"${PR_NUMBER}"' --name-only | grep '\''.changeset/'\'' || echo '\''No changeset found'\''
|
||||
```
|
||||
If missing, ask them to run `npm run changeset`
|
||||
- Missing tests - New features should have tests
|
||||
|
||||
## Find related issues and PRs
|
||||
|
||||
Search thoroughly for context that might help with the review:
|
||||
|
||||
```bash
|
||||
# Find related issues for context
|
||||
gh issue list --search '\''<keywords from the PR>'\'' --state all --limit 30
|
||||
gh issue list --search '\''<error messages or feature names>'\'' --state all --limit 20
|
||||
|
||||
# Find similar PRs for reference
|
||||
gh pr list --search '\''<keywords>'\'' --state all --limit 30
|
||||
```
|
||||
|
||||
For each relevant issue or PR you find, read it including comments:
|
||||
```bash
|
||||
gh issue view <number> --comments
|
||||
gh pr view <number> --comments
|
||||
```
|
||||
|
||||
Look for:
|
||||
- Open issues this PR might fix that weren'\''t linked in the description
|
||||
- Similar PRs that went through review - what feedback did they get? What patterns did they follow?
|
||||
- Context from maintainer discussions that could inform your review
|
||||
|
||||
## Find subject matter experts
|
||||
|
||||
For files changed in this PR, find who knows the code best:
|
||||
```bash
|
||||
# Get files changed
|
||||
gh pr diff '"${PR_NUMBER}"' --name-only
|
||||
|
||||
# For each relevant path, find contributors
|
||||
git log --since='\''6 months ago'\'' --format='\''%an'\'' -- <path> | sort | uniq -c | sort -rn | head -5
|
||||
```
|
||||
|
||||
Cross-reference git authors with GitHub usernames. Include an SME table in your response:
|
||||
|
||||
| SME | Reason |
|
||||
|-----|--------|
|
||||
| @username1 | Authored PR #X which modified this area |
|
||||
| @username2 | Primary contributor to affected file (15 commits in 6 months) |
|
||||
| @username3 | Reviewed similar PR #Y with extensive feedback |
|
||||
|
||||
## Bash command usage
|
||||
|
||||
Don'\''t use operators like `|`, `&&`, or `;` - run each command separately and analyze the output.
|
||||
|
||||
When referencing command outputs, quote them properly to avoid formatting issues.
|
||||
|
||||
## Deep code review
|
||||
|
||||
This is the most important part. Don'\''t just look for syntax issues - understand what the PR is trying to achieve and whether the implementation is the right approach.
|
||||
|
||||
Step 1: Understand the intent
|
||||
Read the PR description and understand what the author is trying to accomplish. What problem are they solving? What feature are they adding?
|
||||
|
||||
Step 2: Form your own opinion first
|
||||
Before analyzing their code, think about how YOU would implement this feature or fix. What files would you touch? What patterns would you follow? What edge cases would you handle?
|
||||
|
||||
Step 3: Compare approaches
|
||||
Now look at their implementation. How does it compare to what you would have done?
|
||||
- Is their approach better in some ways? Note what they did well.
|
||||
- Is their approach missing something? Be specific about what and why.
|
||||
- Are there edge cases they haven'\''t considered?
|
||||
- Does it follow the patterns established in similar parts of the codebase?
|
||||
|
||||
Step 4: Look at the bigger picture
|
||||
- What other files or systems does this change interact with?
|
||||
- Could this break anything else?
|
||||
- Is there additional work needed beyond this PR to complete the feature?
|
||||
- Does this fit well with the overall architecture?
|
||||
|
||||
Step 5: Find reference implementations
|
||||
Look for similar changes in the codebase:
|
||||
```bash
|
||||
git log --oneline --all --grep='\''<relevant keywords>'\'' | head -20
|
||||
git log --oneline -- <similar files> | head -20
|
||||
```
|
||||
|
||||
If this is adding a new API provider, look at how other providers are implemented.
|
||||
If this is adding a new feature, look at how similar features were added.
|
||||
Note where their implementation aligns with or diverges from established patterns.
|
||||
|
||||
Step 6: Standard code review checks
|
||||
- DRY: Is there duplicated code that could be extracted?
|
||||
- Error handling: Are errors handled appropriately?
|
||||
- Security: Any injection risks, credential exposure, unsafe dependencies?
|
||||
- Performance: Any obvious inefficiencies, memory leaks, N+1 patterns?
|
||||
- Types: Is TypeScript used correctly? Any unsafe type assertions?
|
||||
- Naming: Are variables and functions named clearly?
|
||||
- Comments: Is complex logic explained? Are there outdated comments?
|
||||
|
||||
## Inline code suggestions
|
||||
|
||||
For specific code improvements, use GitHub'\''s suggestion syntax via `gh api`.
|
||||
This creates suggestions the author can commit with one click.
|
||||
|
||||
Single-line suggestion:
|
||||
```bash
|
||||
gh api repos/'"${GITHUB_REPO}"'/pulls/'"${PR_NUMBER}"'/reviews \
|
||||
-X POST \
|
||||
-f commit_id="$(gh pr view '"${PR_NUMBER}"' --json headRefOid -q .headRefOid)" \
|
||||
-f event='\''COMMENT'\'' \
|
||||
-f body='\'''\'' \
|
||||
-F comments='\''[
|
||||
{
|
||||
"path": "src/example.ts",
|
||||
"line": 42,
|
||||
"body": "Consider simplifying:\n\n```suggestion\nconst result = items.filter(Boolean);\n```"
|
||||
}
|
||||
]'\''
|
||||
```
|
||||
|
||||
Multi-line suggestion (replacing lines 40-45):
|
||||
```bash
|
||||
gh api repos/'"${GITHUB_REPO}"'/pulls/'"${PR_NUMBER}"'/reviews \
|
||||
-X POST \
|
||||
-f commit_id="$(gh pr view '"${PR_NUMBER}"' --json headRefOid -q .headRefOid)" \
|
||||
-f event='\''COMMENT'\'' \
|
||||
-f body='\'''\'' \
|
||||
-F comments='\''[
|
||||
{
|
||||
"path": "src/example.ts",
|
||||
"start_line": 40,
|
||||
"line": 45,
|
||||
"body": "This can be simplified:\n\n```suggestion\nconst simplified = doThing();\n```"
|
||||
}
|
||||
]'\''
|
||||
```
|
||||
|
||||
Use inline suggestions for concrete improvements. Use regular comments for questions or broader feedback.
|
||||
|
||||
## Post your review
|
||||
|
||||
After your investigation, post a single helpful comment that helps the author and gives maintainers context.
|
||||
|
||||
Start by noting the commit hash you reviewed:
|
||||
```bash
|
||||
git rev-parse --short HEAD
|
||||
```
|
||||
Include this at the top of your comment: "Reviewed at commit: <short hash>"
|
||||
|
||||
Then thank them for their contribution. Be conversational, not robotic.
|
||||
|
||||
Include what'\''s relevant:
|
||||
- In-depth explanation of what the PR does - Be comprehensive. A maintainer should be able to read this section and fully understand the author'\''s intent, why they made the changes, how they implemented it, and what files/systems are affected. Don'\''t just summarize - explain.
|
||||
- Related issues/PRs you found that provide useful context (link to them)
|
||||
- Your review findings (issues to address, suggestions, etc.)
|
||||
- Clear next steps for the author
|
||||
|
||||
Include a '\''For Maintainers'\'' section with:
|
||||
- Anything else useful to help the maintainer resolve this PR
|
||||
- Related issues/PRs with context on why they'\''re relevant
|
||||
- Open issues this PR might fix that weren'\''t linked in the description
|
||||
- Your recommendation: merge as-is, needs changes, needs discussion, close, etc.
|
||||
- SME table - who should review this and why
|
||||
|
||||
For the SME table:
|
||||
| SME | Reason |
|
||||
|-----|--------|
|
||||
| @username | Primary contributor to affected files |
|
||||
|
||||
## Update labels
|
||||
|
||||
Add appropriate labels based on your analysis:
|
||||
```bash
|
||||
gh label list --json name,description --limit 100
|
||||
gh pr edit '"${PR_NUMBER}"' --add-label '\''label1,label2'\''
|
||||
```
|
||||
|
||||
When done, add the reviewed label:
|
||||
```bash
|
||||
gh pr edit '"${PR_NUMBER}"' --add-label '\''Bot Reviewed'\''
|
||||
```
|
||||
|
||||
## Remember
|
||||
|
||||
- This is a one-time automated response - you will NOT see their reply or respond again. Never say things like '\''let me know if you have questions'\'', '\''I can help you with'\'', or '\''feel free to ask'\'' - you won'\''t be there to follow up. Frame any questions for the maintainers who will follow up.
|
||||
- Be helpful and welcoming - Many contributors are new to the project
|
||||
- Be specific - Point to exact lines and suggest fixes, don'\''t give vague feedback
|
||||
- Think deeply - Don'\''t just surface-level review, understand the intent and evaluate the approach
|
||||
- Use inline suggestions - Make it easy for authors to accept changes
|
||||
- You'\''re a first-pass reviewer - A human maintainer will do final approval'
|
||||
@@ -35,26 +35,10 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v4
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
# 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') }}
|
||||
bun-version: latest
|
||||
|
||||
# Cache VS Code installation
|
||||
- name: Cache VS Code
|
||||
@@ -75,20 +59,17 @@ jobs:
|
||||
~/.cache/ms-playwright
|
||||
~/Library/Caches/ms-playwright
|
||||
~/AppData/Local/ms-playwright
|
||||
key: playwright-browsers-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
|
||||
key: playwright-browsers-${{ runner.os }}-${{ hashFiles('bun.lockb') }}
|
||||
restore-keys: |
|
||||
playwright-browsers-${{ runner.os }}-
|
||||
|
||||
- name: Install root dependencies
|
||||
run: npm ci
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install webview-ui dependencies
|
||||
run: cd webview-ui && npm ci
|
||||
|
||||
- name: Install vsce
|
||||
run: npm install -g @vscode/vsce
|
||||
run: bun add -g @vscode/vsce
|
||||
|
||||
- name: Install xvfb on Linux
|
||||
if: matrix.runner == 'ubuntu'
|
||||
@@ -97,11 +78,11 @@ jobs:
|
||||
# Run optimized E2E tests (eliminates redundant builds)
|
||||
- name: Run E2E tests - Linux
|
||||
if: matrix.runner == 'ubuntu'
|
||||
run: xvfb-run -a npm run test:e2e:optimal
|
||||
run: xvfb-run -a bun run test:e2e:optimal
|
||||
|
||||
- name: Run E2E tests - Non-Linux
|
||||
if: matrix.runner != 'ubuntu'
|
||||
run: npm run test:e2e:optimal
|
||||
run: bun run test:e2e:optimal
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ failure() }}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: Publish NPM Release
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
confirm_publish:
|
||||
description: 'Type "publish" to confirm you want to publish to NPM'
|
||||
@@ -9,8 +9,7 @@ on:
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write # Required for pushing tags
|
||||
id-token: write # Required for npm trusted publishing (OIDC)
|
||||
contents: read
|
||||
checks: write # Required by test workflow
|
||||
pull-requests: write # Required by test workflow
|
||||
|
||||
@@ -21,7 +20,7 @@ jobs:
|
||||
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'
|
||||
if: github.repository == 'cline/cline' && github.ref == 'refs/heads/main' && github.event.inputs.confirm_publish == 'publish'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
@@ -31,10 +30,19 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24.x"
|
||||
node-version: "20.x"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
# 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') }}
|
||||
|
||||
- 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
|
||||
@@ -73,18 +81,13 @@ jobs:
|
||||
cat dist-standalone/package.json | grep version
|
||||
|
||||
- name: Publish to NPM with latest tag
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_RELEASE_TOKEN }}
|
||||
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'"
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
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
|
||||
schedule:
|
||||
- cron: "0 12 * * *" # 4 AM PST (UTC-8) = 12 UTC
|
||||
workflow_dispatch:
|
||||
|
||||
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
|
||||
|
||||
@@ -32,12 +27,6 @@ jobs:
|
||||
- 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
|
||||
@@ -50,9 +39,18 @@ jobs:
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24.x"
|
||||
node-version: "20.x"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
# Cache root dependencies - only reuse if package-lock.json exactly matches
|
||||
- name: Cache root dependencies
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
uses: actions/cache@v4
|
||||
id: root-cache
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
|
||||
|
||||
- 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
|
||||
@@ -120,6 +118,8 @@ jobs:
|
||||
|
||||
- name: Publish to NPM with nightly tag
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_RELEASE_TOKEN }}
|
||||
run: |
|
||||
echo "Publishing version ${{ steps.version.outputs.version }} to NPM with tag 'nightly'..."
|
||||
cd dist-standalone
|
||||
|
||||
@@ -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,53 +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:
|
||||
publish-main:
|
||||
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
|
||||
with:
|
||||
confirm_publish: ${{ github.event.inputs.confirm_publish }}
|
||||
|
||||
publish-nightly:
|
||||
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
|
||||
with:
|
||||
force_publish: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.force_nightly_publish == 'true' }}
|
||||
@@ -33,19 +33,16 @@ jobs:
|
||||
fi
|
||||
echo "Found recent commits, proceeding with build"
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
bun-version: latest
|
||||
|
||||
- name: Install root dependencies
|
||||
run: npm ci --include=optional
|
||||
|
||||
- name: Install webview-ui dependencies
|
||||
run: cd webview-ui && npm ci --include=optional
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Install Publishing Tools
|
||||
run: npm install -g @vscode/vsce ovsx
|
||||
run: bun add -g @vscode/vsce ovsx
|
||||
|
||||
- name: Publish Extension as Pre-release
|
||||
env:
|
||||
@@ -61,4 +58,4 @@ jobs:
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
|
||||
run: npm run publish:marketplace:nightly
|
||||
run: bun run publish:marketplace:nightly
|
||||
|
||||
@@ -39,19 +39,16 @@ jobs:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
bun-version: latest
|
||||
|
||||
- name: Install root dependencies
|
||||
run: npm install --include=optional
|
||||
|
||||
- name: Install webview-ui dependencies
|
||||
run: cd webview-ui && npm install --include=optional
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Install Publishing Tools
|
||||
run: npm install -g @vscode/vsce ovsx
|
||||
run: bun add -g @vscode/vsce ovsx
|
||||
|
||||
- name: Get Version
|
||||
id: get_version
|
||||
@@ -93,10 +90,10 @@ jobs:
|
||||
vsce package --allow-package-secrets sendgrid --out "cline-${{ steps.get_version.outputs.version }}.vsix"
|
||||
|
||||
if [ "${{ github.event.inputs.release-type }}" = "pre-release" ]; then
|
||||
npm run publish:marketplace:prerelease
|
||||
bun run publish:marketplace:prerelease
|
||||
echo "Successfully published pre-release version ${{ steps.get_version.outputs.version }} to VS Code Marketplace and Open VSX Registry"
|
||||
else
|
||||
npm run publish:marketplace
|
||||
bun run publish:marketplace
|
||||
echo "Successfully published release version ${{ steps.get_version.outputs.version }} to VS Code Marketplace and Open VSX Registry"
|
||||
fi
|
||||
|
||||
|
||||
+27
-54
@@ -24,25 +24,18 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v4
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'npm'
|
||||
cache-dependency-path: |
|
||||
package-lock.json
|
||||
webview-ui/package-lock.json
|
||||
bun-version: latest
|
||||
|
||||
- name: Install root dependencies
|
||||
run: npm ci
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install webview-ui dependencies
|
||||
run: cd webview-ui && npm ci
|
||||
|
||||
- name: Run Quality Checks (Parallel)
|
||||
run: npm run ci:check-all
|
||||
run: bun run ci:check-all
|
||||
|
||||
test:
|
||||
needs: quality-checks
|
||||
@@ -59,66 +52,54 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v4
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'npm'
|
||||
cache-dependency-path: |
|
||||
package-lock.json
|
||||
webview-ui/package-lock.json
|
||||
bun-version: latest
|
||||
|
||||
- name: Install root dependencies
|
||||
run: npm ci
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install webview-ui dependencies
|
||||
run: cd webview-ui && npm ci
|
||||
|
||||
- name: Set up NPM on Windows
|
||||
if: runner.os == 'Windows'
|
||||
run: |
|
||||
npm config set script-shell "C:\\Program Files\\Git\\bin\\bash.exe"
|
||||
|
||||
# Build the extension and tests (without redundant checks)
|
||||
- name: Build Tests and Extension
|
||||
id: build_step
|
||||
run: npm run ci:build
|
||||
run: bun run ci:build
|
||||
|
||||
- name: Unit Tests with coverage - Linux
|
||||
id: unit_tests_linux
|
||||
if: ${{ !cancelled() && steps.build_step.outcome == 'success' && runner.os == 'Linux' }}
|
||||
run: |
|
||||
npx nyc --nycrc-path .nycrc.unit.json --reporter=lcov npm run test:unit
|
||||
bunx nyc --nycrc-path .nycrc.unit.json --reporter=lcov bun run test:unit
|
||||
|
||||
- name: Unit Tests - Non-Linux
|
||||
id: unit_tests_non_linux
|
||||
if: ${{ !cancelled() && steps.build_step.outcome == 'success' && runner.os != 'Linux' }}
|
||||
run: |
|
||||
npm run test:unit
|
||||
bun run test:unit
|
||||
|
||||
- name: Extension Integration Tests - Linux
|
||||
id: integration_tests_linux
|
||||
if: ${{ !cancelled() && steps.build_step.outcome == 'success' && runner.os == 'Linux' }}
|
||||
run: xvfb-run -a npm run test:coverage
|
||||
run: xvfb-run -a bun run test:coverage
|
||||
|
||||
- name: Extension Integration Tests - Non-Linux
|
||||
id: integration_tests_non_linux
|
||||
if: ${{ !cancelled() && steps.build_step.outcome == 'success' && runner.os != 'Linux' }}
|
||||
run: npm run test:integration
|
||||
run: bun run test:integration
|
||||
|
||||
- name: Webview Tests with Coverage
|
||||
id: webview_tests
|
||||
if: ${{ !cancelled() && steps.build_step.outcome == 'success' }}
|
||||
run: |
|
||||
cd webview-ui
|
||||
npm run test:coverage
|
||||
bun run test:coverage
|
||||
|
||||
- name: CLI Tests
|
||||
id: cli_tests
|
||||
if: ${{ !cancelled() && steps.build_step.outcome == 'success' }}
|
||||
run: cd cli && npm run test:run
|
||||
run: cd cli && bun run test:run
|
||||
|
||||
- name: Save Coverage Reports
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -137,36 +118,28 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v4
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'npm'
|
||||
cache-dependency-path: |
|
||||
package-lock.json
|
||||
webview-ui/package-lock.json
|
||||
testing-platform/package-lock.json
|
||||
bun-version: latest
|
||||
|
||||
- name: Install root dependencies
|
||||
run: npm ci
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install webview-ui dependencies
|
||||
run: cd webview-ui && npm ci
|
||||
|
||||
- name: Download ripgrep binaries
|
||||
run: npm run download-ripgrep
|
||||
run: bun run download-ripgrep
|
||||
|
||||
- name: Compile Standalone
|
||||
run: npm run compile-standalone
|
||||
run: bun run compile-standalone
|
||||
|
||||
- name: Install testing platform dependencies
|
||||
run: cd testing-platform && npm ci
|
||||
run: cd testing-platform && bun install
|
||||
|
||||
- name: Running testing platform integration spec tests
|
||||
timeout-minutes: 7
|
||||
run: npm run test:tp-orchestrator -- tests/specs/ --count=1 --coverage
|
||||
run: bun run test:tp-orchestrator -- tests/specs/ --count=1 --coverage
|
||||
|
||||
- name: Save Coverage Reports
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -10,7 +10,10 @@ tmp
|
||||
.idea
|
||||
.husky/_/
|
||||
|
||||
# Package manager lock files (we use Bun, so bun.lockb is NOT ignored)
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
||||
.clineignore
|
||||
.venv
|
||||
|
||||
Vendored
+91
-50
@@ -5,8 +5,12 @@
|
||||
"tasks": [
|
||||
{
|
||||
"label": "compile-standalone",
|
||||
"type": "npm",
|
||||
"script": "compile-standalone",
|
||||
"type": "shell",
|
||||
"command": "bun",
|
||||
"args": [
|
||||
"run",
|
||||
"compile-standalone"
|
||||
],
|
||||
"group": "build",
|
||||
"problemMatcher": [],
|
||||
"presentation": {
|
||||
@@ -14,9 +18,13 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "npm: protos",
|
||||
"type": "npm",
|
||||
"script": "protos",
|
||||
"label": "bun:protos",
|
||||
"type": "shell",
|
||||
"command": "bun",
|
||||
"args": [
|
||||
"run",
|
||||
"protos"
|
||||
],
|
||||
"problemMatcher": [],
|
||||
"isBackground": false,
|
||||
"presentation": {
|
||||
@@ -31,11 +39,11 @@
|
||||
{
|
||||
"label": "watch",
|
||||
"dependsOn": [
|
||||
"npm: protos",
|
||||
"npm: build:webview",
|
||||
"npm: dev:webview",
|
||||
"npm: watch:tsc",
|
||||
"npm: watch:esbuild"
|
||||
"bun:protos",
|
||||
"bun:build:webview",
|
||||
"bun:dev:webview",
|
||||
"bun:watch:tsc",
|
||||
"bun:watch:esbuild"
|
||||
],
|
||||
"presentation": {
|
||||
"reveal": "always"
|
||||
@@ -48,11 +56,11 @@
|
||||
{
|
||||
"label": "watch:test",
|
||||
"dependsOn": [
|
||||
"npm: protos",
|
||||
"npm: build:webview:test",
|
||||
"npm: dev:webview",
|
||||
"npm: watch:tsc",
|
||||
"npm: watch:esbuild:test"
|
||||
"bun:protos",
|
||||
"bun:build:webview:test",
|
||||
"bun:dev:webview",
|
||||
"bun:watch:tsc",
|
||||
"bun:watch:esbuild:test"
|
||||
],
|
||||
"presentation": {
|
||||
"reveal": "always"
|
||||
@@ -60,14 +68,18 @@
|
||||
"group": "build"
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "build:webview",
|
||||
"type": "shell",
|
||||
"command": "bun",
|
||||
"args": [
|
||||
"run",
|
||||
"build:webview"
|
||||
],
|
||||
"group": "build",
|
||||
"problemMatcher": [],
|
||||
"isBackground": true,
|
||||
"label": "npm: build:webview",
|
||||
"label": "bun:build:webview",
|
||||
"dependsOn": [
|
||||
"npm: protos"
|
||||
"bun:protos"
|
||||
],
|
||||
"presentation": {
|
||||
"group": "watch",
|
||||
@@ -80,14 +92,18 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "build:webview:test",
|
||||
"type": "shell",
|
||||
"command": "bun",
|
||||
"args": [
|
||||
"run",
|
||||
"build:webview:test"
|
||||
],
|
||||
"group": "build",
|
||||
"problemMatcher": [],
|
||||
"isBackground": true,
|
||||
"label": "npm: build:webview:test",
|
||||
"label": "bun:build:webview:test",
|
||||
"dependsOn": [
|
||||
"npm: protos"
|
||||
"bun:protos"
|
||||
],
|
||||
"presentation": {
|
||||
"group": "watch",
|
||||
@@ -101,8 +117,12 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "dev:webview",
|
||||
"type": "shell",
|
||||
"command": "bun",
|
||||
"args": [
|
||||
"run",
|
||||
"dev:webview"
|
||||
],
|
||||
"group": "build",
|
||||
"problemMatcher": [
|
||||
{
|
||||
@@ -122,9 +142,9 @@
|
||||
}
|
||||
],
|
||||
"isBackground": true,
|
||||
"label": "npm: dev:webview",
|
||||
"label": "bun:dev:webview",
|
||||
"dependsOn": [
|
||||
"npm: protos"
|
||||
"bun:protos"
|
||||
],
|
||||
"presentation": {
|
||||
"group": "watch",
|
||||
@@ -137,8 +157,12 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "watch:esbuild",
|
||||
"type": "shell",
|
||||
"command": "bun",
|
||||
"args": [
|
||||
"run",
|
||||
"watch:esbuild"
|
||||
],
|
||||
"group": "build",
|
||||
"problemMatcher": {
|
||||
"pattern": [
|
||||
@@ -160,9 +184,9 @@
|
||||
}
|
||||
},
|
||||
"isBackground": true,
|
||||
"label": "npm: watch:esbuild",
|
||||
"label": "bun:watch:esbuild",
|
||||
"dependsOn": [
|
||||
"npm: protos"
|
||||
"bun:protos"
|
||||
],
|
||||
"presentation": {
|
||||
"group": "watch",
|
||||
@@ -175,8 +199,12 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "watch:esbuild:test",
|
||||
"type": "shell",
|
||||
"command": "bun",
|
||||
"args": [
|
||||
"run",
|
||||
"watch:esbuild:test"
|
||||
],
|
||||
"group": "build",
|
||||
"problemMatcher": {
|
||||
"pattern": [
|
||||
@@ -198,9 +226,9 @@
|
||||
}
|
||||
},
|
||||
"isBackground": true,
|
||||
"label": "npm: watch:esbuild:test",
|
||||
"label": "bun:watch:esbuild:test",
|
||||
"dependsOn": [
|
||||
"npm: protos"
|
||||
"bun:protos"
|
||||
],
|
||||
"presentation": {
|
||||
"group": "watch",
|
||||
@@ -214,14 +242,18 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "watch:tsc",
|
||||
"type": "shell",
|
||||
"command": "bun",
|
||||
"args": [
|
||||
"run",
|
||||
"watch:tsc"
|
||||
],
|
||||
"group": "build",
|
||||
"problemMatcher": "$tsc-watch",
|
||||
"isBackground": true,
|
||||
"label": "npm: watch:tsc",
|
||||
"label": "bun:watch:tsc",
|
||||
"dependsOn": [
|
||||
"npm: protos"
|
||||
"bun:protos"
|
||||
],
|
||||
"presentation": {
|
||||
"group": "watch",
|
||||
@@ -229,12 +261,17 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "watch-tests",
|
||||
"type": "shell",
|
||||
"command": "bun",
|
||||
"args": [
|
||||
"run",
|
||||
"watch-tests"
|
||||
],
|
||||
"label": "bun:watch-tests",
|
||||
"problemMatcher": "$tsc-watch",
|
||||
"isBackground": true,
|
||||
"dependsOn": [
|
||||
"npm: protos"
|
||||
"bun:protos"
|
||||
],
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
@@ -245,9 +282,9 @@
|
||||
{
|
||||
"label": "tasks: watch-tests",
|
||||
"dependsOn": [
|
||||
"npm: protos",
|
||||
"npm: watch",
|
||||
"npm: watch-tests"
|
||||
"bun:protos",
|
||||
"bun:watch",
|
||||
"bun:watch-tests"
|
||||
],
|
||||
"problemMatcher": []
|
||||
},
|
||||
@@ -265,15 +302,19 @@
|
||||
"command": "rm -rf ${workspaceFolder}/dist/tmp/user && mkdir -p ${workspaceFolder}/dist/tmp/user"
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "storybook",
|
||||
"type": "shell",
|
||||
"command": "bun",
|
||||
"args": [
|
||||
"run",
|
||||
"storybook"
|
||||
],
|
||||
"group": "build",
|
||||
"problemMatcher": [],
|
||||
"isBackground": false,
|
||||
"label": "npm: storybook",
|
||||
"label": "bun:storybook",
|
||||
"dependsOn": [
|
||||
"npm: protos",
|
||||
"npm: build:webview"
|
||||
"bun:protos",
|
||||
"bun:build:webview"
|
||||
],
|
||||
"presentation": {
|
||||
"reveal": "always"
|
||||
|
||||
@@ -1,62 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [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
|
||||
|
||||
+38
-22
@@ -34,23 +34,38 @@ We also welcome contributions to our [documentation](https://github.com/cline/cl
|
||||
|
||||
### Local Development Instructions
|
||||
|
||||
1. Clone the repository _(Requires [git-lfs](https://git-lfs.com/))_:
|
||||
> **Note**: This project uses [Bun](https://bun.sh) as the package manager.
|
||||
|
||||
1. Install Bun (if you haven't already):
|
||||
```bash
|
||||
# macOS/Linux
|
||||
curl -fsSL https://bun.sh/install | bash
|
||||
|
||||
# Windows
|
||||
powershell -c "irm bun.sh/install.ps1 | iex"
|
||||
```
|
||||
|
||||
2. Clone the repository _(Requires [git-lfs](https://git-lfs.com/))_:
|
||||
```bash
|
||||
git clone https://github.com/cline/cline.git
|
||||
```
|
||||
2. Open the project in VSCode:
|
||||
|
||||
3. Open the project in VSCode:
|
||||
```bash
|
||||
code cline
|
||||
```
|
||||
3. Install the necessary dependencies for the extension and webview-gui:
|
||||
|
||||
4. Install dependencies (installs for all workspaces):
|
||||
```bash
|
||||
npm run install:all
|
||||
bun install
|
||||
```
|
||||
4. Generate Protocol Buffer files (required before first build):
|
||||
|
||||
5. Generate Protocol Buffer files (required before first build):
|
||||
```bash
|
||||
npm run protos
|
||||
bun 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.)
|
||||
|
||||
6. 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.)
|
||||
|
||||
|
||||
|
||||
@@ -59,7 +74,7 @@ We also welcome contributions to our [documentation](https://github.com/cline/cl
|
||||
|
||||
1. Before creating a PR, generate a changeset entry:
|
||||
```bash
|
||||
npm run changeset
|
||||
bun run changeset
|
||||
```
|
||||
This will prompt you for:
|
||||
- Type of change (major, minor, patch)
|
||||
@@ -75,9 +90,10 @@ We also welcome contributions to our [documentation](https://github.com/cline/cl
|
||||
- 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
|
||||
- Run `bun run test` to run tests locally
|
||||
- Before submitting PR, run `bun run format:fix` to format your code
|
||||
|
||||
### Extension
|
||||
|
||||
@@ -88,12 +104,12 @@ We also welcome contributions to our [documentation](https://github.com/cline/cl
|
||||
- If you dismissed the prompts, you can install them manually from the Extensions panel
|
||||
|
||||
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 `bun run install:all` to install dependencies
|
||||
- Run `bun run protos` to generate Protocol Buffer files (required before first build)
|
||||
- Run `bun 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
|
||||
- **Terminal Workflow**: Use `bun run dev` (generates protos + runs watch mode) or `bun run watch` (if protos already generated)
|
||||
- Before submitting PR, run `bun run format:fix` to format your code
|
||||
|
||||
3. **Linux-specific Setup**
|
||||
VS Code extension tests on Linux require the following system libraries:
|
||||
@@ -149,8 +165,8 @@ Anyone can contribute code to Cline, but we ask that you follow these guidelines
|
||||
|
||||
2. **Code Quality**
|
||||
|
||||
- Run `npm run lint` to check code style
|
||||
- Run `npm run format` to automatically format code
|
||||
- Run `bun run lint` to check code style
|
||||
- Run `bun run format` to automatically format code
|
||||
- All PRs must pass CI checks which include both linting and formatting
|
||||
- Address any warnings or errors from linter before submitting
|
||||
- Follow TypeScript best practices and maintain type safety
|
||||
@@ -158,7 +174,7 @@ Anyone can contribute code to Cline, but we ask that you follow these guidelines
|
||||
3. **Testing**
|
||||
|
||||
- Add tests for new features
|
||||
- Run `npm test` to ensure all tests pass
|
||||
- Run `bun test` to ensure all tests pass
|
||||
- Update existing tests if your changes affect them
|
||||
- Include both unit tests and integration tests where appropriate
|
||||
|
||||
@@ -168,9 +184,9 @@ Anyone can contribute code to Cline, but we ask that you follow these guidelines
|
||||
|
||||
- **Running E2E tests:**
|
||||
```bash
|
||||
npm run test:e2e # Build and run all E2E tests
|
||||
npm run e2e # Run tests without rebuilding
|
||||
npm run test:e2e -- --debug # Run with interactive debugger
|
||||
bun run test:e2e # Build and run all E2E tests
|
||||
bun run e2e # Run tests without rebuilding
|
||||
bun run test:e2e -- --debug # Run with interactive debugger
|
||||
```
|
||||
|
||||
- **Writing E2E tests:**
|
||||
@@ -194,7 +210,7 @@ Anyone can contribute code to Cline, but we ask that you follow these guidelines
|
||||
|
||||
4. **Version Management with Changesets**
|
||||
|
||||
- Create a changeset for any user-facing changes using `npm run changeset`
|
||||
- Create a changeset for any user-facing changes using `bun 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)
|
||||
|
||||
+69
-76
@@ -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,19 +28,19 @@
|
||||
"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",
|
||||
@@ -51,36 +51,35 @@
|
||||
"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": "info"
|
||||
},
|
||||
"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"
|
||||
@@ -95,11 +94,6 @@
|
||||
"lineEnding": "lf",
|
||||
"formatWithErrors": true
|
||||
},
|
||||
"css": {
|
||||
"parser": {
|
||||
"tailwindDirectives": true
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"semicolons": "asNeeded",
|
||||
@@ -118,21 +112,21 @@
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"!**/*.lock*",
|
||||
"!**/*-lock.json"
|
||||
]
|
||||
},
|
||||
"plugins": [
|
||||
@@ -142,15 +136,14 @@
|
||||
{
|
||||
"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"
|
||||
@@ -163,37 +156,37 @@
|
||||
],
|
||||
"includes": [
|
||||
"**",
|
||||
"!!**/esbuild.*",
|
||||
"!!**/*.mts",
|
||||
"!!**/webview-ui/**",
|
||||
"!!**/evals/**",
|
||||
"!!**/standalone/**",
|
||||
"!!**/cli/**",
|
||||
"!!**/e2e/**",
|
||||
"!!**/test/**",
|
||||
"!!**/__tests__/**",
|
||||
"!!**/*.test.ts",
|
||||
"!!**/*.stories.ts",
|
||||
"!!src/dev/**",
|
||||
"!!**/*.mjs",
|
||||
"!!**/*.js",
|
||||
"!!**/scripts/**",
|
||||
"!!**/*.tsx",
|
||||
"!!**/testing-platform/**",
|
||||
"!**/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"
|
||||
"!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"
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
# Bun configuration for Cline monorepo
|
||||
|
||||
[install]
|
||||
# Use npm for package resolution compatibility
|
||||
registry = "https://registry.npmjs.org/"
|
||||
|
||||
# Cache configuration
|
||||
[install.cache]
|
||||
# Enable caching for faster installs
|
||||
disable = false
|
||||
|
||||
[install.scopes]
|
||||
# Configure scoped registries if needed
|
||||
# "@myorg" = "https://registry.example.com/"
|
||||
|
||||
[test]
|
||||
# Test configuration
|
||||
preload = []
|
||||
@@ -1,62 +0,0 @@
|
||||
# cline
|
||||
|
||||
## [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.
|
||||
+25
-25
@@ -13,7 +13,7 @@ The official CLI for Cline. Run Cline tasks directly from the terminal with the
|
||||
## Prerequisites
|
||||
|
||||
- Node.js 20.x or later
|
||||
- npm or yarn
|
||||
- bun or npm or yarn
|
||||
- The parent Cline project dependencies installed
|
||||
|
||||
## Installation
|
||||
@@ -22,13 +22,13 @@ From the repository root:
|
||||
|
||||
```bash
|
||||
# Install all dependencies first
|
||||
npm run install:all
|
||||
bun install
|
||||
|
||||
# Ensure protos are generated
|
||||
npm run protos
|
||||
bun run protos
|
||||
|
||||
# Build and link the CLI globally
|
||||
npm run cli:link
|
||||
bun run cli:link
|
||||
```
|
||||
|
||||
## Usage
|
||||
@@ -192,10 +192,10 @@ These options are available for the default command (running a task directly):
|
||||
|
||||
```bash
|
||||
# 1. Install all dependencies (root, webview-ui, cli)
|
||||
npm run install:all
|
||||
bun install
|
||||
|
||||
# 2. Build and link globally so you can run `cline` from anywhere
|
||||
npm run cli:link
|
||||
bun run cli:link
|
||||
|
||||
# 3. Test it
|
||||
cline --help
|
||||
@@ -207,29 +207,29 @@ 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 |
|
||||
| `bun install` | Install deps for root, webview-ui, and cli |
|
||||
| `bun run cli:build` | Generate protos and build CLI |
|
||||
| `bun run cli:build:production` | Production build (minified) |
|
||||
| `bun run cli:link` | Build and `bun link` so you can run `cline` from anywhere |
|
||||
| `bun run cli:unlink` | Remove the global `cline` symlink |
|
||||
| `bun run cli:dev` | Link + watch mode for development |
|
||||
| `bun run cli:watch` | Watch mode only (no initial build) |
|
||||
| `bun run cli:test` | Run CLI tests |
|
||||
|
||||
### Development Workflow
|
||||
|
||||
1. Run `npm run cli:dev` - this links the CLI globally and starts watch mode
|
||||
1. Run `bun 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
|
||||
5. When done, run `bun 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
|
||||
bun run protos
|
||||
```
|
||||
|
||||
This generates TypeScript types in `src/generated/` that both the CLI and extension use.
|
||||
@@ -238,12 +238,12 @@ This generates TypeScript types in `src/generated/` that both the CLI and extens
|
||||
|
||||
#### 1. Publish to npm
|
||||
```bash
|
||||
npm publish
|
||||
bun publish
|
||||
```
|
||||
|
||||
#### 2. Update the Homebrew formula
|
||||
```bash
|
||||
npm run update-brew-formula
|
||||
bun run update-brew-formula
|
||||
```
|
||||
|
||||
#### 3. Test the formula locally
|
||||
@@ -335,13 +335,13 @@ If you encounter build errors:
|
||||
|
||||
```bash
|
||||
# Make sure all deps are installed
|
||||
npm run install:all
|
||||
bun install
|
||||
|
||||
# Regenerate proto types
|
||||
npm run protos
|
||||
bun run protos
|
||||
|
||||
# Then rebuild
|
||||
npm run cli:build
|
||||
bun run cli:build
|
||||
```
|
||||
|
||||
### "command not found: cline"
|
||||
@@ -349,16 +349,16 @@ npm run cli:build
|
||||
The CLI isn't linked globally. Run:
|
||||
|
||||
```bash
|
||||
npm run cli:link
|
||||
bun 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`)
|
||||
1. Make sure watch mode is running (`bun 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`
|
||||
3. Try unlinking and relinking: `bun run cli:unlink && bun run cli:link`
|
||||
|
||||
### Import Errors from Core
|
||||
|
||||
|
||||
+13
-35
@@ -88,10 +88,6 @@ directory
|
||||
\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
|
||||
@@ -183,10 +179,6 @@ the task
|
||||
.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:
|
||||
@@ -282,21 +274,6 @@ 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
|
||||
@@ -371,19 +348,20 @@ export CLINE_COMMAND_PERMISSIONS=\(aq{\(dqallow\(dq: [\(dqnpm *\(dq, \(dqgit *\(
|
||||
\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
|
||||
.SH FILES
|
||||
\f[B]\(ti/.cline/data/\f[R] : Default configuration directory
|
||||
containing:
|
||||
.PP
|
||||
View logs with \f[CR]cline dev log\f[R].
|
||||
\f[B]globalState.json\f[R] : Global settings and state
|
||||
.PP
|
||||
\f[B]secrets.json\f[R] : API keys and secrets (stored securely)
|
||||
.PP
|
||||
\f[B]workspace/\f[R] : Workspace\-specific state
|
||||
.PP
|
||||
\f[B]tasks/\f[R] : Task history and conversation data
|
||||
.PP
|
||||
\f[B]\(ti/.cline/log/\f[R] : Log files for debugging.
|
||||
View with \f[CR]cline dev log\f[R].
|
||||
.SH BUGS
|
||||
Report bugs at: \c
|
||||
.UR https://github.com/cline/cline/issues
|
||||
|
||||
+4
-24
@@ -70,8 +70,6 @@ Run a new task with a prompt.
|
||||
|
||||
**\--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.
|
||||
@@ -118,7 +116,7 @@ Authenticate a provider and configure the model.
|
||||
|
||||
Check for updates and install if available.
|
||||
|
||||
**cline update** [*options*] : Check npm for newer versions. Options:
|
||||
**cline update** [*options*] : Check bun for newer versions. Options:
|
||||
|
||||
**-v**, **\--verbose** : Show verbose output
|
||||
|
||||
@@ -156,8 +154,6 @@ When running **cline** with just a prompt (no subcommand), these options are ava
|
||||
|
||||
**\--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.
|
||||
|
||||
# JSON OUTPUT FORMAT
|
||||
|
||||
When using **\--json**, each message is output as a JSON object with these fields:
|
||||
@@ -255,22 +251,6 @@ cline history
|
||||
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 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
|
||||
@@ -313,11 +293,11 @@ Format: `{"allow": ["pattern1", "pattern2"], "deny": ["pattern3"], "allowRedirec
|
||||
**Examples:**
|
||||
|
||||
```bash
|
||||
# Allow only npm and git commands.
|
||||
export CLINE_COMMAND_PERMISSIONS='{"allow": ["npm *", "git *"]}'
|
||||
# Allow only bun and git commands.
|
||||
export CLINE_COMMAND_PERMISSIONS='{"allow": ["bun *", "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 *"]}'
|
||||
export CLINE_COMMAND_PERMISSIONS='{"allow": ["bun *", "git *", "node *"], "deny": ["rm -rf *", "sudo *"]}'
|
||||
|
||||
# Allow file operations with redirects
|
||||
export CLINE_COMMAND_PERMISSIONS='{"allow": ["cat *", "echo *"], "allowRedirects": true}'
|
||||
|
||||
+10
-10
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cline",
|
||||
"version": "2.2.0",
|
||||
"version": "2.0.3",
|
||||
"description": "Autonomous coding agent CLI - capable of creating/editing files, running commands, using the browser, and more",
|
||||
"main": "dist/cli.mjs",
|
||||
"bin": {
|
||||
@@ -21,16 +21,16 @@
|
||||
"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",
|
||||
"build:production": "npm run typecheck && npx tsx esbuild.mts --production",
|
||||
"watch": "npx tsx esbuild.mts --watch",
|
||||
"dev": "IS_DEV=true && npm run link && npm run watch ; npm run unlink",
|
||||
"package:brew": "bunx tsx ./scripts/update-brew-formula.mts",
|
||||
"package": "bun pm pack --pack-destination ./dist",
|
||||
"build": "bun run typecheck && bunx tsx esbuild.mts",
|
||||
"build:production": "bun run typecheck && bunx tsx esbuild.mts --production",
|
||||
"watch": "bunx tsx esbuild.mts --watch",
|
||||
"dev": "IS_DEV=true && bun run link && bun run watch ; bun run unlink",
|
||||
"clean": "rimraf dist",
|
||||
"typecheck": "npx tsc --noEmit",
|
||||
"link": "npm run build && npm link",
|
||||
"unlink": "npm unlink -g cline",
|
||||
"typecheck": "bunx tsc --noEmit",
|
||||
"link": "bun run build && bun link",
|
||||
"unlink": "bun unlink",
|
||||
"test": "vitest",
|
||||
"test:run": "vitest run"
|
||||
},
|
||||
|
||||
@@ -172,16 +172,6 @@ class ACPEnvServiceClient implements EnvServiceClientInterface {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,7 +12,11 @@
|
||||
|
||||
import type * as acp from "@agentclientprotocol/sdk"
|
||||
import type { TerminalHandle } from "@agentclientprotocol/sdk"
|
||||
import { DEFAULT_TERMINAL_OUTPUT_LINE_LIMIT, PROCESS_HOT_TIMEOUT_NORMAL } from "@integrations/terminal/constants"
|
||||
import {
|
||||
DEFAULT_SUBAGENT_TERMINAL_OUTPUT_LINE_LIMIT,
|
||||
DEFAULT_TERMINAL_OUTPUT_LINE_LIMIT,
|
||||
PROCESS_HOT_TIMEOUT_NORMAL,
|
||||
} from "@integrations/terminal/constants"
|
||||
import type {
|
||||
ITerminal,
|
||||
ITerminalManager,
|
||||
@@ -138,12 +142,12 @@ export interface ManagedTerminal {
|
||||
* Wraps ACP terminal operations and emits events compatible with ITerminalProcess.
|
||||
*/
|
||||
class AcpTerminalProcess extends EventEmitter<TerminalProcessEvents> implements ITerminalProcess {
|
||||
isHot = false
|
||||
waitForShellIntegration = false
|
||||
isHot: boolean = false
|
||||
waitForShellIntegration: boolean = false
|
||||
|
||||
private _unretrievedOutput = ""
|
||||
private _continued = false
|
||||
private _completed = false
|
||||
private _unretrievedOutput: string = ""
|
||||
private _continued: boolean = false
|
||||
private _completed: boolean = false
|
||||
private _hotTimeout: NodeJS.Timeout | null = null
|
||||
private _exitWaitTimeout: NodeJS.Timeout | null = null
|
||||
private readonly manager: AcpTerminalManager
|
||||
@@ -393,7 +397,7 @@ export class AcpTerminalManager implements ITerminalManager {
|
||||
private readonly numericIdToStringId: Map<number, string> = new Map()
|
||||
|
||||
/** Next numeric ID to assign */
|
||||
private nextNumericId = 1
|
||||
private nextNumericId: number = 1
|
||||
|
||||
/** Active processes indexed by numeric terminal ID */
|
||||
private readonly processes: Map<number, AcpTerminalProcess> = new Map()
|
||||
@@ -402,8 +406,9 @@ export class AcpTerminalManager implements ITerminalManager {
|
||||
private readonly terminalInfos: Map<number, TerminalInfo> = new Map()
|
||||
|
||||
// Configuration options for ITerminalManager
|
||||
private terminalReuseEnabled = true
|
||||
private terminalReuseEnabled: boolean = true
|
||||
private terminalOutputLineLimit: number = DEFAULT_TERMINAL_OUTPUT_LINE_LIMIT
|
||||
private subagentTerminalOutputLineLimit: number = DEFAULT_SUBAGENT_TERMINAL_OUTPUT_LINE_LIMIT
|
||||
|
||||
/**
|
||||
* Creates a new AcpTerminalManager.
|
||||
@@ -662,6 +667,14 @@ export class AcpTerminalManager implements ITerminalManager {
|
||||
this.terminalOutputLineLimit = limit
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the maximum number of output lines for subagent commands.
|
||||
* @param limit Maximum number of lines
|
||||
*/
|
||||
setSubagentTerminalOutputLineLimit(limit: number): void {
|
||||
this.subagentTerminalOutputLineLimit = limit
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default terminal profile.
|
||||
* @param profile The profile identifier
|
||||
@@ -674,10 +687,15 @@ export class AcpTerminalManager implements ITerminalManager {
|
||||
* Process output lines, potentially truncating if over limit.
|
||||
* @param outputLines Array of output lines
|
||||
* @param overrideLimit Optional limit override
|
||||
* @param isSubagentCommand Whether this is a subagent command
|
||||
* @returns Processed output string
|
||||
*/
|
||||
processOutput(outputLines: string[], overrideLimit?: number): string {
|
||||
const limit = overrideLimit !== undefined ? overrideLimit : this.terminalOutputLineLimit
|
||||
processOutput(outputLines: string[], overrideLimit?: number, isSubagentCommand?: boolean): string {
|
||||
const limit = isSubagentCommand
|
||||
? overrideLimit !== undefined
|
||||
? overrideLimit
|
||||
: this.subagentTerminalOutputLineLimit
|
||||
: this.terminalOutputLineLimit
|
||||
|
||||
if (outputLines.length > limit) {
|
||||
const halfLimit = Math.floor(limit / 2)
|
||||
|
||||
@@ -173,7 +173,7 @@ export class ClineAgent implements acp.Agent {
|
||||
async initialize(params: acp.InitializeRequest, connection?: acp.AgentSideConnection): Promise<acp.InitializeResponse> {
|
||||
this.clientCapabilities = params.clientCapabilities
|
||||
this.initializeHostProvider(this.clientCapabilities, connection)
|
||||
await ClineEndpoint.initialize(this.ctx.EXTENSION_DIR)
|
||||
await ClineEndpoint.initialize()
|
||||
await StateManager.initialize(this.ctx.extensionContext)
|
||||
|
||||
return {
|
||||
@@ -246,8 +246,8 @@ export class ClineAgent implements acp.Agent {
|
||||
},
|
||||
hostBridgeClientProvider,
|
||||
(message: string) => Logger.info(message),
|
||||
async (path: string) => {
|
||||
return AuthHandler.getInstance().getCallbackUrl(path)
|
||||
async () => {
|
||||
return AuthHandler.getInstance().getCallbackUrl()
|
||||
},
|
||||
async () => "", // get binary location not needed in ACP mode
|
||||
this.ctx.EXTENSION_DIR,
|
||||
@@ -973,7 +973,7 @@ export class ClineAgent implements acp.Agent {
|
||||
// Get the callback URL first to ensure the server is ready
|
||||
let callbackUrl: string
|
||||
try {
|
||||
callbackUrl = await authHandler.getCallbackUrl("/auth")
|
||||
callbackUrl = await authHandler.getCallbackUrl()
|
||||
Logger.debug("[ClineAgent] Callback URL ready:", callbackUrl)
|
||||
} catch (error) {
|
||||
Logger.error("[ClineAgent] Failed to get callback URL:", error)
|
||||
|
||||
@@ -312,10 +312,6 @@ function translateSayMessage(
|
||||
// API request finished - no specific update needed
|
||||
break
|
||||
|
||||
case "subagent_usage":
|
||||
// Hidden aggregate metrics event used for task-level accounting.
|
||||
break
|
||||
|
||||
case "task":
|
||||
// Task started - don't echo the user's prompt back to them
|
||||
// The ACP client already knows what they typed
|
||||
|
||||
@@ -34,7 +34,7 @@ type AsciiMotionCliProps = {
|
||||
autoPlay?: boolean;
|
||||
loop?: boolean;
|
||||
onReady?: (api: PlaybackAPI) => void;
|
||||
onInteraction?: () => void; // Called when user scrolls, clicks, or drags
|
||||
onScroll?: () => void; // Called when user scrolls (scroll wheel)
|
||||
};
|
||||
|
||||
const FRAMES: FrameData[] = [
|
||||
@@ -333364,7 +333364,7 @@ const FRAME_BOTTOM_RIGHT = 128;
|
||||
|
||||
export const AsciiMotionCli: React.FC<AsciiMotionCliProps> = ({
|
||||
hasDarkBackground = true,
|
||||
onInteraction,
|
||||
onScroll,
|
||||
}) => {
|
||||
const [frameIndex, setFrameIndex] = useState(0);
|
||||
const [targetFrame, setTargetFrame] = useState(0);
|
||||
@@ -333390,13 +333390,13 @@ export const AsciiMotionCli: React.FC<AsciiMotionCliProps> = ({
|
||||
// Stop animation on terminal resize to prevent visual glitches
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
onInteraction?.();
|
||||
onScroll?.();
|
||||
};
|
||||
process.stdout.on("resize", handleResize);
|
||||
return () => {
|
||||
process.stdout.off("resize", handleResize);
|
||||
};
|
||||
}, [onInteraction]);
|
||||
}, [onScroll]);
|
||||
|
||||
// Mouse tracking - gracefully handle environments without tty support
|
||||
useEffect(() => {
|
||||
@@ -333417,19 +333417,13 @@ export const AsciiMotionCli: React.FC<AsciiMotionCliProps> = ({
|
||||
const handleData = (data: Buffer) => {
|
||||
const str = data.toString();
|
||||
|
||||
// Parse mouse events: \x1b[<button;x;yM (M=press, m=release)
|
||||
// Parse mouse events: \x1b[<button;x;yM
|
||||
const mouseMatch = str.match(/\x1b\[<(\d+);(\d+);(\d+)([Mm])/);
|
||||
if (mouseMatch) {
|
||||
const button = parseInt(mouseMatch[1], 10);
|
||||
const isPress = mouseMatch[4] === "M";
|
||||
// Button 64/65 = scroll up/down
|
||||
// Button 0-2 = left/middle/right click (on press)
|
||||
// Button 32-34 = drag with left/middle/right button held
|
||||
const isScroll = button === 64 || button === 65;
|
||||
const isClick = isPress && button >= 0 && button <= 2;
|
||||
const isDrag = button >= 32 && button <= 34;
|
||||
if (isScroll || isClick || isDrag) {
|
||||
onInteraction?.();
|
||||
// Button 64 = scroll up, 65 = scroll down
|
||||
if (button === 64 || button === 65) {
|
||||
onScroll?.();
|
||||
}
|
||||
// Throttle cursor updates to ~20fps to reduce re-renders
|
||||
const now = Date.now();
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
import { Box, Text, useApp, useInput } from "ink"
|
||||
import Spinner from "ink-spinner"
|
||||
// biome-ignore lint/style/useImportType: React is used as a value by JSX (jsx: "react" in tsconfig)
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react"
|
||||
import { StateManager } from "@/core/storage/StateManager"
|
||||
import { openAiCodexOAuthManager } from "@/integrations/openai-codex/oauth"
|
||||
@@ -13,13 +12,13 @@ import { AuthService } from "@/services/auth/AuthService"
|
||||
import { liteLlmDefaultModelId, openAiCodexDefaultModelId, openRouterDefaultModelId } from "@/shared/api"
|
||||
import { openExternal } from "@/utils/env"
|
||||
import { COLORS } from "../constants/colors"
|
||||
import { getAllFeaturedModels } from "../constants/featured-models"
|
||||
import { useStdinContext } from "../context/StdinContext"
|
||||
import { useOcaAuth } from "../hooks/useOcaAuth"
|
||||
import { useScrollableList } from "../hooks/useScrollableList"
|
||||
import { type DetectedSources, detectImportSources, type ImportSource } from "../utils/import-configs"
|
||||
import { 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 { type BedrockConfig, BedrockSetup } from "./BedrockSetup"
|
||||
@@ -31,7 +30,7 @@ import {
|
||||
} from "./FeaturedModelPicker"
|
||||
import { ImportView } from "./ImportView"
|
||||
import { getDefaultModelId, hasModelPicker, ModelPicker } from "./ModelPicker"
|
||||
import { getProviderLabel } from "./ProviderPicker"
|
||||
import { CLI_EXCLUDED_PROVIDERS, getProviderLabel, getProviderOrder } from "./ProviderPicker"
|
||||
|
||||
type AuthStep =
|
||||
| "menu"
|
||||
@@ -49,6 +48,9 @@ type AuthStep =
|
||||
| "bedrock"
|
||||
| "import"
|
||||
|
||||
// Featured models loaded from shared constants
|
||||
const featuredModels = getAllFeaturedModels()
|
||||
|
||||
interface AuthViewProps {
|
||||
controller: any
|
||||
onComplete?: () => void
|
||||
@@ -147,9 +149,6 @@ const TextInput: React.FC<{
|
||||
|
||||
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 ||
|
||||
@@ -191,6 +190,11 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
onError: handleOcaAuthError,
|
||||
})
|
||||
|
||||
// Use providers.json order, filtered to exclude CLI-incompatible providers
|
||||
const sortedProviders = useMemo(() => {
|
||||
return getProviderOrder().filter((p) => !CLI_EXCLUDED_PROVIDERS.has(p))
|
||||
}, [])
|
||||
|
||||
// Main menu items - conditionally include import options
|
||||
const mainMenuItems: SelectItem[] = useMemo(() => {
|
||||
const items: SelectItem[] = [{ label: "Sign in with Cline", value: "cline_auth" }]
|
||||
@@ -216,13 +220,15 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
const providerItems: SelectItem[] = useMemo(() => {
|
||||
const search = providerSearch.toLowerCase()
|
||||
const filtered = providerSearch
|
||||
? providers.filter((p) => p.toLowerCase().includes(search) || getProviderLabel(p).toLowerCase().includes(search))
|
||||
: providers
|
||||
? sortedProviders.filter(
|
||||
(p) => p.toLowerCase().includes(search) || getProviderLabel(p).toLowerCase().includes(search),
|
||||
)
|
||||
: sortedProviders
|
||||
return filtered.map((p: string) => ({
|
||||
label: getProviderLabel(p),
|
||||
value: p,
|
||||
}))
|
||||
}, [providers, providerSearch])
|
||||
}, [sortedProviders, providerSearch])
|
||||
|
||||
// Use shared scrollable list hook for provider windowing
|
||||
const TOTAL_PROVIDER_ROWS = 8
|
||||
@@ -858,7 +864,7 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
|
||||
{index === menuIndex ? "❯ " : " "}
|
||||
{item.label}
|
||||
</Text>
|
||||
{item.value === "cline_auth" && <Text color="yellow"> (try Opus 4.6!)</Text>}
|
||||
{item.value === "cline_auth" && <Text color="yellow"> (try Kimi K2.5 free!)</Text>}
|
||||
</Text>
|
||||
</Box>
|
||||
))}
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
import type { ClineMessage } from "@shared/ExtensionMessage"
|
||||
import { render } from "ink-testing-library"
|
||||
import React from "react"
|
||||
import { describe, expect, it, vi } from "vitest"
|
||||
import { ChatMessage } from "./ChatMessage"
|
||||
|
||||
vi.mock("../hooks/useTerminalSize", () => ({
|
||||
useTerminalSize: () => ({
|
||||
columns: 120,
|
||||
rows: 40,
|
||||
resizeKey: 0,
|
||||
}),
|
||||
}))
|
||||
|
||||
describe("ChatMessage subagent rendering", () => {
|
||||
it("renders subagent approval prompts as a tree", () => {
|
||||
const message: ClineMessage = {
|
||||
ts: Date.now(),
|
||||
type: "ask",
|
||||
ask: "use_subagents",
|
||||
text: JSON.stringify({
|
||||
prompts: [
|
||||
"Find codebase stats and size",
|
||||
"Find funny comments and easter eggs",
|
||||
"Find unusual patterns and history",
|
||||
],
|
||||
}),
|
||||
}
|
||||
|
||||
const { lastFrame } = render(React.createElement(ChatMessage, { message, mode: "act" }))
|
||||
const frame = lastFrame() || ""
|
||||
|
||||
expect(frame).toContain("Cline wants to run subagents")
|
||||
expect(frame).toContain("├─ Find codebase stats and size")
|
||||
expect(frame).toContain("├─ Find funny comments and easter eggs")
|
||||
expect(frame).toContain("└─ Find unusual patterns and history")
|
||||
})
|
||||
|
||||
it("renders subagent progress rows with compact token stats and completion checks", () => {
|
||||
const message: ClineMessage = {
|
||||
ts: Date.now(),
|
||||
type: "say",
|
||||
say: "subagent",
|
||||
text: JSON.stringify({
|
||||
status: "running",
|
||||
total: 3,
|
||||
completed: 1,
|
||||
successes: 1,
|
||||
failures: 0,
|
||||
toolCalls: 21,
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
contextWindow: 0,
|
||||
maxContextTokens: 0,
|
||||
maxContextUsagePercentage: 0,
|
||||
items: [
|
||||
{
|
||||
index: 1,
|
||||
prompt: "Find codebase stats and size",
|
||||
status: "completed",
|
||||
toolCalls: 5,
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
totalCost: 0.034,
|
||||
contextTokens: 24400,
|
||||
contextWindow: 200000,
|
||||
contextUsagePercentage: 12.2,
|
||||
},
|
||||
{
|
||||
index: 2,
|
||||
prompt: "Find funny comments and easter eggs",
|
||||
status: "running",
|
||||
toolCalls: 11,
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
totalCost: 0.056,
|
||||
contextTokens: 31600,
|
||||
contextWindow: 200000,
|
||||
contextUsagePercentage: 15.8,
|
||||
},
|
||||
{
|
||||
index: 3,
|
||||
prompt: "Find unusual patterns and history",
|
||||
status: "pending",
|
||||
toolCalls: 5,
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
totalCost: 0,
|
||||
contextTokens: 28900,
|
||||
contextWindow: 200000,
|
||||
contextUsagePercentage: 14.4,
|
||||
},
|
||||
],
|
||||
}),
|
||||
}
|
||||
|
||||
const { lastFrame } = render(React.createElement(ChatMessage, { isStreaming: true, message, mode: "act" }))
|
||||
const frame = lastFrame() || ""
|
||||
|
||||
expect(frame).toContain("Cline is running subagents")
|
||||
expect(frame).toContain("✓ Find codebase stats and size")
|
||||
expect(frame).toContain("5 tool uses · 24.4k tokens · $0.03")
|
||||
expect(frame).toContain("11 tool uses · 31.6k tokens · $0.06")
|
||||
expect(frame).toContain("5 tool uses · 28.9k tokens · $0.00")
|
||||
})
|
||||
})
|
||||
@@ -17,7 +17,6 @@ import { useTerminalSize } from "../hooks/useTerminalSize"
|
||||
import { jsonParseSafe } from "../utils/parser"
|
||||
import { getToolDescription, isFileEditTool, parseToolFromMessage } from "../utils/tools"
|
||||
import { DiffView } from "./DiffView"
|
||||
import { SubagentMessage } from "./SubagentMessage"
|
||||
|
||||
/**
|
||||
* Add "(Tab)" hint after "Act mode" mentions.
|
||||
@@ -25,7 +24,7 @@ import { SubagentMessage } from "./SubagentMessage"
|
||||
* Matches just "Act mode" without requiring "to " prefix because markdown
|
||||
* processing may split "toggle to **Act mode**" into separate text chunks.
|
||||
*/
|
||||
function addActModeHint(text: string, keyPrefix: string): React.ReactNode[] {
|
||||
function addActModeHint(text: string): React.ReactNode[] {
|
||||
// Match "Act mode" in various capitalizations, but not if already followed by (Tab)
|
||||
const actModeRegex = /\bact\s+mode\b(?!\s*\(tab\))/gi
|
||||
const parts = text.split(actModeRegex)
|
||||
@@ -42,7 +41,7 @@ function addActModeHint(text: string, keyPrefix: string): React.ReactNode[] {
|
||||
}
|
||||
if (matches[i]) {
|
||||
nodes.push(
|
||||
<React.Fragment key={`${keyPrefix}-act-mode-${i}`}>
|
||||
<React.Fragment key={`act-mode-${i}`}>
|
||||
{matches[i]}
|
||||
<Text color="gray"> (Tab)</Text>
|
||||
</React.Fragment>,
|
||||
@@ -60,8 +59,6 @@ function addActModeHint(text: string, keyPrefix: string): React.ReactNode[] {
|
||||
*/
|
||||
function renderInlineMarkdown(text: string): React.ReactNode[] {
|
||||
const nodes: React.ReactNode[] = []
|
||||
let hintCallIndex = 0
|
||||
const addHintedText = (value: string) => addActModeHint(value, `hint-${hintCallIndex++}`)
|
||||
// Match **bold**, *italic*, or `code` - order matters (** before *)
|
||||
const regex = /(\*\*[^*]+\*\*|\*[^*]+\*|`[^`]+`)/g
|
||||
let lastIndex = 0
|
||||
@@ -71,7 +68,7 @@ function renderInlineMarkdown(text: string): React.ReactNode[] {
|
||||
// Add text before match (with Act Mode hint processing)
|
||||
if (match.index > lastIndex) {
|
||||
const beforeText = text.slice(lastIndex, match.index)
|
||||
nodes.push(...addHintedText(beforeText))
|
||||
nodes.push(...addActModeHint(beforeText))
|
||||
}
|
||||
|
||||
const fullMatch = match[0]
|
||||
@@ -80,7 +77,7 @@ function renderInlineMarkdown(text: string): React.ReactNode[] {
|
||||
if (fullMatch.startsWith("**") && fullMatch.endsWith("**")) {
|
||||
// Bold - also process for Act Mode hints inside bold text
|
||||
const boldContent = fullMatch.slice(2, -2)
|
||||
const hintedContent = addHintedText(boldContent)
|
||||
const hintedContent = addActModeHint(boldContent)
|
||||
nodes.push(
|
||||
<Text bold key={key}>
|
||||
{hintedContent}
|
||||
@@ -103,10 +100,10 @@ function renderInlineMarkdown(text: string): React.ReactNode[] {
|
||||
|
||||
// Add remaining text (with Act Mode hint processing)
|
||||
if (lastIndex < text.length) {
|
||||
nodes.push(...addHintedText(text.slice(lastIndex)))
|
||||
nodes.push(...addActModeHint(text.slice(lastIndex)))
|
||||
}
|
||||
|
||||
return nodes.length > 0 ? nodes : addHintedText(text)
|
||||
return nodes.length > 0 ? nodes : addActModeHint(text)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -227,7 +224,7 @@ function truncate(text: string, maxLength: number): string {
|
||||
/**
|
||||
* Format tool result for display
|
||||
*/
|
||||
function formatToolResult(result: string, maxLines = 5): string[] {
|
||||
function formatToolResult(result: string, maxLines: number = 5): string[] {
|
||||
const lines = result.split("\n")
|
||||
if (lines.length <= maxLines) {
|
||||
return lines
|
||||
@@ -449,10 +446,6 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({ message, mode, isStrea
|
||||
)
|
||||
}
|
||||
|
||||
if ((type === "ask" && ask === "use_subagents") || say === "use_subagents" || say === "subagent") {
|
||||
return <SubagentMessage isStreaming={isStreaming} message={message} mode={mode} />
|
||||
}
|
||||
|
||||
// MCP response
|
||||
if (say === "mcp_server_response" && text) {
|
||||
const lines = formatToolResult(text, 8)
|
||||
@@ -807,8 +800,6 @@ export const ChatMessageList: React.FC<ChatMessageListProps> = ({ messages, maxM
|
||||
const displayMessages = messages.filter((m) => {
|
||||
// Skip api_req_finished, they're just markers
|
||||
if (m.say === "api_req_finished") return false
|
||||
// Skip hidden aggregated usage messages
|
||||
if (m.say === "subagent_usage") return false
|
||||
// Skip empty text messages
|
||||
if (m.say === "text" && !m.text?.trim()) return false
|
||||
// Skip checkpoint messages
|
||||
|
||||
+31
-119
@@ -109,11 +109,10 @@ import { getApiMetrics, getLastApiReqTotalTokens } from "@shared/getApiMetrics"
|
||||
import { EmptyRequest, StringRequest } from "@shared/proto/cline/common"
|
||||
import type { SlashCommandInfo } from "@shared/proto/cline/slash"
|
||||
import { CLI_ONLY_COMMANDS } from "@shared/slashCommands"
|
||||
import { getProviderDefaultModelId, getProviderModelIdKey } from "@shared/storage"
|
||||
import { getProviderModelIdKey } from "@shared/storage"
|
||||
import type { Mode } from "@shared/storage/types"
|
||||
import { execSync } from "child_process"
|
||||
import { Box, Static, Text, useApp, useInput } from "ink"
|
||||
// biome-ignore lint/style/useImportType: JSX requires React as a value (jsx: "react" in tsconfig)
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { getAvailableSlashCommands } from "@/core/controller/slash/getAvailableSlashCommands"
|
||||
import { showTaskWithId } from "@/core/controller/task/showTaskWithId"
|
||||
@@ -138,7 +137,6 @@ import {
|
||||
import { isMouseEscapeSequence } from "../utils/input"
|
||||
import { jsonParseSafe, parseImagesFromInput } from "../utils/parser"
|
||||
import { extractSlashQuery, filterCommands, insertSlashCommand, sortCommandsWorkflowsFirst } from "../utils/slash-commands"
|
||||
import { waitFor } from "../utils/timeout"
|
||||
import { isFileEditTool, parseToolFromMessage } from "../utils/tools"
|
||||
import { shutdownEvent } from "../vscode-shim"
|
||||
import { ActionButtons, type ButtonActionType, getButtonConfig, getVisibleButtons } from "./ActionButtons"
|
||||
@@ -153,24 +151,6 @@ import { SettingsPanelContent } from "./SettingsPanelContent"
|
||||
import { SlashCommandMenu } from "./SlashCommandMenu"
|
||||
import { ThinkingIndicator } from "./ThinkingIndicator"
|
||||
|
||||
/**
|
||||
* Persistent input storage that survives React remounts (e.g., during terminal resize).
|
||||
* Keyed by a stable identifier so each task/session maintains its own input state.
|
||||
*/
|
||||
interface PersistedInputState {
|
||||
text: string
|
||||
cursorPos: number
|
||||
pastedTexts: Map<number, string>
|
||||
pasteCounter: number
|
||||
}
|
||||
|
||||
const inputStateStorage = new Map<string, PersistedInputState>()
|
||||
|
||||
function getInputStorageKey(controller: any, taskId?: string): string {
|
||||
// Use taskId if available, otherwise fall back to controller instance
|
||||
return taskId || (controller?.task?.taskId ?? "default")
|
||||
}
|
||||
|
||||
interface ChatViewProps {
|
||||
controller?: any
|
||||
onExit?: () => void
|
||||
@@ -229,9 +209,9 @@ function getGitDiffStats(cwd?: string): GitDiffStats | null {
|
||||
const delMatch = output.match(/(\d+) deletion/)
|
||||
|
||||
return {
|
||||
files: filesMatch ? Number.parseInt(filesMatch[1], 10) : 0,
|
||||
additions: addMatch ? Number.parseInt(addMatch[1], 10) : 0,
|
||||
deletions: delMatch ? Number.parseInt(delMatch[1], 10) : 0,
|
||||
files: filesMatch ? parseInt(filesMatch[1], 10) : 0,
|
||||
additions: addMatch ? parseInt(addMatch[1], 10) : 0,
|
||||
deletions: delMatch ? parseInt(delMatch[1], 10) : 0,
|
||||
}
|
||||
} catch {
|
||||
return null
|
||||
@@ -242,7 +222,7 @@ function getGitDiffStats(cwd?: string): GitDiffStats | null {
|
||||
* Create a progress bar for context window usage
|
||||
* Returns { filled, empty } strings to allow different coloring
|
||||
*/
|
||||
function createContextBar(used: number, total: number, width = 8): { filled: string; empty: string } {
|
||||
function createContextBar(used: number, total: number, width: number = 8): { filled: string; empty: string } {
|
||||
const ratio = Math.min(used / total, 1)
|
||||
// Use ceil so any usage > 0 shows at least one bar
|
||||
const filledCount = used > 0 ? Math.max(1, Math.ceil(ratio * width)) : 0
|
||||
@@ -332,7 +312,7 @@ function parseAskOptions(text: string): string[] {
|
||||
*/
|
||||
function expandPastedTexts(text: string, pastedTexts: Map<number, string>): string {
|
||||
return text.replace(/\[Pasted text #(\d+) \+\d+ lines\]/g, (match, num) => {
|
||||
const content = pastedTexts.get(Number.parseInt(num, 10))
|
||||
const content = pastedTexts.get(parseInt(num, 10))
|
||||
return content ?? match
|
||||
})
|
||||
}
|
||||
@@ -369,14 +349,9 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
insertText: insertTextAtCursor,
|
||||
} = useTextInput()
|
||||
|
||||
// Get storage key for persisting input across remounts
|
||||
const storageKey = useMemo(() => getInputStorageKey(ctrl, taskId), [ctrl, taskId])
|
||||
|
||||
// Refs for text input and cursor position (used by useHomeEndKeys and to avoid stale closures in useInput)
|
||||
// Ref for text input (used by useHomeEndKeys)
|
||||
const textInputRef = useRef(textInput)
|
||||
textInputRef.current = textInput
|
||||
const cursorPosRef = useRef(cursorPos)
|
||||
cursorPosRef.current = cursorPos
|
||||
|
||||
const [fileResults, setFileResults] = useState<FileSearchResult[]>([])
|
||||
const [selectedIndex, setSelectedIndex] = useState(0) // For file menu
|
||||
@@ -388,10 +363,8 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
const [userScrolled, setUserScrolled] = useState(false)
|
||||
|
||||
// Pasted text storage - maps placeholder number to full pasted content
|
||||
const [pastedTexts, setPastedTexts] = useState<Map<number, string>>(() => {
|
||||
return inputStateStorage.get(storageKey)?.pastedTexts ?? new Map()
|
||||
})
|
||||
const pasteCounterRef = useRef<number>(inputStateStorage.get(storageKey)?.pasteCounter ?? 0)
|
||||
const [pastedTexts, setPastedTexts] = useState<Map<number, string>>(new Map())
|
||||
const pasteCounterRef = useRef(0)
|
||||
// Track paste timing to combine chunks that arrive in rapid succession
|
||||
const lastPasteTimeRef = useRef<number>(0)
|
||||
const activePasteNumRef = useRef<number>(0)
|
||||
@@ -425,29 +398,6 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
// Track when we're exiting to hide UI elements before exit
|
||||
const [isExiting, setIsExiting] = useState(false)
|
||||
|
||||
// Restore input state from storage on mount (after resize remount)
|
||||
useEffect(() => {
|
||||
const stored = inputStateStorage.get(storageKey)
|
||||
if (stored) {
|
||||
setTextInput(stored.text)
|
||||
setCursorPos(stored.cursorPos)
|
||||
setPastedTexts(stored.pastedTexts)
|
||||
pasteCounterRef.current = stored.pasteCounter
|
||||
}
|
||||
}, [storageKey, setTextInput, setCursorPos])
|
||||
|
||||
// Persist input state to storage whenever it changes (survives remount)
|
||||
useEffect(() => {
|
||||
if (textInput || pastedTexts.size > 0) {
|
||||
inputStateStorage.set(storageKey, {
|
||||
text: textInput,
|
||||
cursorPos,
|
||||
pastedTexts: new Map(pastedTexts),
|
||||
pasteCounter: pasteCounterRef.current,
|
||||
})
|
||||
}
|
||||
}, [storageKey, textInput, cursorPos, pastedTexts])
|
||||
|
||||
// Task switch handling: when switching tasks via /history, we clear the terminal and
|
||||
// increment a counter used as the root Box's key. This forces React to remount the tree,
|
||||
// giving us a fresh Static instance. Mirrors how App.tsx handles resize with resizeKey.
|
||||
@@ -502,12 +452,11 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
// Get model ID based on current mode and provider
|
||||
// Different providers use different state keys (e.g., cline uses actModeOpenRouterModelId)
|
||||
// Re-read when activePanel changes (settings panel closes) to pick up changes
|
||||
// Falls back to provider's default model if no model has been explicitly set
|
||||
const modelId = useMemo(() => {
|
||||
if (!provider) return ""
|
||||
const stateManager = StateManager.get()
|
||||
const modelKey = getProviderModelIdKey(provider as ApiProvider, mode)
|
||||
return (stateManager.getGlobalSettingsKey(modelKey) as string) || getProviderDefaultModelId(provider as ApiProvider) || ""
|
||||
return (stateManager.getGlobalSettingsKey(modelKey) as string) || ""
|
||||
}, [mode, provider, activePanel])
|
||||
|
||||
const toggleMode = useCallback(async () => {
|
||||
@@ -540,14 +489,12 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
clearState() // Force clear React state (bypasses empty messages check)
|
||||
setTextInput("")
|
||||
setCursorPos(0)
|
||||
// Clear persisted state
|
||||
inputStateStorage.delete(storageKey)
|
||||
|
||||
// Post the now-empty state
|
||||
if (ctrl) {
|
||||
ctrl.postStateToWebview()
|
||||
}
|
||||
}, [ctrl, clearState, storageKey])
|
||||
}, [ctrl, clearState])
|
||||
|
||||
const refs = useRef({
|
||||
searchTimeout: null as NodeJS.Timeout | null,
|
||||
@@ -808,8 +755,6 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
setCursorPos(0)
|
||||
setPastedTexts(new Map()) // Clear stored pastes
|
||||
pasteCounterRef.current = 0
|
||||
// Clear persisted state
|
||||
inputStateStorage.delete(storageKey)
|
||||
|
||||
try {
|
||||
await ctrl.task.handleWebviewAskResponse(responseType, expandedText)
|
||||
@@ -817,7 +762,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
// Controller may be disposed
|
||||
}
|
||||
},
|
||||
[ctrl, pendingAsk, pastedTexts, storageKey],
|
||||
[ctrl, pendingAsk, pastedTexts],
|
||||
)
|
||||
|
||||
// Handle cancel/interrupt
|
||||
@@ -908,8 +853,6 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
setCursorPos(0)
|
||||
setPastedTexts(new Map()) // Clear stored pastes
|
||||
pasteCounterRef.current = 0
|
||||
// Clear persisted state
|
||||
inputStateStorage.delete(storageKey)
|
||||
|
||||
try {
|
||||
// Convert image paths to data URLs if needed
|
||||
@@ -937,12 +880,10 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
onError?.()
|
||||
}
|
||||
},
|
||||
[ctrl, onError, pastedTexts, storageKey],
|
||||
[ctrl, onError, pastedTexts],
|
||||
)
|
||||
|
||||
// Auto-submit initial prompt if provided
|
||||
// When taskId is also provided, this sends the prompt to resume the existing task
|
||||
// When no taskId, this creates a new task with the prompt
|
||||
useEffect(() => {
|
||||
const autoSubmit = async () => {
|
||||
if (!initialPrompt && (!initialImages || initialImages.length === 0)) {
|
||||
@@ -963,32 +904,8 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
if (initialPrompt) {
|
||||
setTerminalTitle(initialPrompt)
|
||||
}
|
||||
|
||||
if (taskId) {
|
||||
// Resuming an existing task with a prompt - wait for task to load first
|
||||
// The task loading happens in the other useEffect via showTaskWithId
|
||||
// We need to wait for it to complete before sending the resume message
|
||||
const task = await waitFor(() => ctrl.task, 5000)
|
||||
|
||||
if (task) {
|
||||
// Send the prompt as a message to resume the task
|
||||
await task.handleWebviewAskResponse("messageResponse", initialPrompt || "")
|
||||
} else {
|
||||
// Task failed to load, fall back to creating new task
|
||||
Logger.error(`Failed to load task ${taskId} for resume, creating new task instead`)
|
||||
await ctrl.initTask(
|
||||
initialPrompt || "",
|
||||
initialImages && initialImages.length > 0 ? initialImages : undefined,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// New task - use initTask
|
||||
// initialImages are already data URLs from index.ts processing
|
||||
await ctrl.initTask(
|
||||
initialPrompt || "",
|
||||
initialImages && initialImages.length > 0 ? initialImages : undefined,
|
||||
)
|
||||
}
|
||||
// initialImages are already data URLs from index.ts processing
|
||||
await ctrl.initTask(initialPrompt || "", initialImages && initialImages.length > 0 ? initialImages : undefined)
|
||||
} catch (_error) {
|
||||
onError?.()
|
||||
}
|
||||
@@ -1084,11 +1001,11 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
// 3. Handle Option+arrow via key.meta (backup - Ink sometimes parses these instead of passing raw sequence)
|
||||
if (key.meta) {
|
||||
if (key.leftArrow) {
|
||||
setCursorPos(findWordStart(textInputRef.current, cursorPosRef.current))
|
||||
setCursorPos(findWordStart(textInput, cursorPos))
|
||||
return
|
||||
}
|
||||
if (key.rightArrow) {
|
||||
setCursorPos(findWordEnd(textInputRef.current, cursorPosRef.current))
|
||||
setCursorPos(findWordEnd(textInput, cursorPos))
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -1274,8 +1191,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
if (hasPrimary && buttonConfig.primaryAction) {
|
||||
handleButtonAction(buttonConfig.primaryAction, true)
|
||||
return
|
||||
}
|
||||
if (hasSecondary && !hasPrimary && buttonConfig.secondaryAction) {
|
||||
} else if (hasSecondary && !hasPrimary && buttonConfig.secondaryAction) {
|
||||
handleButtonAction(buttonConfig.secondaryAction, false)
|
||||
return
|
||||
}
|
||||
@@ -1296,7 +1212,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
}
|
||||
// Number selection for options (only when no text typed yet)
|
||||
if (askType === "options") {
|
||||
const num = Number.parseInt(input, 10)
|
||||
const num = parseInt(input, 10)
|
||||
if (textInput === "" && !Number.isNaN(num) && num >= 1 && num <= askOptions.length) {
|
||||
const selectedOption = askOptions[num - 1]
|
||||
sendAskResponse("messageResponse", selectedOption)
|
||||
@@ -1338,10 +1254,10 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
}
|
||||
pasteUpdateTimeoutRef.current = setTimeout(() => {
|
||||
const newPlaceholder = `[Pasted text #${pasteNum} +${activePasteLinesRef.current} lines]`
|
||||
const pattern = new RegExp(`\\[Pasted text #${pasteNum} \\+\\d+ lines\\]`)
|
||||
const newText = textInputRef.current.replace(pattern, newPlaceholder)
|
||||
textInputRef.current = newText // Update ref immediately so setCursorPos bounds check works
|
||||
setTextInput(newText)
|
||||
setTextInput((prev) => {
|
||||
const pattern = new RegExp(`\\[Pasted text #${pasteNum} \\+\\d+ lines\\]`)
|
||||
return prev.replace(pattern, newPlaceholder)
|
||||
})
|
||||
// Update cursor to be right after the placeholder
|
||||
setCursorPos(activePasteStartPosRef.current + newPlaceholder.length)
|
||||
Logger.info(`Paste #${pasteNum} complete: ${activePasteLinesRef.current} lines`)
|
||||
@@ -1354,8 +1270,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
pasteCounterRef.current += 1
|
||||
const pasteNum = pasteCounterRef.current
|
||||
activePasteNumRef.current = pasteNum
|
||||
const currentCursorPos = cursorPosRef.current // Use ref to avoid stale closure
|
||||
activePasteStartPosRef.current = currentCursorPos // Track where placeholder starts
|
||||
activePasteStartPosRef.current = cursorPos // Track where placeholder starts
|
||||
// Count line breaks in the pasted content (handle both \n and \r)
|
||||
const extraLines = input.match(/[\r\n]/g)?.length || 0
|
||||
activePasteLinesRef.current = extraLines // Track total lines
|
||||
@@ -1367,11 +1282,8 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
return next
|
||||
})
|
||||
|
||||
const newText =
|
||||
textInputRef.current.slice(0, currentCursorPos) + placeholder + textInputRef.current.slice(currentCursorPos)
|
||||
textInputRef.current = newText // Update ref immediately so setCursorPos bounds check works
|
||||
setTextInput(newText)
|
||||
setCursorPos(currentCursorPos + placeholder.length)
|
||||
setTextInput((prev) => prev.slice(0, cursorPos) + placeholder + prev.slice(cursorPos))
|
||||
setCursorPos(cursorPos + placeholder.length)
|
||||
return // Exit early - don't also add the raw input via normal handling below
|
||||
}
|
||||
|
||||
@@ -1400,15 +1312,15 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
return
|
||||
}
|
||||
if (key.rightArrow && !inSlashMenu && !inFileMenu) {
|
||||
setCursorPos((pos) => Math.min(textInputRef.current.length, pos + 1))
|
||||
setCursorPos((pos) => Math.min(textInput.length, pos + 1))
|
||||
return
|
||||
}
|
||||
if (key.upArrow && !inSlashMenu && !inFileMenu) {
|
||||
setCursorPos(moveCursorUp(textInputRef.current, cursorPosRef.current))
|
||||
setCursorPos(moveCursorUp(textInput, cursorPos))
|
||||
return
|
||||
}
|
||||
if (key.downArrow && !inSlashMenu && !inFileMenu) {
|
||||
setCursorPos(moveCursorDown(textInputRef.current, cursorPosRef.current))
|
||||
setCursorPos(moveCursorDown(textInput, cursorPos))
|
||||
return
|
||||
}
|
||||
// Normal input (single char or short paste)
|
||||
@@ -1474,10 +1386,10 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
|
||||
{/* Dynamic region - only current streaming message + input */}
|
||||
<Box flexDirection="column" width="100%">
|
||||
{/* Animated robot and welcome text - only shown before messages start and user hasn't interacted */}
|
||||
{/* Animated robot and welcome text - only shown before messages start and user hasn't scrolled */}
|
||||
{isWelcomeState && (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<AsciiMotionCli onInteraction={() => setUserScrolled(true)} />
|
||||
<AsciiMotionCli onScroll={() => setUserScrolled(true)} />
|
||||
<Text> </Text>
|
||||
<Text bold color="white">
|
||||
{centerText("What can I do for you?")}
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
import { Box, Text, useApp, useInput } from "ink"
|
||||
import React, { useMemo, useState } from "react"
|
||||
import { useStdinContext } from "../context/StdinContext"
|
||||
import { fuzzyFilter } from "../utils/fuzzy-search"
|
||||
import {
|
||||
BooleanSelect,
|
||||
buildConfigEntries,
|
||||
@@ -22,8 +21,6 @@ import {
|
||||
HookInfo,
|
||||
HookRow,
|
||||
MAX_VISIBLE,
|
||||
ObjectEditorPanel,
|
||||
ObjectEditorState,
|
||||
parseValue,
|
||||
SEPARATOR,
|
||||
SectionHeader,
|
||||
@@ -108,8 +105,6 @@ export const ConfigView: React.FC<ConfigViewProps> = ({
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||
const [editValue, setEditValue] = useState("")
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [objectEditor, setObjectEditor] = useState<ObjectEditorState | null>(null)
|
||||
|
||||
// Build entries for settings tab
|
||||
const configEntries = useMemo(
|
||||
@@ -117,13 +112,6 @@ export const ConfigView: React.FC<ConfigViewProps> = ({
|
||||
[globalState, workspaceState],
|
||||
)
|
||||
|
||||
const filteredConfigEntries = useMemo(() => {
|
||||
if (!searchQuery.trim()) {
|
||||
return configEntries
|
||||
}
|
||||
return fuzzyFilter(configEntries, searchQuery, (entry) => `${entry.key} ${String(entry.value ?? "")}`)
|
||||
}, [configEntries, searchQuery])
|
||||
|
||||
// Build entries for rules tab
|
||||
const ruleEntries = useMemo(() => {
|
||||
const entries: ToggleEntry[] = []
|
||||
@@ -171,7 +159,7 @@ export const ConfigView: React.FC<ConfigViewProps> = ({
|
||||
const currentListLength = useMemo(() => {
|
||||
switch (currentTab) {
|
||||
case "settings":
|
||||
return filteredConfigEntries.length
|
||||
return configEntries.length
|
||||
case "rules":
|
||||
return ruleEntries.length
|
||||
case "workflows":
|
||||
@@ -183,14 +171,7 @@ export const ConfigView: React.FC<ConfigViewProps> = ({
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}, [
|
||||
currentTab,
|
||||
filteredConfigEntries.length,
|
||||
ruleEntries.length,
|
||||
workflowEntries.length,
|
||||
hookEntries.length,
|
||||
skillEntries.length,
|
||||
])
|
||||
}, [currentTab, configEntries.length, ruleEntries.length, workflowEntries.length, hookEntries.length, skillEntries.length])
|
||||
|
||||
// Get available tabs
|
||||
const availableTabs = useMemo(() => {
|
||||
@@ -210,11 +191,10 @@ export const ConfigView: React.FC<ConfigViewProps> = ({
|
||||
setCurrentTab(newTab)
|
||||
setSelectedIndex(0)
|
||||
setIsEditing(false)
|
||||
setObjectEditor(null)
|
||||
}
|
||||
|
||||
// Settings tab handlers
|
||||
const selectedConfigEntry = filteredConfigEntries[selectedIndex]
|
||||
const selectedConfigEntry = configEntries[selectedIndex]
|
||||
|
||||
const handleSettingsSave = (value: string | boolean) => {
|
||||
if (!selectedConfigEntry) {
|
||||
@@ -230,43 +210,6 @@ export const ConfigView: React.FC<ConfigViewProps> = ({
|
||||
setIsEditing(false)
|
||||
}
|
||||
|
||||
const getObjectAtPath = (root: Record<string, unknown>, path: string[]): Record<string, unknown> => {
|
||||
let current: unknown = root
|
||||
for (const segment of path) {
|
||||
if (!current || typeof current !== "object") {
|
||||
return {}
|
||||
}
|
||||
current = (current as Record<string, unknown>)[segment]
|
||||
}
|
||||
return current && typeof current === "object" ? (current as Record<string, unknown>) : {}
|
||||
}
|
||||
|
||||
const setObjectValueAtPath = (
|
||||
root: Record<string, unknown>,
|
||||
path: string[],
|
||||
key: string,
|
||||
value: unknown,
|
||||
): Record<string, unknown> => {
|
||||
if (path.length === 0) {
|
||||
return { ...root, [key]: value }
|
||||
}
|
||||
const [head, ...rest] = path
|
||||
const child = root[head]
|
||||
const childObj = child && typeof child === "object" ? (child as Record<string, unknown>) : {}
|
||||
return {
|
||||
...root,
|
||||
[head]: setObjectValueAtPath(childObj, rest, key, value),
|
||||
}
|
||||
}
|
||||
|
||||
const persistObjectEditor = (nextObject: Record<string, unknown>, source: "global" | "workspace", key: string) => {
|
||||
if (source === "global" && onUpdateGlobal) {
|
||||
onUpdateGlobal(key as GlobalStateAndSettingsKey, nextObject as never)
|
||||
} else if (source === "workspace" && onUpdateWorkspace) {
|
||||
onUpdateWorkspace(key as LocalStateKey, nextObject as never)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSettingsReset = () => {
|
||||
if (!selectedConfigEntry?.isEditable || selectedConfigEntry.source !== "global") {
|
||||
return
|
||||
@@ -297,22 +240,15 @@ export const ConfigView: React.FC<ConfigViewProps> = ({
|
||||
// Input handling
|
||||
useInput(
|
||||
(input, key) => {
|
||||
if (objectEditor) {
|
||||
return
|
||||
}
|
||||
|
||||
if (key.escape) {
|
||||
if (input.toLowerCase() === "q" || key.escape) {
|
||||
exit()
|
||||
}
|
||||
|
||||
if (key.leftArrow || key.rightArrow || (input >= "1" && input <= "5")) {
|
||||
const currentTabIndex = availableTabs.findIndex((t) => t.key === currentTab)
|
||||
const targetIdx =
|
||||
input >= "1" && input <= "5"
|
||||
? Number.parseInt(input) - 1
|
||||
: key.leftArrow
|
||||
? (currentTabIndex - 1 + availableTabs.length) % availableTabs.length
|
||||
: (currentTabIndex + 1) % availableTabs.length
|
||||
// Tab navigation with Tab key or number keys
|
||||
if (key.tab || (input >= "1" && input <= "5")) {
|
||||
const targetIdx = key.tab
|
||||
? (availableTabs.findIndex((t) => t.key === currentTab) + 1) % availableTabs.length
|
||||
: parseInt(input) - 1
|
||||
if (targetIdx >= 0 && targetIdx < availableTabs.length) {
|
||||
handleTabChange(availableTabs[targetIdx].key)
|
||||
}
|
||||
@@ -320,45 +256,21 @@ export const ConfigView: React.FC<ConfigViewProps> = ({
|
||||
}
|
||||
|
||||
// List navigation (arrow keys and vim-style j/k)
|
||||
if (key.upArrow) {
|
||||
if (key.upArrow || input === "k") {
|
||||
setSelectedIndex((i) => (i > 0 ? i - 1 : currentListLength - 1))
|
||||
} else if (key.downArrow) {
|
||||
} else if (key.downArrow || input === "j") {
|
||||
setSelectedIndex((i) => (i < currentListLength - 1 ? i + 1 : 0))
|
||||
}
|
||||
|
||||
// Tab-specific actions
|
||||
if (currentTab === "settings") {
|
||||
if ((key.return || key.tab) && selectedConfigEntry?.isEditable) {
|
||||
if (selectedConfigEntry.type === "boolean") {
|
||||
handleSettingsSave(!selectedConfigEntry.value)
|
||||
return
|
||||
}
|
||||
if (selectedConfigEntry.type === "object") {
|
||||
const value =
|
||||
selectedConfigEntry.value && typeof selectedConfigEntry.value === "object"
|
||||
? (selectedConfigEntry.value as Record<string, unknown>)
|
||||
: {}
|
||||
setObjectEditor({
|
||||
source: selectedConfigEntry.source,
|
||||
key: selectedConfigEntry.key,
|
||||
path: [],
|
||||
value,
|
||||
selectedIndex: 0,
|
||||
isEditingValue: false,
|
||||
editValue: "",
|
||||
})
|
||||
return
|
||||
}
|
||||
if ((key.return || input === "e") && selectedConfigEntry?.isEditable) {
|
||||
setEditValue(selectedConfigEntry.value !== undefined ? String(selectedConfigEntry.value) : "")
|
||||
setIsEditing(true)
|
||||
} else if (key.ctrl && input.toLowerCase() === "r") {
|
||||
} else if (input === "r") {
|
||||
handleSettingsReset()
|
||||
} else if (key.backspace || key.delete) {
|
||||
setSearchQuery((prev) => prev.slice(0, -1))
|
||||
} else if (input && !key.ctrl && !key.meta && !key.escape && !key.upArrow && !key.downArrow) {
|
||||
setSearchQuery((prev) => prev + input)
|
||||
}
|
||||
} else if (key.return || key.tab || input === " ") {
|
||||
} else if (key.return || input === " ") {
|
||||
// Toggle for rules/workflows/hooks/skills
|
||||
handleToggle()
|
||||
}
|
||||
@@ -426,31 +338,13 @@ export const ConfigView: React.FC<ConfigViewProps> = ({
|
||||
)
|
||||
}
|
||||
|
||||
if (objectEditor && currentTab === "settings") {
|
||||
return (
|
||||
<ObjectEditorPanel
|
||||
getObjectAtPath={getObjectAtPath}
|
||||
onClose={() => setObjectEditor(null)}
|
||||
onPersist={(nextObject) => persistObjectEditor(nextObject, objectEditor.source, objectEditor.key)}
|
||||
setObjectValueAtPath={setObjectValueAtPath}
|
||||
setState={setObjectEditor}
|
||||
state={objectEditor}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// Render tab content
|
||||
const renderTabContent = () => {
|
||||
switch (currentTab) {
|
||||
case "settings": {
|
||||
const visibleEntries = filteredConfigEntries.slice(startIndex, startIndex + MAX_VISIBLE)
|
||||
const visibleEntries = configEntries.slice(startIndex, startIndex + MAX_VISIBLE)
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Box>
|
||||
<Text>Search: </Text>
|
||||
<Text color="white">{searchQuery}</Text>
|
||||
<Text inverse> </Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>Data directory: </Text>
|
||||
<Text color="blue" underline>
|
||||
@@ -613,12 +507,12 @@ export const ConfigView: React.FC<ConfigViewProps> = ({
|
||||
|
||||
// Help text based on current tab
|
||||
const getHelpText = () => {
|
||||
const base = "↑/↓ Navigate • ←/→ tabs • 1-5 tabs • Esc Exit"
|
||||
const base = "↑/↓/j/k Navigate • Tab/1-5 Switch tabs • q/Esc Exit"
|
||||
if (currentTab === "settings") {
|
||||
return `${base} • Type to search • Enter/Tab Edit (booleans toggle) • Backspace clear search • Ctrl+R Reset`
|
||||
return `${base} • Enter/e Edit • r Reset`
|
||||
}
|
||||
const openFolder = onOpenFolder ? " • o Open folder" : ""
|
||||
return `${base} • Enter/Tab/Space Toggle${openFolder}`
|
||||
return `${base} • Enter/Space Toggle${openFolder}`
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -46,19 +46,16 @@ export interface SkillInfo {
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export interface ObjectEditorState {
|
||||
source: "global" | "workspace"
|
||||
key: string
|
||||
path: string[]
|
||||
value: Record<string, unknown>
|
||||
selectedIndex: number
|
||||
isEditingValue: boolean
|
||||
editValue: string
|
||||
}
|
||||
export const EXCLUDED_KEYS = new Set([
|
||||
"taskHistory",
|
||||
"primaryRootIndex",
|
||||
"subagentsEnabled",
|
||||
"subagentTerminalOutputLineLimit",
|
||||
"welcomeViewCompleted",
|
||||
"isNewUser",
|
||||
])
|
||||
|
||||
export const EXCLUDED_KEYS = new Set(["taskHistory", "primaryRootIndex", "welcomeViewCompleted", "isNewUser"])
|
||||
|
||||
export const EDITABLE_TYPES: Set<ValueType> = new Set(["string", "number", "boolean", "object"])
|
||||
export const EDITABLE_TYPES: Set<ValueType> = new Set(["string", "number", "boolean"])
|
||||
export const MAX_VISIBLE = 12
|
||||
export const SEPARATOR = "─".repeat(80)
|
||||
|
||||
@@ -138,7 +135,7 @@ export function parseValue(input: string, type: ValueType): unknown {
|
||||
return input.toLowerCase() === "true" || input === "1"
|
||||
}
|
||||
if (type === "number") {
|
||||
const num = Number.parseFloat(input)
|
||||
const num = parseFloat(input)
|
||||
return Number.isNaN(num) ? 0 : num
|
||||
}
|
||||
if (type === "object") {
|
||||
@@ -220,7 +217,7 @@ export const TextInput: React.FC<TextInputProps> = ({ label, onChange, onCancel,
|
||||
</Text>
|
||||
<Box>
|
||||
<Text color="white">{value}</Text>
|
||||
<Text color="cyan">|</Text>
|
||||
<Text inverse> </Text>
|
||||
</Box>
|
||||
<Text color="gray">Type: {type} • Enter to save • Esc to cancel</Text>
|
||||
</Box>
|
||||
@@ -382,169 +379,3 @@ export const SectionHeader: React.FC<{ title: string }> = ({ title }) => (
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
|
||||
interface ObjectEditorPanelProps {
|
||||
state: ObjectEditorState
|
||||
setState: React.Dispatch<React.SetStateAction<ObjectEditorState | null>>
|
||||
onClose: () => void
|
||||
onPersist: (nextObject: Record<string, unknown>) => void
|
||||
getObjectAtPath: (root: Record<string, unknown>, path: string[]) => Record<string, unknown>
|
||||
setObjectValueAtPath: (root: Record<string, unknown>, path: string[], key: string, value: unknown) => Record<string, unknown>
|
||||
}
|
||||
|
||||
export const ObjectEditorPanel: React.FC<ObjectEditorPanelProps> = ({
|
||||
state,
|
||||
setState,
|
||||
onClose,
|
||||
onPersist,
|
||||
getObjectAtPath,
|
||||
setObjectValueAtPath,
|
||||
}) => {
|
||||
const { isRawModeSupported } = useStdinContext()
|
||||
const currentNode = getObjectAtPath(state.value, state.path)
|
||||
const objectEntries = Object.entries(currentNode).sort(([a], [b]) => a.localeCompare(b))
|
||||
const selectedEntry = objectEntries[state.selectedIndex]
|
||||
const breadcrumb = [state.key, ...state.path].join(" › ")
|
||||
|
||||
useInput(
|
||||
(input, key) => {
|
||||
if (state.isEditingValue) {
|
||||
if (key.escape) {
|
||||
setState((prev) => (prev ? { ...prev, isEditingValue: false, editValue: "" } : prev))
|
||||
return
|
||||
}
|
||||
if (key.return) {
|
||||
if (!selectedEntry) {
|
||||
setState((prev) => (prev ? { ...prev, isEditingValue: false, editValue: "" } : prev))
|
||||
return
|
||||
}
|
||||
const [entryKey, entryValue] = selectedEntry
|
||||
let parsed: unknown = state.editValue
|
||||
if (typeof entryValue === "boolean") {
|
||||
parsed = state.editValue.toLowerCase() === "true" || state.editValue === "1"
|
||||
} else if (typeof entryValue === "number") {
|
||||
const maybeNum = Number(state.editValue)
|
||||
parsed = Number.isNaN(maybeNum) ? 0 : maybeNum
|
||||
}
|
||||
const nextObject = setObjectValueAtPath(state.value, state.path, entryKey, parsed)
|
||||
onPersist(nextObject)
|
||||
setState((prev) => (prev ? { ...prev, value: nextObject, isEditingValue: false, editValue: "" } : prev))
|
||||
return
|
||||
}
|
||||
if (key.backspace || key.delete) {
|
||||
setState((prev) => (prev ? { ...prev, editValue: prev.editValue.slice(0, -1) } : prev))
|
||||
return
|
||||
}
|
||||
if (input && !key.ctrl && !key.meta) {
|
||||
setState((prev) => (prev ? { ...prev, editValue: prev.editValue + input } : prev))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (key.escape) {
|
||||
if (state.path.length > 0) {
|
||||
setState((prev) => (prev ? { ...prev, path: prev.path.slice(0, -1), selectedIndex: 0 } : prev))
|
||||
} else {
|
||||
onClose()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (key.upArrow || input === "k") {
|
||||
setState((prev) =>
|
||||
prev
|
||||
? {
|
||||
...prev,
|
||||
selectedIndex:
|
||||
objectEntries.length > 0
|
||||
? prev.selectedIndex > 0
|
||||
? prev.selectedIndex - 1
|
||||
: objectEntries.length - 1
|
||||
: 0,
|
||||
}
|
||||
: prev,
|
||||
)
|
||||
return
|
||||
}
|
||||
if (key.downArrow || input === "j") {
|
||||
setState((prev) =>
|
||||
prev
|
||||
? {
|
||||
...prev,
|
||||
selectedIndex:
|
||||
objectEntries.length > 0
|
||||
? prev.selectedIndex < objectEntries.length - 1
|
||||
? prev.selectedIndex + 1
|
||||
: 0
|
||||
: 0,
|
||||
}
|
||||
: prev,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (key.return || key.tab) {
|
||||
if (!selectedEntry) {
|
||||
return
|
||||
}
|
||||
const [entryKey, entryValue] = selectedEntry
|
||||
if (typeof entryValue === "boolean") {
|
||||
const nextObject = setObjectValueAtPath(state.value, state.path, entryKey, !entryValue)
|
||||
onPersist(nextObject)
|
||||
setState((prev) => (prev ? { ...prev, value: nextObject } : prev))
|
||||
return
|
||||
}
|
||||
if (entryValue && typeof entryValue === "object" && !Array.isArray(entryValue)) {
|
||||
setState((prev) => (prev ? { ...prev, path: [...prev.path, entryKey], selectedIndex: 0 } : prev))
|
||||
return
|
||||
}
|
||||
setState((prev) =>
|
||||
prev
|
||||
? { ...prev, isEditingValue: true, editValue: entryValue !== undefined ? String(entryValue) : "" }
|
||||
: prev,
|
||||
)
|
||||
}
|
||||
},
|
||||
{ isActive: isRawModeSupported },
|
||||
)
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text bold color="white">
|
||||
⚙️ Edit Nested Object
|
||||
</Text>
|
||||
<Text color="gray">{SEPARATOR}</Text>
|
||||
<Text color="cyan">{breadcrumb}</Text>
|
||||
{state.isEditingValue ? (
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<Box>
|
||||
<Text color="white">{state.editValue}</Text>
|
||||
<Text color="cyan">|</Text>
|
||||
</Box>
|
||||
<Text color="gray">Enter to save • Esc to cancel</Text>
|
||||
</Box>
|
||||
) : (
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
{objectEntries.length === 0 ? (
|
||||
<Text color="gray">No nested keys at this level.</Text>
|
||||
) : (
|
||||
objectEntries.map(([key, value], idx) => {
|
||||
const isSelected = idx === state.selectedIndex
|
||||
const valueText =
|
||||
value && typeof value === "object" && !Array.isArray(value) ? "{...}" : String(value)
|
||||
return (
|
||||
<Text color={isSelected ? "cyan" : undefined} key={key}>
|
||||
{isSelected ? "❯ " : " "}
|
||||
<Text color="cyan">{key}</Text>
|
||||
<Text color="gray">: </Text>
|
||||
<Text color="white">{valueText}</Text>
|
||||
</Text>
|
||||
)
|
||||
})
|
||||
)}
|
||||
<Text color="gray">↑/↓ Navigate • Enter/Tab Edit or drill in • Esc Back/Close</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -45,15 +45,15 @@ export const FeaturedModelPicker: React.FC<FeaturedModelPickerProps> = ({
|
||||
<Text bold color={isSelected ? COLORS.primaryBlue : "white"}>
|
||||
{model.name}
|
||||
</Text>
|
||||
{model.labels.map((label) => (
|
||||
<Text key={label}>
|
||||
{model.label && (
|
||||
<Text>
|
||||
<Text> </Text>
|
||||
<Text backgroundColor={label === "FREE" ? "gray" : COLORS.primaryBlue} color="black">
|
||||
<Text backgroundColor={model.label === "FREE" ? "gray" : COLORS.primaryBlue} color="black">
|
||||
{" "}
|
||||
{label}{" "}
|
||||
{model.label}{" "}
|
||||
</Text>
|
||||
</Text>
|
||||
))}
|
||||
)}
|
||||
</Box>
|
||||
<Box paddingLeft={2}>
|
||||
<Text color="gray">{model.description}</Text>
|
||||
|
||||
@@ -43,30 +43,6 @@ export const HelpPanelContent: React.FC<HelpPanelContentProps> = ({ onClose }) =
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box flexDirection="column">
|
||||
<Text bold>Keyboard Shortcuts</Text>
|
||||
<Text>
|
||||
{" "}
|
||||
<Text color="white">Ctrl+U</Text> - Clear entire input (delete to start)
|
||||
</Text>
|
||||
<Text>
|
||||
{" "}
|
||||
<Text color="white">Ctrl+K</Text> - Delete from cursor to end
|
||||
</Text>
|
||||
<Text>
|
||||
{" "}
|
||||
<Text color="white">Ctrl+W</Text> - Delete word backwards
|
||||
</Text>
|
||||
<Text>
|
||||
{" "}
|
||||
<Text color="white">Ctrl+A / Ctrl+E</Text> - Jump to start / end of input
|
||||
</Text>
|
||||
<Text>
|
||||
{" "}
|
||||
<Text color="white">Alt/Option+←/→</Text> - Move by word
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box flexDirection="column">
|
||||
<Text bold>Slash Commands</Text>
|
||||
<Text>
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
import React, { useMemo } from "react"
|
||||
import { StateManager } from "@/core/storage/StateManager"
|
||||
import type { ApiConfiguration } from "@/shared/api"
|
||||
import { getProviderLabel, useValidProviders } from "../utils/providers"
|
||||
import { SearchableList, type SearchableListItem } from "./SearchableList"
|
||||
import { CLI_EXCLUDED_PROVIDERS, getProviderLabel, getProviderOrder } from "../utils/providers"
|
||||
import { SearchableList, SearchableListItem } from "./SearchableList"
|
||||
|
||||
// Re-export for backwards compatibility
|
||||
export { getProviderLabel }
|
||||
export { CLI_EXCLUDED_PROVIDERS, getProviderLabel, getProviderOrder }
|
||||
|
||||
/**
|
||||
* Check if a provider is configured (has required credentials/settings)
|
||||
@@ -125,16 +125,17 @@ interface ProviderPickerProps {
|
||||
export const ProviderPicker: React.FC<ProviderPickerProps> = ({ onSelect, isActive = true }) => {
|
||||
// Get API configuration to check which providers are configured
|
||||
const apiConfig = StateManager.get().getApiConfiguration()
|
||||
const sorted = useValidProviders()
|
||||
|
||||
// Use providers.json order, filtered to exclude CLI-incompatible providers
|
||||
const items: SearchableListItem[] = useMemo(() => {
|
||||
const sorted = getProviderOrder().filter((p: string) => !CLI_EXCLUDED_PROVIDERS.has(p))
|
||||
|
||||
return sorted.map((providerId: string) => ({
|
||||
id: providerId,
|
||||
label: getProviderLabel(providerId),
|
||||
suffix: isProviderConfigured(providerId, apiConfig) ? "(Configured)" : undefined,
|
||||
}))
|
||||
}, [apiConfig, sorted])
|
||||
}, [apiConfig])
|
||||
|
||||
return <SearchableList isActive={isActive} items={items} onSelect={(item) => onSelect(item.id)} />
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import type { AutoApprovalSettings } from "@shared/AutoApprovalSettings"
|
||||
import { DEFAULT_AUTO_APPROVAL_SETTINGS } from "@shared/AutoApprovalSettings"
|
||||
import type { ApiProvider, ModelInfo } from "@shared/api"
|
||||
import { getProviderModelIdKey, isSettingsKey, ProviderToApiKeyMap } from "@shared/storage"
|
||||
import { isOpenaiReasoningEffort, OPENAI_REASONING_EFFORT_OPTIONS, type OpenaiReasoningEffort } from "@shared/storage/types"
|
||||
import type { TelemetrySetting } from "@shared/TelemetrySetting"
|
||||
import { Box, Text, useInput } from "ink"
|
||||
import Spinner from "ink-spinner"
|
||||
@@ -19,7 +18,6 @@ import { openAiCodexOAuthManager } from "@/integrations/openai-codex/oauth"
|
||||
import { ClineAccountService } from "@/services/account/ClineAccountService"
|
||||
import { AuthService, ClineAccountOrganization } from "@/services/auth/AuthService"
|
||||
import { openExternal } from "@/utils/env"
|
||||
import { supportsReasoningEffortForModel } from "@/utils/model-utils"
|
||||
import { version as CLI_VERSION } from "../../package.json"
|
||||
import { COLORS } from "../constants/colors"
|
||||
import { useStdinContext } from "../context/StdinContext"
|
||||
@@ -53,25 +51,13 @@ type SettingsTab = "api" | "auto-approve" | "features" | "other" | "account"
|
||||
interface ListItem {
|
||||
key: string
|
||||
label: string
|
||||
type: "checkbox" | "readonly" | "editable" | "separator" | "header" | "spacer" | "action" | "cycle"
|
||||
type: "checkbox" | "readonly" | "editable" | "separator" | "header" | "spacer" | "action"
|
||||
value: string | boolean
|
||||
description?: string
|
||||
isSubItem?: boolean
|
||||
parentKey?: string
|
||||
}
|
||||
|
||||
function normalizeReasoningEffort(value: unknown): OpenaiReasoningEffort {
|
||||
if (isOpenaiReasoningEffort(value)) {
|
||||
return value
|
||||
}
|
||||
return "low"
|
||||
}
|
||||
|
||||
function nextReasoningEffort(current: OpenaiReasoningEffort): OpenaiReasoningEffort {
|
||||
const idx = OPENAI_REASONING_EFFORT_OPTIONS.indexOf(current)
|
||||
return OPENAI_REASONING_EFFORT_OPTIONS[(idx + 1) % OPENAI_REASONING_EFFORT_OPTIONS.length]
|
||||
}
|
||||
|
||||
const TABS: PanelTab[] = [
|
||||
{ key: "api", label: "API" },
|
||||
{ key: "auto-approve", label: "Auto-approve" },
|
||||
@@ -82,12 +68,6 @@ const TABS: PanelTab[] = [
|
||||
|
||||
// Settings configuration for simple boolean toggles
|
||||
const FEATURE_SETTINGS = {
|
||||
subagents: {
|
||||
stateKey: "subagentsEnabled",
|
||||
default: false,
|
||||
label: "Subagents",
|
||||
description: "Let Cline run focused subagents in parallel to explore the codebase for you",
|
||||
},
|
||||
autoCondense: {
|
||||
stateKey: "useAutoCondense",
|
||||
default: false,
|
||||
@@ -118,12 +98,6 @@ const FEATURE_SETTINGS = {
|
||||
label: "Parallel tool calling",
|
||||
description: "Allow multiple tools in a single response",
|
||||
},
|
||||
doubleCheckCompletion: {
|
||||
stateKey: "doubleCheckCompletionEnabled",
|
||||
default: false,
|
||||
label: "Double-check completion",
|
||||
description: "Reject first completion attempt and require re-verification",
|
||||
},
|
||||
} as const
|
||||
|
||||
type FeatureKey = keyof typeof FEATURE_SETTINGS
|
||||
@@ -191,12 +165,6 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
|
||||
const [planThinkingEnabled, setPlanThinkingEnabled] = useState<boolean>(
|
||||
() => (stateManager.getGlobalSettingsKey("planModeThinkingBudgetTokens") ?? 0) > 0,
|
||||
)
|
||||
const [actReasoningEffort, setActReasoningEffort] = useState<OpenaiReasoningEffort>(() =>
|
||||
normalizeReasoningEffort(stateManager.getGlobalSettingsKey("actModeReasoningEffort")),
|
||||
)
|
||||
const [planReasoningEffort, setPlanReasoningEffort] = useState<OpenaiReasoningEffort>(() =>
|
||||
normalizeReasoningEffort(stateManager.getGlobalSettingsKey("planModeReasoningEffort")),
|
||||
)
|
||||
|
||||
// Auto-approve settings (complex nested object)
|
||||
const [autoApproveSettings, setAutoApproveSettings] = useState<AutoApprovalSettings>(() => {
|
||||
@@ -427,12 +395,9 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
|
||||
|
||||
// Build items list based on current tab
|
||||
const items: ListItem[] = useMemo(() => {
|
||||
// Some providers/models expose reasoning effort instead of thinking budget controls.
|
||||
const providerUsesReasoningEffort = provider === "openai-native" || provider === "openai-codex"
|
||||
const showActReasoningEffort = supportsReasoningEffortForModel(actModelId || "")
|
||||
const showPlanReasoningEffort = supportsReasoningEffortForModel(planModelId || "")
|
||||
const showActThinkingOption = !providerUsesReasoningEffort && !showActReasoningEffort
|
||||
const showPlanThinkingOption = !providerUsesReasoningEffort && !showPlanReasoningEffort
|
||||
// OpenAI Native, Codex, and GPT models don't support thinking budget (they use reasoning effort)
|
||||
const isGptModel = actModelId?.toLowerCase().includes("gpt") || planModelId?.toLowerCase().includes("gpt")
|
||||
const showThinkingOption = provider !== "openai-native" && provider !== "openai-codex" && !isGptModel
|
||||
|
||||
switch (currentTab) {
|
||||
case "api":
|
||||
@@ -456,7 +421,7 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
|
||||
type: "editable" as const,
|
||||
value: actModelId || "not set",
|
||||
},
|
||||
...(showActThinkingOption
|
||||
...(showThinkingOption
|
||||
? [
|
||||
{
|
||||
key: "actThinkingEnabled",
|
||||
@@ -466,16 +431,6 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(showActReasoningEffort
|
||||
? [
|
||||
{
|
||||
key: "actReasoningEffort",
|
||||
label: "Reasoning effort",
|
||||
type: "cycle" as const,
|
||||
value: actReasoningEffort,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{ key: "planHeader", label: "Plan Mode", type: "header" as const, value: "" },
|
||||
{
|
||||
key: "planModelId",
|
||||
@@ -483,7 +438,7 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
|
||||
type: "editable" as const,
|
||||
value: planModelId || "not set",
|
||||
},
|
||||
...(showPlanThinkingOption
|
||||
...(showThinkingOption
|
||||
? [
|
||||
{
|
||||
key: "planThinkingEnabled",
|
||||
@@ -493,16 +448,6 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(showPlanReasoningEffort
|
||||
? [
|
||||
{
|
||||
key: "planReasoningEffort",
|
||||
label: "Reasoning effort",
|
||||
type: "cycle" as const,
|
||||
value: planReasoningEffort,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{ key: "spacer1", label: "", type: "spacer" as const, value: "" },
|
||||
]
|
||||
: [
|
||||
@@ -512,7 +457,7 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
|
||||
type: "editable" as const,
|
||||
value: actModelId || "not set",
|
||||
},
|
||||
...(showActThinkingOption
|
||||
...(showThinkingOption
|
||||
? [
|
||||
{
|
||||
key: "actThinkingEnabled",
|
||||
@@ -522,16 +467,6 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(showActReasoningEffort
|
||||
? [
|
||||
{
|
||||
key: "actReasoningEffort",
|
||||
label: "Reasoning effort",
|
||||
type: "cycle" as const,
|
||||
value: actReasoningEffort,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]),
|
||||
{
|
||||
key: "separateModels",
|
||||
@@ -694,8 +629,6 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
|
||||
separateModels,
|
||||
actThinkingEnabled,
|
||||
planThinkingEnabled,
|
||||
actReasoningEffort,
|
||||
planReasoningEffort,
|
||||
autoApproveSettings,
|
||||
features,
|
||||
preferredLanguage,
|
||||
@@ -729,33 +662,6 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
|
||||
}
|
||||
}, [items.length, selectedIndex])
|
||||
|
||||
const rebuildTaskApi = useCallback(() => {
|
||||
if (!controller?.task) {
|
||||
return
|
||||
}
|
||||
const currentMode = stateManager.getGlobalSettingsKey("mode")
|
||||
const apiConfig = stateManager.getApiConfiguration()
|
||||
controller.task.api = buildApiHandler({ ...apiConfig, ulid: controller.task.ulid }, currentMode)
|
||||
}, [controller, stateManager])
|
||||
|
||||
const setReasoningEffortForMode = useCallback(
|
||||
(mode: "act" | "plan", effort: OpenaiReasoningEffort) => {
|
||||
if (mode === "act") {
|
||||
setActReasoningEffort(effort)
|
||||
stateManager.setGlobalState("actModeReasoningEffort", effort)
|
||||
if (!separateModels) {
|
||||
setPlanReasoningEffort(effort)
|
||||
stateManager.setGlobalState("planModeReasoningEffort", effort)
|
||||
}
|
||||
} else {
|
||||
setPlanReasoningEffort(effort)
|
||||
stateManager.setGlobalState("planModeReasoningEffort", effort)
|
||||
}
|
||||
rebuildTaskApi()
|
||||
},
|
||||
[separateModels, rebuildTaskApi, stateManager],
|
||||
)
|
||||
|
||||
// Handle toggle/edit for selected item
|
||||
const handleAction = useCallback(() => {
|
||||
const item = items[selectedIndex]
|
||||
@@ -779,15 +685,6 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
|
||||
return
|
||||
}
|
||||
|
||||
if (item.type === "cycle") {
|
||||
const targetMode = item.key === "actReasoningEffort" ? "act" : item.key === "planReasoningEffort" ? "plan" : undefined
|
||||
if (targetMode) {
|
||||
const currentEffort = targetMode === "act" ? actReasoningEffort : planReasoningEffort
|
||||
setReasoningEffortForMode(targetMode, nextReasoningEffort(currentEffort))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (item.type === "editable") {
|
||||
// For provider field, use the provider picker
|
||||
if (item.key === "provider") {
|
||||
@@ -845,16 +742,7 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
|
||||
const actModel = stateManager.getGlobalSettingsKey(actKey)
|
||||
if (planKey) stateManager.setGlobalState(planKey, actModel)
|
||||
}
|
||||
const actThinkingBudget = stateManager.getGlobalSettingsKey("actModeThinkingBudgetTokens") ?? 0
|
||||
stateManager.setGlobalState("planModeThinkingBudgetTokens", actThinkingBudget)
|
||||
setPlanThinkingEnabled(actThinkingBudget > 0)
|
||||
|
||||
const actEffort = normalizeReasoningEffort(stateManager.getGlobalSettingsKey("actModeReasoningEffort"))
|
||||
stateManager.setGlobalState("planModeReasoningEffort", actEffort)
|
||||
setPlanReasoningEffort(actEffort)
|
||||
}
|
||||
|
||||
rebuildTaskApi()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -862,19 +750,23 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
|
||||
if (item.key === "actThinkingEnabled") {
|
||||
setActThinkingEnabled(newValue)
|
||||
stateManager.setGlobalState("actModeThinkingBudgetTokens", newValue ? 1024 : 0)
|
||||
if (!separateModels) {
|
||||
setPlanThinkingEnabled(newValue)
|
||||
stateManager.setGlobalState("planModeThinkingBudgetTokens", newValue ? 1024 : 0)
|
||||
}
|
||||
// Rebuild API handler to apply thinking budget change
|
||||
rebuildTaskApi()
|
||||
if (controller?.task) {
|
||||
const currentMode = stateManager.getGlobalSettingsKey("mode")
|
||||
const apiConfig = stateManager.getApiConfiguration()
|
||||
controller.task.api = buildApiHandler({ ...apiConfig, ulid: controller.task.ulid }, currentMode)
|
||||
}
|
||||
return
|
||||
}
|
||||
if (item.key === "planThinkingEnabled") {
|
||||
setPlanThinkingEnabled(newValue)
|
||||
stateManager.setGlobalState("planModeThinkingBudgetTokens", newValue ? 1024 : 0)
|
||||
// Rebuild API handler to apply thinking budget change
|
||||
rebuildTaskApi()
|
||||
if (controller?.task) {
|
||||
const currentMode = stateManager.getGlobalSettingsKey("mode")
|
||||
const apiConfig = stateManager.getApiConfiguration()
|
||||
controller.task.api = buildApiHandler({ ...apiConfig, ulid: controller.task.ulid }, currentMode)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -931,11 +823,6 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
|
||||
handleClineLogin,
|
||||
handleClineLogout,
|
||||
accountOrganizations,
|
||||
separateModels,
|
||||
actReasoningEffort,
|
||||
planReasoningEffort,
|
||||
rebuildTaskApi,
|
||||
setReasoningEffortForMode,
|
||||
])
|
||||
|
||||
// Handle model selection from picker
|
||||
@@ -1682,21 +1569,6 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
|
||||
)
|
||||
}
|
||||
|
||||
if (item.type === "cycle") {
|
||||
return (
|
||||
<Text key={item.key}>
|
||||
<Text bold color={isSelected ? COLORS.primaryBlue : undefined}>
|
||||
{isSelected ? "❯" : " "}{" "}
|
||||
</Text>
|
||||
<Text color={isSelected ? COLORS.primaryBlue : "white"}>{item.label}: </Text>
|
||||
<Text color={COLORS.primaryBlue}>
|
||||
{typeof item.value === "string" ? item.value : String(item.value)}
|
||||
</Text>
|
||||
{isSelected && <Text color="gray"> (Tab to cycle)</Text>}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
// Readonly or editable field
|
||||
return (
|
||||
<Text key={item.key}>
|
||||
|
||||
@@ -1,361 +0,0 @@
|
||||
import type { ClineAskUseSubagents, ClineMessage, ClineSaySubagentStatus } from "@shared/ExtensionMessage"
|
||||
import { Box, Text } from "ink"
|
||||
import Spinner from "ink-spinner"
|
||||
import React from "react"
|
||||
import { COLORS } from "../constants/colors"
|
||||
import { useTerminalSize } from "../hooks/useTerminalSize"
|
||||
import { jsonParseSafe } from "../utils/parser"
|
||||
|
||||
interface SubagentMessageProps {
|
||||
message: ClineMessage
|
||||
isStreaming?: boolean
|
||||
mode?: "act" | "plan"
|
||||
}
|
||||
|
||||
const TREE_PREFIX_WIDTH = 5
|
||||
const MIN_PROMPT_WIDTH = 20
|
||||
|
||||
const DotRow: React.FC<{ children: React.ReactNode; color?: string; flashing?: boolean }> = ({
|
||||
children,
|
||||
color,
|
||||
flashing = false,
|
||||
}) => (
|
||||
<Box flexDirection="row">
|
||||
<Box width={2}>
|
||||
{flashing ? (
|
||||
<Text color={color}>
|
||||
<Spinner type="toggle8" />
|
||||
</Text>
|
||||
) : (
|
||||
<Text color={color}>⏺</Text>
|
||||
)}
|
||||
</Box>
|
||||
<Box flexGrow={1}>{children}</Box>
|
||||
</Box>
|
||||
)
|
||||
|
||||
function formatCompactTokens(tokens: number | undefined): string {
|
||||
const value = Number.isFinite(tokens) ? Math.max(0, tokens || 0) : 0
|
||||
return new Intl.NumberFormat("en-US", {
|
||||
notation: "compact",
|
||||
maximumFractionDigits: 1,
|
||||
})
|
||||
.format(value)
|
||||
.toLowerCase()
|
||||
}
|
||||
|
||||
function formatCompactCost(cost: number | undefined): string {
|
||||
const value = Number.isFinite(cost) ? Math.max(0, cost || 0) : 0
|
||||
const maximumFractionDigits = value >= 0.01 ? 2 : 4
|
||||
return new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits,
|
||||
}).format(value)
|
||||
}
|
||||
|
||||
function formatSubagentStatsValues(
|
||||
toolCalls: number | undefined,
|
||||
contextTokens: number | undefined,
|
||||
totalCost: number | undefined,
|
||||
latestToolCall?: string,
|
||||
) {
|
||||
const safeToolCalls = Number.isFinite(toolCalls) ? Math.max(0, toolCalls || 0) : 0
|
||||
const toolUses = safeToolCalls === 1 ? "tool use" : "tool uses"
|
||||
const tokensUsed = formatCompactTokens(contextTokens || 0)
|
||||
const formattedCost = formatCompactCost(totalCost || 0)
|
||||
const stats = `${safeToolCalls} ${toolUses} · ${tokensUsed} tokens · ${formattedCost}`
|
||||
const latestTool = latestToolCall?.trim()
|
||||
return latestTool ? `${latestTool} · ${stats}` : stats
|
||||
}
|
||||
|
||||
function wrapPrompt(text: string, width: number): string[] {
|
||||
if (!text) {
|
||||
return [""]
|
||||
}
|
||||
|
||||
const normalizedWidth = Math.max(1, width)
|
||||
const wrappedLines: string[] = []
|
||||
const paragraphs = text.split("\n")
|
||||
|
||||
for (const paragraph of paragraphs) {
|
||||
const words = paragraph.trim().split(/\s+/).filter(Boolean)
|
||||
if (words.length === 0) {
|
||||
wrappedLines.push("")
|
||||
continue
|
||||
}
|
||||
|
||||
let line = ""
|
||||
for (const word of words) {
|
||||
if (!line) {
|
||||
if (word.length <= normalizedWidth) {
|
||||
line = word
|
||||
continue
|
||||
}
|
||||
|
||||
let remaining = word
|
||||
while (remaining.length > normalizedWidth) {
|
||||
wrappedLines.push(remaining.slice(0, normalizedWidth))
|
||||
remaining = remaining.slice(normalizedWidth)
|
||||
}
|
||||
line = remaining
|
||||
continue
|
||||
}
|
||||
|
||||
if (line.length + 1 + word.length <= normalizedWidth) {
|
||||
line = `${line} ${word}`
|
||||
continue
|
||||
}
|
||||
|
||||
wrappedLines.push(line)
|
||||
|
||||
if (word.length <= normalizedWidth) {
|
||||
line = word
|
||||
continue
|
||||
}
|
||||
|
||||
let remaining = word
|
||||
while (remaining.length > normalizedWidth) {
|
||||
wrappedLines.push(remaining.slice(0, normalizedWidth))
|
||||
remaining = remaining.slice(normalizedWidth)
|
||||
}
|
||||
line = remaining
|
||||
}
|
||||
|
||||
if (line) {
|
||||
wrappedLines.push(line)
|
||||
}
|
||||
}
|
||||
|
||||
return wrappedLines.length > 0 ? wrappedLines : [text]
|
||||
}
|
||||
|
||||
const TreePromptRow: React.FC<{
|
||||
prefix: React.ReactNode
|
||||
continuationPrefix: string
|
||||
prompt: string
|
||||
promptWidth: number
|
||||
color?: string
|
||||
}> = ({ prefix, continuationPrefix, prompt, promptWidth, color }) => {
|
||||
const lines = wrapPrompt(prompt, promptWidth)
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" width="100%">
|
||||
{lines.map((line, index) => (
|
||||
<Box flexDirection="row" key={`${line}-${index}`} width="100%">
|
||||
<Box flexShrink={0} width={TREE_PREFIX_WIDTH}>
|
||||
{index === 0 ? prefix : <Text color="gray">{continuationPrefix}</Text>}
|
||||
</Box>
|
||||
<Box flexGrow={1}>
|
||||
<Text color={color}>{line}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const TreeStatsRow: React.FC<{ prefix: string; stats: string }> = ({ prefix, stats }) => (
|
||||
<Box flexDirection="row" width="100%">
|
||||
<Box flexShrink={0} width={TREE_PREFIX_WIDTH}>
|
||||
<Text color="gray">{prefix}</Text>
|
||||
</Box>
|
||||
<Box flexGrow={1}>
|
||||
<Text color="gray">⎿ {stats}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
|
||||
export const SubagentMessage: React.FC<SubagentMessageProps> = ({ message, mode, isStreaming }) => {
|
||||
const { type, ask, say, text, partial } = message
|
||||
const toolColor = mode === "plan" ? "yellow" : COLORS.primaryBlue
|
||||
const { columns } = useTerminalSize()
|
||||
const promptWidth = Math.max(MIN_PROMPT_WIDTH, columns - 2 - TREE_PREFIX_WIDTH)
|
||||
|
||||
if ((type === "ask" && ask === "use_subagents") || say === "use_subagents") {
|
||||
const parsed = text
|
||||
? jsonParseSafe<ClineAskUseSubagents>(text, {
|
||||
prompts: [],
|
||||
})
|
||||
: { prompts: [] }
|
||||
|
||||
const prompts = (parsed.prompts || []).map((prompt) => prompt?.trim()).filter(Boolean)
|
||||
if (prompts.length === 0) {
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1} width="100%">
|
||||
<DotRow color={toolColor}>
|
||||
<Text color={toolColor}>Cline wants to run subagents:</Text>
|
||||
</DotRow>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const singular = prompts.length === 1
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1} width="100%">
|
||||
<DotRow color={toolColor} flashing={partial === true && isStreaming}>
|
||||
<Text color={toolColor}>{singular ? "Cline wants to run a subagent:" : "Cline wants to run subagents:"}</Text>
|
||||
</DotRow>
|
||||
<Box flexDirection="column" marginLeft={2} width="100%">
|
||||
{prompts.map((prompt, index) => {
|
||||
const isLastPrompt = index === prompts.length - 1
|
||||
const branch = isLastPrompt ? "└─" : "├─"
|
||||
const continuationPrefix = isLastPrompt ? " " : "│ "
|
||||
const shouldShowPromptStats = partial !== true || !isLastPrompt
|
||||
return (
|
||||
<Box flexDirection="column" key={`${prompt}-${index}`}>
|
||||
<TreePromptRow
|
||||
color={toolColor}
|
||||
continuationPrefix={continuationPrefix}
|
||||
prefix={<Text color={toolColor}>{`${branch} `}</Text>}
|
||||
prompt={prompt}
|
||||
promptWidth={promptWidth}
|
||||
/>
|
||||
{shouldShowPromptStats && (
|
||||
<TreeStatsRow
|
||||
prefix={continuationPrefix}
|
||||
stats={formatSubagentStatsValues(undefined, undefined, undefined)}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
if (say === "subagent" && text) {
|
||||
const parsed = jsonParseSafe<ClineSaySubagentStatus>(text, {
|
||||
status: "running",
|
||||
total: 0,
|
||||
completed: 0,
|
||||
successes: 0,
|
||||
failures: 0,
|
||||
toolCalls: 0,
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
contextWindow: 0,
|
||||
maxContextTokens: 0,
|
||||
maxContextUsagePercentage: 0,
|
||||
items: [],
|
||||
})
|
||||
|
||||
const items = parsed.items || []
|
||||
if (items.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1} width="100%">
|
||||
<DotRow color={toolColor} flashing={partial === true && isStreaming}>
|
||||
<Text color={toolColor}>
|
||||
{items.length === 1 ? "Cline is running a subagent:" : "Cline is running subagents:"}
|
||||
</Text>
|
||||
</DotRow>
|
||||
<Box flexDirection="column" marginLeft={2} width="100%">
|
||||
{items.map((entry, index) => {
|
||||
const isLastEntry = index === items.length - 1
|
||||
const branch = isLastEntry ? "└─" : "├─"
|
||||
const continuationPrefix = isLastEntry ? " " : "│ "
|
||||
const key = `${entry.index}-${index}`
|
||||
const shouldShowStats = true
|
||||
|
||||
if (entry.status === "completed") {
|
||||
return (
|
||||
<Box flexDirection="column" key={key}>
|
||||
<TreePromptRow
|
||||
color="green"
|
||||
continuationPrefix={continuationPrefix}
|
||||
prefix={
|
||||
<Box flexDirection="row">
|
||||
<Text color="gray">{`${branch} `}</Text>
|
||||
<Text color="green">✓</Text>
|
||||
</Box>
|
||||
}
|
||||
prompt={entry.prompt}
|
||||
promptWidth={promptWidth}
|
||||
/>
|
||||
<TreeStatsRow
|
||||
prefix={continuationPrefix}
|
||||
stats={formatSubagentStatsValues(
|
||||
entry.toolCalls,
|
||||
entry.contextTokens,
|
||||
entry.totalCost,
|
||||
entry.latestToolCall,
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
if (entry.status === "failed") {
|
||||
return (
|
||||
<Box flexDirection="column" key={key}>
|
||||
<TreePromptRow
|
||||
color="red"
|
||||
continuationPrefix={continuationPrefix}
|
||||
prefix={
|
||||
<Box flexDirection="row">
|
||||
<Text color="gray">{`${branch} `}</Text>
|
||||
<Text color="red">✗</Text>
|
||||
</Box>
|
||||
}
|
||||
prompt={entry.prompt}
|
||||
promptWidth={promptWidth}
|
||||
/>
|
||||
<TreeStatsRow
|
||||
prefix={continuationPrefix}
|
||||
stats={formatSubagentStatsValues(
|
||||
entry.toolCalls,
|
||||
entry.contextTokens,
|
||||
entry.totalCost,
|
||||
entry.latestToolCall,
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" key={key}>
|
||||
<TreePromptRow
|
||||
color={toolColor}
|
||||
continuationPrefix={continuationPrefix}
|
||||
prefix={
|
||||
<Box flexDirection="row">
|
||||
<Text color="gray">{branch} </Text>
|
||||
{entry.status === "running" ? (
|
||||
<Text color={toolColor}>
|
||||
<Spinner type="dots" />
|
||||
</Text>
|
||||
) : (
|
||||
<Text color={toolColor}>•</Text>
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
prompt={entry.prompt}
|
||||
promptWidth={promptWidth}
|
||||
/>
|
||||
{shouldShowStats && (
|
||||
<TreeStatsRow
|
||||
prefix={continuationPrefix}
|
||||
stats={formatSubagentStatsValues(
|
||||
entry.toolCalls,
|
||||
entry.contextTokens,
|
||||
entry.totalCost,
|
||||
entry.latestToolCall,
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -7,54 +7,48 @@ export interface FeaturedModel {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
labels: string[]
|
||||
label: string
|
||||
}
|
||||
|
||||
export const FEATURED_MODELS = {
|
||||
recommended: [
|
||||
{
|
||||
id: "anthropic/claude-opus-4.6",
|
||||
name: "Claude Opus 4.6",
|
||||
id: "anthropic/claude-opus-4.5",
|
||||
name: "Claude Opus 4.5",
|
||||
description: "State-of-the-art for complex coding",
|
||||
labels: ["BEST"],
|
||||
label: "Best",
|
||||
},
|
||||
{
|
||||
id: "openai/gpt-5.2-codex",
|
||||
name: "GPT 5.2 Codex",
|
||||
description: "OpenAI's latest with strong coding abilities",
|
||||
labels: ["NEW"],
|
||||
label: "New",
|
||||
},
|
||||
{
|
||||
id: "google/gemini-3-pro-preview",
|
||||
name: "Gemini 3 Pro",
|
||||
description: "1M context window for large codebases",
|
||||
labels: ["TRENDING"],
|
||||
label: "Trending",
|
||||
},
|
||||
] as FeaturedModel[],
|
||||
free: [
|
||||
{
|
||||
id: "minimax/minimax-m2.1",
|
||||
name: "MiniMax M2.1",
|
||||
description: "Exceptional Multi-Programming Language Capabilities",
|
||||
labels: ["FREE"],
|
||||
},
|
||||
{
|
||||
id: "moonshotai/kimi-k2.5",
|
||||
name: "Kimi K2.5",
|
||||
description: "State-of-the-art model topping benchmarks",
|
||||
labels: ["FREE"],
|
||||
label: "FREE",
|
||||
},
|
||||
{
|
||||
id: "kwaipilot/kat-coder-pro",
|
||||
name: "KAT Coder Pro",
|
||||
description: "Advanced agentic coding model",
|
||||
labels: ["FREE"],
|
||||
label: "FREE",
|
||||
},
|
||||
{
|
||||
id: "arcee-ai/trinity-large-preview:free",
|
||||
name: "Trinity Large Preview",
|
||||
description: "US built open source coding model",
|
||||
labels: ["FREE"],
|
||||
label: "FREE",
|
||||
},
|
||||
] as FeaturedModel[],
|
||||
}
|
||||
|
||||
@@ -142,17 +142,6 @@ export class CliEnvServiceClient implements EnvServiceClientInterface {
|
||||
printInfo("Shutting down...")
|
||||
return proto.cline.Empty.create()
|
||||
}
|
||||
|
||||
async openExternal(request: proto.cline.StringRequest): Promise<proto.cline.Empty> {
|
||||
const url = request.value || ""
|
||||
if (url) {
|
||||
printInfo(`🌐 Opening: ${url}`)
|
||||
// Dynamically import 'open' to open URL in default browser
|
||||
const { default: open } = await import("open")
|
||||
await open(url)
|
||||
}
|
||||
return proto.cline.Empty.create()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
* - Ctrl+A/E: start/end of line
|
||||
* - Ctrl+W: delete word backwards
|
||||
* - Ctrl+U: delete to start of line
|
||||
* - Ctrl+K: delete to end of line
|
||||
*
|
||||
* Note: Home/End keys are handled by useHomeEndKeys hook because Ink doesn't
|
||||
* expose them in useInput (it sets input='' for these keys).
|
||||
@@ -153,14 +152,6 @@ export function useTextInput(): UseTextInputReturn {
|
||||
}
|
||||
}, [])
|
||||
|
||||
const deleteToEnd = useCallback(() => {
|
||||
const pos = cursorRef.current
|
||||
if (pos < textRef.current.length) {
|
||||
setTextState((prev) => prev.slice(0, pos))
|
||||
// Cursor stays at same position (now at end of text)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Cursor movement (internal, used by handlers)
|
||||
const moveToStart = useCallback(() => setCursorPosState(0), [])
|
||||
const moveToEnd = useCallback(() => setCursorPosState(textRef.current.length), [])
|
||||
@@ -199,9 +190,6 @@ export function useTextInput(): UseTextInputReturn {
|
||||
case "u": // Ctrl+U - delete to start
|
||||
deleteToStart()
|
||||
return true
|
||||
case "k": // Ctrl+K - delete to end
|
||||
deleteToEnd()
|
||||
return true
|
||||
case "w": // Ctrl+W - delete word backwards
|
||||
deleteWordBefore()
|
||||
return true
|
||||
@@ -209,7 +197,7 @@ export function useTextInput(): UseTextInputReturn {
|
||||
return false
|
||||
}
|
||||
},
|
||||
[moveToStart, moveToEnd, deleteToStart, deleteToEnd, deleteWordBefore],
|
||||
[moveToStart, moveToEnd, deleteToStart, deleteWordBefore],
|
||||
)
|
||||
|
||||
return {
|
||||
|
||||
+2
-42
@@ -30,9 +30,7 @@ describe("CLI Commands", () => {
|
||||
.option("-v, --verbose", "Show verbose output")
|
||||
.option("-c, --cwd <path>", "Working directory")
|
||||
.option("--config <path>", "Configuration directory")
|
||||
.option("--thinking [tokens]", "Enable extended thinking")
|
||||
.option("--reasoning-effort <effort>", "Reasoning effort")
|
||||
.option("--max-consecutive-mistakes <count>", "Maximum consecutive mistakes")
|
||||
.option("--thinking", "Enable extended thinking")
|
||||
.action(() => {})
|
||||
|
||||
program
|
||||
@@ -69,9 +67,7 @@ describe("CLI Commands", () => {
|
||||
.option("-v, --verbose", "Verbose output")
|
||||
.option("-c, --cwd <path>", "Working directory")
|
||||
.option("--config <path>", "Configuration directory")
|
||||
.option("--thinking [tokens]", "Enable extended thinking")
|
||||
.option("--reasoning-effort <effort>", "Reasoning effort")
|
||||
.option("--max-consecutive-mistakes <count>", "Maximum consecutive mistakes")
|
||||
.option("--thinking", "Enable extended thinking")
|
||||
.action(() => {})
|
||||
})
|
||||
|
||||
@@ -150,27 +146,6 @@ describe("CLI Commands", () => {
|
||||
expect(taskCmd.opts().thinking).toBe(true)
|
||||
})
|
||||
|
||||
it("should parse --thinking with token budget", () => {
|
||||
const taskCmd = program.commands.find((c) => c.name() === "task")!
|
||||
const args = ["test prompt", "--thinking", "8000"]
|
||||
taskCmd.parse(args, { from: "user" })
|
||||
expect(taskCmd.opts().thinking).toBe("8000")
|
||||
})
|
||||
|
||||
it("should parse --reasoning-effort option", () => {
|
||||
const taskCmd = program.commands.find((c) => c.name() === "task")!
|
||||
const args = ["test prompt", "--reasoning-effort", "high"]
|
||||
taskCmd.parse(args, { from: "user" })
|
||||
expect(taskCmd.opts().reasoningEffort).toBe("high")
|
||||
})
|
||||
|
||||
it("should parse --max-consecutive-mistakes option", () => {
|
||||
const taskCmd = program.commands.find((c) => c.name() === "task")!
|
||||
const args = ["test prompt", "--max-consecutive-mistakes", "999"]
|
||||
taskCmd.parse(args, { from: "user" })
|
||||
expect(taskCmd.opts().maxConsecutiveMistakes).toBe("999")
|
||||
})
|
||||
|
||||
it("should parse short flags", () => {
|
||||
const taskCmd = program.commands.find((c) => c.name() === "task")!
|
||||
const args = ["test prompt", "-a", "-v", "-m", "gpt-4"]
|
||||
@@ -306,21 +281,6 @@ describe("CLI Commands", () => {
|
||||
program.parse(["node", "cli", "--thinking"])
|
||||
expect(program.opts().thinking).toBe(true)
|
||||
})
|
||||
|
||||
it("should parse --thinking with token budget", () => {
|
||||
program.parse(["node", "cli", "--thinking", "4096"])
|
||||
expect(program.opts().thinking).toBe("4096")
|
||||
})
|
||||
|
||||
it("should parse --reasoning-effort option", () => {
|
||||
program.parse(["node", "cli", "--reasoning-effort", "medium"])
|
||||
expect(program.opts().reasoningEffort).toBe("medium")
|
||||
})
|
||||
|
||||
it("should parse --max-consecutive-mistakes option", () => {
|
||||
program.parse(["node", "cli", "--max-consecutive-mistakes", "7"])
|
||||
expect(program.opts().maxConsecutiveMistakes).toBe("7")
|
||||
})
|
||||
})
|
||||
|
||||
describe("command structure", () => {
|
||||
|
||||
+172
-397
@@ -19,12 +19,9 @@ import { BannerService } from "@/services/banner/BannerService"
|
||||
import { ErrorService } from "@/services/error/ErrorService"
|
||||
import { initializeDistinctId } from "@/services/logging/distinctId"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { PostHogClientProvider } from "@/services/telemetry/providers/posthog/PostHogClientProvider"
|
||||
import { HistoryItem } from "@/shared/HistoryItem"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { Session } from "@/shared/services/Session"
|
||||
import { getProviderModelIdKey, ProviderToApiKeyMap } from "@/shared/storage"
|
||||
import { isOpenaiReasoningEffort, OPENAI_REASONING_EFFORT_OPTIONS, type OpenaiReasoningEffort } from "@/shared/storage/types"
|
||||
import { version as CLI_VERSION } from "../package.json"
|
||||
import { runAcpMode } from "./acp/index.js"
|
||||
import { App } from "./components/App"
|
||||
@@ -34,7 +31,6 @@ import { CliCommentReviewController } from "./controllers/CliCommentReviewContro
|
||||
import { CliWebviewProvider } from "./controllers/CliWebviewProvider"
|
||||
import { restoreConsole } from "./utils/console"
|
||||
import { printInfo, printWarning } from "./utils/display"
|
||||
import { selectOutputMode } from "./utils/mode-selection"
|
||||
import { parseImagesFromInput, processImagePaths } from "./utils/parser"
|
||||
import { CLINE_CLI_DIR, getCliBinaryPath } from "./utils/path"
|
||||
import { readStdinIfPiped } from "./utils/piped"
|
||||
@@ -45,252 +41,6 @@ import { autoUpdateOnStartup, checkForUpdates } from "./utils/update"
|
||||
import { initializeCliContext } from "./vscode-context"
|
||||
import { CLI_LOG_FILE, shutdownEvent, window } from "./vscode-shim"
|
||||
|
||||
/**
|
||||
* Common options shared between runTask and resumeTask
|
||||
*/
|
||||
interface TaskOptions {
|
||||
act?: boolean
|
||||
plan?: boolean
|
||||
model?: string
|
||||
verbose?: boolean
|
||||
cwd?: string
|
||||
config?: string
|
||||
thinking?: boolean | string
|
||||
reasoningEffort?: string
|
||||
maxConsecutiveMistakes?: string
|
||||
yolo?: boolean
|
||||
doubleCheckCompletion?: boolean
|
||||
timeout?: string
|
||||
json?: boolean
|
||||
stdinWasPiped?: boolean
|
||||
}
|
||||
|
||||
let telemetryDisposed = false
|
||||
|
||||
async function disposeTelemetryServices(): Promise<void> {
|
||||
if (telemetryDisposed) {
|
||||
return
|
||||
}
|
||||
|
||||
telemetryDisposed = true
|
||||
await Promise.allSettled([telemetryService.dispose(), PostHogClientProvider.getInstance().dispose()])
|
||||
}
|
||||
|
||||
async function disposeCliContext(ctx: CliContext): Promise<void> {
|
||||
await ctx.controller.stateManager.flushPendingState()
|
||||
await ctx.controller.dispose()
|
||||
await ErrorService.get().dispose()
|
||||
await disposeTelemetryServices()
|
||||
}
|
||||
|
||||
function setModeScopedState(currentMode: "act" | "plan", setter: (mode: "act" | "plan") => void): void {
|
||||
const stateManager = StateManager.get()
|
||||
setter(currentMode)
|
||||
|
||||
const separateModels = stateManager.getGlobalSettingsKey("planActSeparateModelsSetting") ?? false
|
||||
if (!separateModels) {
|
||||
const otherMode: "act" | "plan" = currentMode === "act" ? "plan" : "act"
|
||||
setter(otherMode)
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeReasoningEffort(value?: string): OpenaiReasoningEffort | undefined {
|
||||
if (value === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const normalized = value.toLowerCase()
|
||||
if (isOpenaiReasoningEffort(normalized)) {
|
||||
return normalized
|
||||
}
|
||||
|
||||
printWarning(
|
||||
`Invalid --reasoning-effort '${value}'. Using 'medium'. Valid values: ${OPENAI_REASONING_EFFORT_OPTIONS.join(", ")}.`,
|
||||
)
|
||||
return "medium"
|
||||
}
|
||||
|
||||
function normalizeMaxConsecutiveMistakes(value?: string): number | undefined {
|
||||
if (value === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const parsed = Number.parseInt(value, 10)
|
||||
if (Number.isNaN(parsed) || parsed < 1) {
|
||||
printWarning(`Invalid --max-consecutive-mistakes value '${value}'. Expected integer >= 1.`)
|
||||
return undefined
|
||||
}
|
||||
|
||||
return parsed
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply task-related options (mode, model, thinking, yolo) to StateManager.
|
||||
* Shared between runTask and resumeTask to avoid duplication.
|
||||
*/
|
||||
function applyTaskOptions(options: TaskOptions): void {
|
||||
// Apply mode flag
|
||||
if (options.plan) {
|
||||
StateManager.get().setGlobalState("mode", "plan")
|
||||
telemetryService.captureHostEvent("mode_flag", "plan")
|
||||
} else if (options.act) {
|
||||
StateManager.get().setGlobalState("mode", "act")
|
||||
telemetryService.captureHostEvent("mode_flag", "act")
|
||||
}
|
||||
|
||||
// Apply model override if specified
|
||||
if (options.model) {
|
||||
const selectedMode = (StateManager.get().getGlobalSettingsKey("mode") || "act") as "act" | "plan"
|
||||
const providerKey = selectedMode === "act" ? "actModeApiProvider" : "planModeApiProvider"
|
||||
const currentProvider = StateManager.get().getGlobalSettingsKey(providerKey) as ApiProvider
|
||||
const modelKey = getProviderModelIdKey(currentProvider, selectedMode)
|
||||
if (modelKey) {
|
||||
StateManager.get().setGlobalState(modelKey, options.model)
|
||||
}
|
||||
telemetryService.captureHostEvent("model_flag", options.model)
|
||||
}
|
||||
|
||||
// Set thinking budget based on --thinking flag (boolean or number)
|
||||
let thinkingBudget = 0
|
||||
if (options.thinking) {
|
||||
if (typeof options.thinking === "string") {
|
||||
const parsed = Number.parseInt(options.thinking, 10)
|
||||
if (Number.isNaN(parsed) || parsed < 0) {
|
||||
printWarning(`Invalid --thinking value '${options.thinking}'. Using default 1024.`)
|
||||
thinkingBudget = 1024
|
||||
} else {
|
||||
thinkingBudget = parsed
|
||||
}
|
||||
} else {
|
||||
thinkingBudget = 1024
|
||||
}
|
||||
}
|
||||
const currentMode = (StateManager.get().getGlobalSettingsKey("mode") || "act") as "act" | "plan"
|
||||
setModeScopedState(currentMode, (mode) => {
|
||||
const thinkingKey = mode === "act" ? "actModeThinkingBudgetTokens" : "planModeThinkingBudgetTokens"
|
||||
StateManager.get().setGlobalState(thinkingKey, thinkingBudget)
|
||||
})
|
||||
if (options.thinking) {
|
||||
telemetryService.captureHostEvent("thinking_flag", "true")
|
||||
}
|
||||
|
||||
const reasoningEffort = normalizeReasoningEffort(options.reasoningEffort)
|
||||
if (reasoningEffort !== undefined) {
|
||||
setModeScopedState(currentMode, (mode) => {
|
||||
const reasoningKey = mode === "act" ? "actModeReasoningEffort" : "planModeReasoningEffort"
|
||||
StateManager.get().setGlobalState(reasoningKey, reasoningEffort)
|
||||
})
|
||||
telemetryService.captureHostEvent("reasoning_effort_flag", reasoningEffort)
|
||||
}
|
||||
|
||||
const maxConsecutiveMistakes = normalizeMaxConsecutiveMistakes(options.maxConsecutiveMistakes)
|
||||
if (maxConsecutiveMistakes !== undefined) {
|
||||
StateManager.get().setGlobalState("maxConsecutiveMistakes", maxConsecutiveMistakes)
|
||||
telemetryService.captureHostEvent("max_consecutive_mistakes_flag", String(maxConsecutiveMistakes))
|
||||
}
|
||||
|
||||
// Set yolo mode based on --yolo flag
|
||||
if (options.yolo) {
|
||||
StateManager.get().setGlobalState("yoloModeToggled", true)
|
||||
telemetryService.captureHostEvent("yolo_flag", "true")
|
||||
}
|
||||
|
||||
// Set double-check completion based on flag
|
||||
if (options.doubleCheckCompletion) {
|
||||
StateManager.get().setGlobalState("doubleCheckCompletionEnabled", true)
|
||||
telemetryService.captureHostEvent("double_check_completion_flag", "true")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get mode selection result using the extracted, testable selectOutputMode function.
|
||||
* This wrapper provides the current process TTY state.
|
||||
*/
|
||||
function getModeSelection(options: TaskOptions) {
|
||||
return selectOutputMode({
|
||||
stdoutIsTTY: process.stdout.isTTY === true,
|
||||
stdinIsTTY: process.stdin.isTTY === true,
|
||||
stdinWasPiped: options.stdinWasPiped ?? false,
|
||||
json: options.json,
|
||||
yolo: options.yolo,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if plain text mode should be used based on options and environment.
|
||||
*/
|
||||
function shouldUsePlainTextMode(options: TaskOptions): boolean {
|
||||
return getModeSelection(options).usePlainTextMode
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the reason for using plain text mode (for telemetry).
|
||||
*/
|
||||
function getPlainTextModeReason(options: TaskOptions): string {
|
||||
return getModeSelection(options).reason
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a task in plain text mode (no Ink UI).
|
||||
* Handles auth check, task execution, cleanup, and exit.
|
||||
*/
|
||||
async function runTaskInPlainTextMode(
|
||||
ctx: CliContext,
|
||||
options: TaskOptions,
|
||||
taskConfig: {
|
||||
prompt?: string
|
||||
taskId?: string
|
||||
imageDataUrls?: string[]
|
||||
},
|
||||
): Promise<never> {
|
||||
// Set flag so shutdown handler knows not to clear Ink UI lines
|
||||
isPlainTextMode = true
|
||||
|
||||
// Check if auth is configured before attempting to run the task
|
||||
// In plain text mode we can't show the interactive auth flow
|
||||
const hasAuth = await isAuthConfigured()
|
||||
if (!hasAuth) {
|
||||
printWarning("Not authenticated. Please run 'cline auth' first to configure your API credentials.")
|
||||
await disposeCliContext(ctx)
|
||||
exit(1)
|
||||
}
|
||||
|
||||
const reason = getPlainTextModeReason(options)
|
||||
telemetryService.captureHostEvent("plain_text_mode", reason)
|
||||
|
||||
// Plain text mode: no Ink rendering, just clean text output
|
||||
const success = await runPlainTextTask({
|
||||
controller: ctx.controller,
|
||||
prompt: taskConfig.prompt,
|
||||
taskId: taskConfig.taskId,
|
||||
imageDataUrls: taskConfig.imageDataUrls,
|
||||
verbose: options.verbose,
|
||||
jsonOutput: options.json,
|
||||
timeoutSeconds: options.timeout ? Number.parseInt(options.timeout, 10) : undefined,
|
||||
})
|
||||
|
||||
// Cleanup
|
||||
await disposeCliContext(ctx)
|
||||
|
||||
// Ensure stdout is fully drained before exiting - critical for piping
|
||||
await drainStdout()
|
||||
exit(success ? 0 : 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the standard cleanup function for Ink apps.
|
||||
*/
|
||||
function createInkCleanup(ctx: CliContext, onTaskError?: () => boolean): () => Promise<void> {
|
||||
return async () => {
|
||||
await disposeCliContext(ctx)
|
||||
if (onTaskError?.()) {
|
||||
printWarning("Task ended with errors.")
|
||||
exit(1)
|
||||
}
|
||||
exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
// Track active context for graceful shutdown
|
||||
let activeContext: CliContext | null = null
|
||||
let isShuttingDown = false
|
||||
@@ -342,11 +92,10 @@ function setupSignalHandlers() {
|
||||
if (task) {
|
||||
task.abortTask()
|
||||
}
|
||||
await disposeCliContext(activeContext)
|
||||
} else {
|
||||
await ErrorService.get().dispose()
|
||||
await disposeTelemetryServices()
|
||||
await activeContext.controller.stateManager.flushPendingState()
|
||||
await activeContext.controller.dispose()
|
||||
}
|
||||
await ErrorService.get().dispose()
|
||||
} catch {
|
||||
// Best effort cleanup
|
||||
}
|
||||
@@ -399,19 +148,9 @@ async function initializeCli(options: InitOptions): Promise<CliContext> {
|
||||
workspaceDir: workspacePath,
|
||||
})
|
||||
|
||||
// Set up output channel and Logger early so ClineEndpoint.initialize logs are captured
|
||||
const outputChannel = window.createOutputChannel("Cline CLI")
|
||||
const logToChannel = (message: string) => outputChannel.appendLine(message)
|
||||
|
||||
// Configure the shared Logging class early to capture all initialization logs
|
||||
Logger.subscribe(logToChannel)
|
||||
|
||||
await ClineEndpoint.initialize(EXTENSION_DIR)
|
||||
await ClineEndpoint.initialize()
|
||||
await initializeDistinctId(extensionContext)
|
||||
|
||||
// Auto-update check (after endpoints initialized, so we can detect bundled configs)
|
||||
autoUpdateOnStartup(CLI_VERSION)
|
||||
|
||||
// Initialize/reset session tracking for this CLI run
|
||||
Session.reset()
|
||||
|
||||
@@ -419,9 +158,11 @@ async function initializeCli(options: InitOptions): Promise<CliContext> {
|
||||
AuthHandler.getInstance().setEnabled(true)
|
||||
}
|
||||
|
||||
const outputChannel = window.createOutputChannel("Cline CLI")
|
||||
outputChannel.appendLine(
|
||||
`Cline CLI initialized. Data dir: ${DATA_DIR}, Extension dir: ${EXTENSION_DIR}, Log dir: ${CLINE_CLI_DIR.log}`,
|
||||
)
|
||||
const logToChannel = (message: string) => outputChannel.appendLine(message)
|
||||
|
||||
HostProvider.initialize(
|
||||
() => new CliWebviewProvider(extensionContext as any),
|
||||
@@ -430,7 +171,7 @@ async function initializeCli(options: InitOptions): Promise<CliContext> {
|
||||
() => new StandaloneTerminalManager(),
|
||||
createCliHostBridgeProvider(workspacePath),
|
||||
logToChannel,
|
||||
async (path: string) => (options.enableAuth ? AuthHandler.getInstance().getCallbackUrl(path) : ""),
|
||||
async () => (options.enableAuth ? AuthHandler.getInstance().getCallbackUrl() : ""),
|
||||
getCliBinaryPath,
|
||||
EXTENSION_DIR,
|
||||
DATA_DIR,
|
||||
@@ -442,13 +183,16 @@ async function initializeCli(options: InitOptions): Promise<CliContext> {
|
||||
// Initialize OpenAI Codex OAuth manager with extension context for secrets storage
|
||||
openAiCodexOAuthManager.initialize(extensionContext)
|
||||
|
||||
// Configure the shared Logging class to use HostProvider's output channel
|
||||
Logger.subscribe((msg: string) => HostProvider.get().logToChannel(msg))
|
||||
|
||||
const webview = HostProvider.get().createWebviewProvider() as CliWebviewProvider
|
||||
const controller = webview.controller
|
||||
|
||||
BannerService.initialize(webview.controller)
|
||||
|
||||
await telemetryService.captureExtensionActivated()
|
||||
await telemetryService.captureHostEvent("cline_cli", "initialized")
|
||||
telemetryService.captureExtensionActivated()
|
||||
telemetryService.captureHostEvent("cline_cli", "initialized")
|
||||
|
||||
const ctx = { extensionContext, dataDir: DATA_DIR, extensionDir: EXTENSION_DIR, workspacePath, controller }
|
||||
activeContext = ctx
|
||||
@@ -484,7 +228,24 @@ async function runInkApp(element: React.ReactElement, cleanup: () => Promise<voi
|
||||
/**
|
||||
* Run a task with the given prompt - uses welcome view for consistent behavior
|
||||
*/
|
||||
async function runTask(prompt: string, options: TaskOptions & { images?: string[] }, existingContext?: CliContext) {
|
||||
async function runTask(
|
||||
prompt: string,
|
||||
options: {
|
||||
act?: boolean
|
||||
plan?: boolean
|
||||
model?: string
|
||||
verbose?: boolean
|
||||
cwd?: string
|
||||
config?: string
|
||||
thinking?: boolean
|
||||
yolo?: boolean
|
||||
timeout?: string
|
||||
images?: string[]
|
||||
json?: boolean
|
||||
stdinWasPiped?: boolean
|
||||
},
|
||||
existingContext?: CliContext,
|
||||
) {
|
||||
const ctx = existingContext || (await initializeCli({ ...options, enableAuth: true }))
|
||||
|
||||
// Parse images from the prompt text (e.g., @/path/to/image.png)
|
||||
@@ -501,23 +262,101 @@ async function runTask(prompt: string, options: TaskOptions & { images?: string[
|
||||
// Task without prompt starts in interactive mode
|
||||
telemetryService.captureHostEvent("task_command", prompt ? "task" : "interactive")
|
||||
|
||||
// Apply shared task options (mode, model, thinking, yolo)
|
||||
applyTaskOptions(options)
|
||||
await StateManager.get().flushPendingState()
|
||||
|
||||
// Use plain text mode when output is redirected, stdin was piped, JSON mode is enabled, or --yolo flag is used
|
||||
if (shouldUsePlainTextMode(options)) {
|
||||
return runTaskInPlainTextMode(ctx, options, {
|
||||
prompt: taskPrompt,
|
||||
imageDataUrls: imageDataUrls.length > 0 ? imageDataUrls : undefined,
|
||||
})
|
||||
if (options.plan) {
|
||||
StateManager.get().setGlobalState("mode", "plan")
|
||||
telemetryService.captureHostEvent("mode_flag", "plan")
|
||||
} else if (options.act) {
|
||||
StateManager.get().setGlobalState("mode", "act")
|
||||
telemetryService.captureHostEvent("mode_flag", "act")
|
||||
}
|
||||
|
||||
if (options.model) {
|
||||
const selectedMode = (StateManager.get().getGlobalSettingsKey("mode") || "act") as "act" | "plan"
|
||||
|
||||
// Get the current provider for the selected mode
|
||||
const providerKey = selectedMode === "act" ? "actModeApiProvider" : "planModeApiProvider"
|
||||
const currentProvider = StateManager.get().getGlobalSettingsKey(providerKey) as ApiProvider
|
||||
|
||||
// Update model ID using provider-specific key (e.g., cline uses actModeOpenRouterModelId)
|
||||
const modelKey = getProviderModelIdKey(currentProvider, selectedMode)
|
||||
if (modelKey) {
|
||||
StateManager.get().setGlobalState(modelKey, options.model)
|
||||
}
|
||||
telemetryService.captureHostEvent("model_flag", options.model)
|
||||
}
|
||||
|
||||
// Set thinking budget based on --thinking flag
|
||||
const thinkingBudget = options.thinking ? 1024 : 0
|
||||
const currentMode = StateManager.get().getGlobalSettingsKey("mode") || "act"
|
||||
const thinkingKey = currentMode === "act" ? "actModeThinkingBudgetTokens" : "planModeThinkingBudgetTokens"
|
||||
StateManager.get().setGlobalState(thinkingKey, thinkingBudget)
|
||||
if (options.thinking) {
|
||||
telemetryService.captureHostEvent("thinking_flag", "true")
|
||||
}
|
||||
|
||||
// Set yolo mode based on --yolo flag
|
||||
if (options.yolo) {
|
||||
StateManager.get().setGlobalState("yoloModeToggled", true)
|
||||
telemetryService.captureHostEvent("yolo_flag", "true")
|
||||
}
|
||||
|
||||
await StateManager.get().flushPendingState()
|
||||
|
||||
// Detect if output is a TTY (interactive terminal) or redirected to a file/pipe
|
||||
const isTTY = process.stdout.isTTY === true
|
||||
|
||||
// Use plain text mode when output is redirected, stdin was piped, JSON mode is enabled, or --yolo flag is used
|
||||
// Ink requires raw mode on stdin which isn't available when stdin is piped
|
||||
// Note: we use the stdinWasPiped flag passed from the caller because process.stdin.isTTY
|
||||
// may not be reliable after stdin has been consumed by readStdinIfPiped()
|
||||
if (!isTTY || options.stdinWasPiped || options.json || options.yolo) {
|
||||
// Set flag so shutdown handler knows not to clear Ink UI lines
|
||||
isPlainTextMode = true
|
||||
|
||||
// Check if auth is configured before attempting to run the task
|
||||
// In plain text mode we can't show the interactive auth flow
|
||||
const hasAuth = await isAuthConfigured()
|
||||
if (!hasAuth) {
|
||||
printWarning("Not authenticated. Please run 'cline auth' first to configure your API credentials.")
|
||||
await ctx.controller.stateManager.flushPendingState()
|
||||
await ctx.controller.dispose()
|
||||
await ErrorService.get().dispose()
|
||||
exit(1)
|
||||
}
|
||||
|
||||
const reason = options.yolo
|
||||
? "yolo_flag"
|
||||
: options.json
|
||||
? "json"
|
||||
: options.stdinWasPiped
|
||||
? "piped_stdin"
|
||||
: "redirected_output"
|
||||
telemetryService.captureHostEvent("plain_text_mode", reason)
|
||||
// Plain text mode: no Ink rendering, just clean text output
|
||||
const success = await runPlainTextTask({
|
||||
controller: ctx.controller,
|
||||
prompt: taskPrompt,
|
||||
imageDataUrls: imageDataUrls.length > 0 ? imageDataUrls : undefined,
|
||||
verbose: options.verbose,
|
||||
jsonOutput: options.json,
|
||||
timeoutSeconds: options.timeout ? parseInt(options.timeout, 10) : undefined,
|
||||
})
|
||||
|
||||
// Cleanup
|
||||
await ctx.controller.stateManager.flushPendingState()
|
||||
await ctx.controller.dispose()
|
||||
await ErrorService.get().dispose()
|
||||
|
||||
// Ensure stdout is fully drained before exiting - critical for piping
|
||||
await drainStdout()
|
||||
exit(success ? 0 : 1)
|
||||
}
|
||||
|
||||
// Interactive mode: Render the welcome view with optional initial prompt/images
|
||||
// If prompt provided (cline task "prompt"), ChatView will auto-submit
|
||||
// If no prompt (cline interactive), user will type it in
|
||||
let taskError = false
|
||||
|
||||
// Render the welcome view with optional initial prompt/images
|
||||
// If prompt provided (cline task "prompt"), ChatView will auto-submit
|
||||
// If no prompt (cline interactive), user will type it in
|
||||
await runInkApp(
|
||||
React.createElement(App, {
|
||||
view: "welcome",
|
||||
@@ -530,10 +369,20 @@ async function runTask(prompt: string, options: TaskOptions & { images?: string[
|
||||
taskError = true
|
||||
},
|
||||
onWelcomeExit: () => {
|
||||
// User pressed Esc; Ink exits and cleanup handles process exit.
|
||||
// User pressed Esc
|
||||
exit(0)
|
||||
},
|
||||
}),
|
||||
createInkCleanup(ctx, () => taskError),
|
||||
async () => {
|
||||
await ctx.controller.stateManager.flushPendingState()
|
||||
await ctx.controller.dispose()
|
||||
await ErrorService.get().dispose()
|
||||
if (taskError) {
|
||||
printWarning("Task ended with errors.")
|
||||
exit(1)
|
||||
}
|
||||
exit(0)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -546,8 +395,8 @@ async function listHistory(options: { config?: string; limit?: number; page?: nu
|
||||
const taskHistory = StateManager.get().getGlobalStateKey("taskHistory") || []
|
||||
// Sort by timestamp (newest first) before pagination
|
||||
const sortedHistory = [...taskHistory].sort((a: any, b: any) => (b.ts || 0) - (a.ts || 0))
|
||||
const limit = typeof options.limit === "string" ? Number.parseInt(options.limit, 10) : options.limit || 10
|
||||
const initialPage = typeof options.page === "string" ? Number.parseInt(options.page, 10) : options.page || 1
|
||||
const limit = typeof options.limit === "string" ? parseInt(options.limit, 10) : options.limit || 10
|
||||
const initialPage = typeof options.page === "string" ? parseInt(options.page, 10) : options.page || 1
|
||||
const totalCount = sortedHistory.length
|
||||
const totalPages = Math.ceil(totalCount / limit)
|
||||
|
||||
@@ -555,7 +404,9 @@ async function listHistory(options: { config?: string; limit?: number; page?: nu
|
||||
|
||||
if (sortedHistory.length === 0) {
|
||||
printInfo("No task history found.")
|
||||
await disposeCliContext(ctx)
|
||||
await ctx.controller.stateManager.flushPendingState()
|
||||
await ctx.controller.dispose()
|
||||
await ErrorService.get().dispose()
|
||||
exit(0)
|
||||
}
|
||||
|
||||
@@ -569,7 +420,9 @@ async function listHistory(options: { config?: string; limit?: number; page?: nu
|
||||
isRawModeSupported: checkRawModeSupport(),
|
||||
}),
|
||||
async () => {
|
||||
await disposeCliContext(ctx)
|
||||
await ctx.controller.stateManager.flushPendingState()
|
||||
await ctx.controller.dispose()
|
||||
await ErrorService.get().dispose()
|
||||
exit(0)
|
||||
},
|
||||
)
|
||||
@@ -598,7 +451,9 @@ async function showConfig(options: { config?: string }) {
|
||||
isRawModeSupported: checkRawModeSupport(),
|
||||
}),
|
||||
async () => {
|
||||
await disposeCliContext(ctx)
|
||||
await ctx.controller.stateManager.flushPendingState()
|
||||
await ctx.controller.dispose()
|
||||
await ErrorService.get().dispose()
|
||||
exit(0)
|
||||
},
|
||||
)
|
||||
@@ -674,15 +529,17 @@ async function runAuth(options: {
|
||||
baseurl: options.baseurl,
|
||||
})
|
||||
|
||||
await ctx.controller.stateManager.flushPendingState()
|
||||
await ctx.controller.dispose()
|
||||
await ErrorService.get().dispose()
|
||||
|
||||
if (!result.success) {
|
||||
printWarning(result.error || "Quick setup failed")
|
||||
await telemetryService.captureHostEvent("auth", "error")
|
||||
await disposeCliContext(ctx)
|
||||
telemetryService.captureHostEvent("auth", "error")
|
||||
exit(1)
|
||||
}
|
||||
|
||||
await telemetryService.captureHostEvent("auth", "completed")
|
||||
await disposeCliContext(ctx)
|
||||
telemetryService.captureHostEvent("auth", "completed")
|
||||
exit(0)
|
||||
}
|
||||
|
||||
@@ -696,6 +553,7 @@ async function runAuth(options: {
|
||||
isRawModeSupported: checkRawModeSupport(),
|
||||
onComplete: () => {
|
||||
telemetryService.captureHostEvent("auth", "completed")
|
||||
exit(0)
|
||||
},
|
||||
onError: () => {
|
||||
telemetryService.captureHostEvent("auth", "error")
|
||||
@@ -703,10 +561,16 @@ async function runAuth(options: {
|
||||
},
|
||||
}),
|
||||
async () => {
|
||||
await disposeCliContext(ctx)
|
||||
exit(authError ? 1 : 0)
|
||||
await ctx.controller.stateManager.flushPendingState()
|
||||
await ctx.controller.dispose()
|
||||
await ErrorService.get().dispose()
|
||||
exit(0)
|
||||
},
|
||||
)
|
||||
|
||||
if (authError) {
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Setup CLI commands
|
||||
@@ -730,18 +594,9 @@ program
|
||||
.option("-v, --verbose", "Show verbose output")
|
||||
.option("-c, --cwd <path>", "Working directory for the task")
|
||||
.option("--config <path>", "Path to Cline configuration directory")
|
||||
.option("--thinking [tokens]", "Enable extended thinking (default: 1024 tokens)")
|
||||
.option("--reasoning-effort <effort>", "Reasoning effort: none|low|medium|high|xhigh")
|
||||
.option("--max-consecutive-mistakes <count>", "Maximum consecutive mistakes before halting in yolo mode")
|
||||
.option("--thinking", "Enable extended thinking (1024 token budget)")
|
||||
.option("--json", "Output messages as JSON instead of styled text")
|
||||
.option("--double-check-completion", "Reject first completion attempt to force re-verification")
|
||||
.option("-T, --taskId <id>", "Resume an existing task by ID")
|
||||
.action((prompt, options) => {
|
||||
if (options.taskId) {
|
||||
return resumeTask(options.taskId, { ...options, initialPrompt: prompt })
|
||||
}
|
||||
return runTask(prompt, options)
|
||||
})
|
||||
.action((prompt, options) => runTask(prompt, options))
|
||||
|
||||
program
|
||||
.command("history")
|
||||
@@ -854,67 +709,6 @@ async function checkAnyProviderConfigured(): Promise<boolean> {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that a task exists in history
|
||||
* @returns The task history item if found, null otherwise
|
||||
*/
|
||||
function findTaskInHistory(taskId: string): HistoryItem | null {
|
||||
const taskHistory = StateManager.get().getGlobalStateKey("taskHistory") || []
|
||||
return taskHistory.find((item) => item.id === taskId) || null
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume an existing task by ID
|
||||
* Loads the task and optionally prefills the input with a prompt
|
||||
*/
|
||||
async function resumeTask(taskId: string, options: TaskOptions & { initialPrompt?: string }) {
|
||||
const ctx = await initializeCli({ ...options, enableAuth: true })
|
||||
|
||||
// Validate task exists
|
||||
const historyItem = findTaskInHistory(taskId)
|
||||
if (!historyItem) {
|
||||
printWarning(`Task not found: ${taskId}`)
|
||||
printInfo("Use 'cline history' to see available tasks.")
|
||||
await disposeCliContext(ctx)
|
||||
exit(1)
|
||||
}
|
||||
|
||||
telemetryService.captureHostEvent("resume_task_command", options.initialPrompt ? "with_prompt" : "interactive")
|
||||
|
||||
// Apply shared task options (mode, model, thinking, yolo)
|
||||
applyTaskOptions(options)
|
||||
await StateManager.get().flushPendingState()
|
||||
|
||||
// Use plain text mode for non-interactive scenarios
|
||||
if (shouldUsePlainTextMode(options)) {
|
||||
return runTaskInPlainTextMode(ctx, options, {
|
||||
prompt: options.initialPrompt,
|
||||
taskId: taskId,
|
||||
})
|
||||
}
|
||||
|
||||
// Interactive mode: render the task view with the existing task
|
||||
let taskError = false
|
||||
|
||||
await runInkApp(
|
||||
React.createElement(App, {
|
||||
view: "task",
|
||||
taskId: taskId,
|
||||
verbose: options.verbose,
|
||||
controller: ctx.controller,
|
||||
isRawModeSupported: checkRawModeSupport(),
|
||||
initialPrompt: options.initialPrompt || undefined,
|
||||
onError: () => {
|
||||
taskError = true
|
||||
},
|
||||
onWelcomeExit: () => {
|
||||
// User pressed Esc; Ink exits and cleanup handles process exit.
|
||||
},
|
||||
}),
|
||||
createInkCleanup(ctx, () => taskError),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Show welcome prompt and wait for user input
|
||||
* If auth is not configured, show auth flow first
|
||||
@@ -935,14 +729,16 @@ async function showWelcome(options: { verbose?: boolean; cwd?: string; config?:
|
||||
controller: ctx.controller,
|
||||
isRawModeSupported: checkRawModeSupport(),
|
||||
onWelcomeExit: () => {
|
||||
// User pressed Esc; Ink exits and cleanup handles process exit.
|
||||
exit(0)
|
||||
},
|
||||
onError: () => {
|
||||
hadError = true
|
||||
},
|
||||
}),
|
||||
async () => {
|
||||
await disposeCliContext(ctx)
|
||||
await ctx.controller.stateManager.flushPendingState()
|
||||
await ctx.controller.dispose()
|
||||
await ErrorService.get().dispose()
|
||||
exit(hadError ? 1 : 0)
|
||||
},
|
||||
)
|
||||
@@ -959,13 +755,9 @@ program
|
||||
.option("-v, --verbose", "Show verbose output")
|
||||
.option("-c, --cwd <path>", "Working directory")
|
||||
.option("--config <path>", "Configuration directory")
|
||||
.option("--thinking [tokens]", "Enable extended thinking (default: 1024 tokens)")
|
||||
.option("--reasoning-effort <effort>", "Reasoning effort: none|low|medium|high|xhigh")
|
||||
.option("--max-consecutive-mistakes <count>", "Maximum consecutive mistakes before halting in yolo mode")
|
||||
.option("--thinking", "Enable extended thinking (1024 token budget)")
|
||||
.option("--json", "Output messages as JSON instead of styled text")
|
||||
.option("--double-check-completion", "Reject first completion attempt to force re-verification")
|
||||
.option("--acp", "Run in ACP (Agent Client Protocol) mode for editor integration")
|
||||
.option("-T, --taskId <id>", "Resume an existing task by ID")
|
||||
.action(async (prompt, options) => {
|
||||
// Check for ACP mode first - this takes precedence over everything else
|
||||
if (options.acp) {
|
||||
@@ -980,18 +772,8 @@ program
|
||||
// Always check for piped stdin content
|
||||
const stdinInput = await readStdinIfPiped()
|
||||
|
||||
// Track whether stdin was actually piped (even if empty) vs not piped (null)
|
||||
// stdinInput === null means stdin wasn't piped (TTY or not FIFO/file)
|
||||
// stdinInput === "" means stdin was piped but empty
|
||||
// stdinInput has content means stdin was piped with data
|
||||
const stdinWasPiped = stdinInput !== null
|
||||
|
||||
// Error if stdin was piped but empty AND no prompt was provided
|
||||
// This handles:
|
||||
// - `echo "" | cline` -> error (empty stdin, no prompt)
|
||||
// - `cline "prompt"` in GitHub Actions -> OK (empty stdin ignored, has prompt)
|
||||
// - `cat file | cline "explain"` -> OK (has stdin AND prompt)
|
||||
if (stdinInput === "" && !prompt) {
|
||||
// Error if stdin was piped but empty (e.g., `echo "" | cline`)
|
||||
if (stdinInput === "") {
|
||||
printWarning("Empty input received from stdin. Please provide content to process.")
|
||||
exit(1)
|
||||
}
|
||||
@@ -1014,24 +796,17 @@ program
|
||||
}
|
||||
}
|
||||
|
||||
// Handle --taskId flag to resume an existing task
|
||||
if (options.taskId) {
|
||||
await resumeTask(options.taskId, {
|
||||
...options,
|
||||
initialPrompt: effectivePrompt,
|
||||
stdinWasPiped,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (effectivePrompt) {
|
||||
// Pass stdinWasPiped flag so runTask knows to use plain text mode
|
||||
await runTask(effectivePrompt, { ...options, stdinWasPiped })
|
||||
await runTask(effectivePrompt, { ...options, stdinWasPiped: !!stdinInput })
|
||||
} else {
|
||||
// Show welcome prompt if no prompt given
|
||||
await showWelcome(options)
|
||||
}
|
||||
})
|
||||
|
||||
// Background auto-update check (non-blocking)
|
||||
autoUpdateOnStartup(CLI_VERSION)
|
||||
|
||||
// Parse and run
|
||||
program.parse()
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
/**
|
||||
* Opens a URL in the user's default browser.
|
||||
* Uses dynamic import of the 'open' package to open URLs.
|
||||
*
|
||||
* @param url - The URL to open in the browser
|
||||
*/
|
||||
export async function openUrlInBrowser(url: string): Promise<void> {
|
||||
const { default: open } = await import("open")
|
||||
await open(url)
|
||||
}
|
||||
@@ -1,194 +0,0 @@
|
||||
import { describe, expect, it } from "vitest"
|
||||
import { selectOutputMode } from "./mode-selection"
|
||||
|
||||
describe("selectOutputMode", () => {
|
||||
describe("interactive mode (Ink)", () => {
|
||||
it("should use interactive mode when both stdin and stdout are TTY", () => {
|
||||
const result = selectOutputMode({
|
||||
stdoutIsTTY: true,
|
||||
stdinIsTTY: true,
|
||||
stdinWasPiped: false,
|
||||
})
|
||||
expect(result.usePlainTextMode).toBe(false)
|
||||
expect(result.reason).toBe("interactive")
|
||||
})
|
||||
})
|
||||
|
||||
describe("yolo flag", () => {
|
||||
it("should use plain text mode when --yolo flag is set", () => {
|
||||
const result = selectOutputMode({
|
||||
stdoutIsTTY: true,
|
||||
stdinIsTTY: true,
|
||||
stdinWasPiped: false,
|
||||
yolo: true,
|
||||
})
|
||||
expect(result.usePlainTextMode).toBe(true)
|
||||
expect(result.reason).toBe("yolo_flag")
|
||||
})
|
||||
|
||||
it("should prioritize yolo over other flags", () => {
|
||||
const result = selectOutputMode({
|
||||
stdoutIsTTY: false,
|
||||
stdinIsTTY: false,
|
||||
stdinWasPiped: true,
|
||||
json: true,
|
||||
yolo: true,
|
||||
})
|
||||
expect(result.reason).toBe("yolo_flag")
|
||||
})
|
||||
})
|
||||
|
||||
describe("json flag", () => {
|
||||
it("should use plain text mode when --json flag is set", () => {
|
||||
const result = selectOutputMode({
|
||||
stdoutIsTTY: true,
|
||||
stdinIsTTY: true,
|
||||
stdinWasPiped: false,
|
||||
json: true,
|
||||
})
|
||||
expect(result.usePlainTextMode).toBe(true)
|
||||
expect(result.reason).toBe("json")
|
||||
})
|
||||
})
|
||||
|
||||
describe("piped stdin", () => {
|
||||
it("should use plain text mode when stdin was piped (echo x | cline)", () => {
|
||||
const result = selectOutputMode({
|
||||
stdoutIsTTY: true,
|
||||
stdinIsTTY: false, // piped stdin is not a TTY
|
||||
stdinWasPiped: true,
|
||||
})
|
||||
expect(result.usePlainTextMode).toBe(true)
|
||||
expect(result.reason).toBe("piped_stdin")
|
||||
})
|
||||
|
||||
it("should use plain text mode when stdin was piped but empty (echo '' | cline 'prompt')", () => {
|
||||
const result = selectOutputMode({
|
||||
stdoutIsTTY: true,
|
||||
stdinIsTTY: false,
|
||||
stdinWasPiped: true, // empty pipe still counts as piped
|
||||
})
|
||||
expect(result.usePlainTextMode).toBe(true)
|
||||
expect(result.reason).toBe("piped_stdin")
|
||||
})
|
||||
})
|
||||
|
||||
describe("stdin redirected (< /dev/null)", () => {
|
||||
it("should use plain text mode when stdin is redirected from /dev/null", () => {
|
||||
// cline "prompt" < /dev/null
|
||||
// stdin is not a TTY, but also not a FIFO/file, so stdinWasPiped=false
|
||||
const result = selectOutputMode({
|
||||
stdoutIsTTY: true,
|
||||
stdinIsTTY: false, // redirected, not a TTY
|
||||
stdinWasPiped: false, // /dev/null is a character device, not FIFO
|
||||
})
|
||||
expect(result.usePlainTextMode).toBe(true)
|
||||
expect(result.reason).toBe("stdin_redirected")
|
||||
})
|
||||
})
|
||||
|
||||
describe("stdout redirected", () => {
|
||||
it("should use plain text mode when stdout is redirected to file", () => {
|
||||
// cline "prompt" > output.txt
|
||||
const result = selectOutputMode({
|
||||
stdoutIsTTY: false,
|
||||
stdinIsTTY: true,
|
||||
stdinWasPiped: false,
|
||||
})
|
||||
expect(result.usePlainTextMode).toBe(true)
|
||||
expect(result.reason).toBe("stdout_redirected")
|
||||
})
|
||||
|
||||
it("should use plain text mode when stdout is piped", () => {
|
||||
// cline "prompt" | grep something
|
||||
const result = selectOutputMode({
|
||||
stdoutIsTTY: false,
|
||||
stdinIsTTY: true,
|
||||
stdinWasPiped: false,
|
||||
})
|
||||
expect(result.usePlainTextMode).toBe(true)
|
||||
expect(result.reason).toBe("stdout_redirected")
|
||||
})
|
||||
})
|
||||
|
||||
describe("GitHub Actions scenarios", () => {
|
||||
it("should use plain text mode in GitHub Actions (stdin is empty FIFO)", () => {
|
||||
// In GitHub Actions: stdin is an empty FIFO pipe
|
||||
// stdinIsTTY=false, stdinWasPiped=true (FIFO detected)
|
||||
const result = selectOutputMode({
|
||||
stdoutIsTTY: true, // GitHub Actions stdout is TTY-like
|
||||
stdinIsTTY: false,
|
||||
stdinWasPiped: true, // empty FIFO still counts as piped
|
||||
})
|
||||
expect(result.usePlainTextMode).toBe(true)
|
||||
})
|
||||
|
||||
it("should use plain text mode with --yolo in CI", () => {
|
||||
const result = selectOutputMode({
|
||||
stdoutIsTTY: false,
|
||||
stdinIsTTY: false,
|
||||
stdinWasPiped: false,
|
||||
yolo: true,
|
||||
})
|
||||
expect(result.usePlainTextMode).toBe(true)
|
||||
expect(result.reason).toBe("yolo_flag")
|
||||
})
|
||||
})
|
||||
|
||||
describe("real-world scenarios", () => {
|
||||
it("cline (no args, interactive terminal)", () => {
|
||||
const result = selectOutputMode({
|
||||
stdoutIsTTY: true,
|
||||
stdinIsTTY: true,
|
||||
stdinWasPiped: false,
|
||||
})
|
||||
expect(result.usePlainTextMode).toBe(false)
|
||||
})
|
||||
|
||||
it('cline "prompt" (prompt arg, interactive terminal)', () => {
|
||||
const result = selectOutputMode({
|
||||
stdoutIsTTY: true,
|
||||
stdinIsTTY: true,
|
||||
stdinWasPiped: false,
|
||||
})
|
||||
expect(result.usePlainTextMode).toBe(false)
|
||||
})
|
||||
|
||||
it('cat file | cline "explain"', () => {
|
||||
const result = selectOutputMode({
|
||||
stdoutIsTTY: true,
|
||||
stdinIsTTY: false,
|
||||
stdinWasPiped: true,
|
||||
})
|
||||
expect(result.usePlainTextMode).toBe(true)
|
||||
})
|
||||
|
||||
it('cline --yolo "prompt"', () => {
|
||||
const result = selectOutputMode({
|
||||
stdoutIsTTY: true,
|
||||
stdinIsTTY: true,
|
||||
stdinWasPiped: false,
|
||||
yolo: true,
|
||||
})
|
||||
expect(result.usePlainTextMode).toBe(true)
|
||||
})
|
||||
|
||||
it('cline "prompt" < /dev/null', () => {
|
||||
const result = selectOutputMode({
|
||||
stdoutIsTTY: true,
|
||||
stdinIsTTY: false,
|
||||
stdinWasPiped: false,
|
||||
})
|
||||
expect(result.usePlainTextMode).toBe(true)
|
||||
})
|
||||
|
||||
it('cline "prompt" > output.log', () => {
|
||||
const result = selectOutputMode({
|
||||
stdoutIsTTY: false,
|
||||
stdinIsTTY: true,
|
||||
stdinWasPiped: false,
|
||||
})
|
||||
expect(result.usePlainTextMode).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,63 +0,0 @@
|
||||
/**
|
||||
* Mode selection logic for CLI - determines whether to use Ink (interactive) or plain text mode
|
||||
*
|
||||
* This is extracted as a pure function for testability. The decision tree:
|
||||
* - Plain text mode when output is redirected (stdout not TTY)
|
||||
* - Plain text mode when input is redirected (stdin not TTY) - Ink requires raw mode
|
||||
* - Plain text mode when stdin was piped (e.g., echo "x" | cline)
|
||||
* - Plain text mode when --json flag is used
|
||||
* - Plain text mode when --yolo flag is used
|
||||
* - Otherwise: Interactive Ink mode
|
||||
*/
|
||||
|
||||
export interface ModeSelectionInput {
|
||||
/** Is stdout connected to a TTY (interactive terminal)? */
|
||||
stdoutIsTTY: boolean
|
||||
/** Is stdin connected to a TTY (interactive terminal)? */
|
||||
stdinIsTTY: boolean
|
||||
/** Was stdin piped (FIFO or file), even if empty? */
|
||||
stdinWasPiped: boolean
|
||||
/** --json flag for machine-readable output */
|
||||
json?: boolean
|
||||
/** --yolo flag for auto-approve mode */
|
||||
yolo?: boolean
|
||||
}
|
||||
|
||||
export interface ModeSelectionResult {
|
||||
/** Use plain text mode instead of Ink */
|
||||
usePlainTextMode: boolean
|
||||
/** Reason for the mode selection (for telemetry/debugging) */
|
||||
reason: "interactive" | "yolo_flag" | "json" | "piped_stdin" | "stdin_redirected" | "stdout_redirected"
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether to use plain text mode or interactive Ink mode
|
||||
*
|
||||
* @param input - Environment and option flags
|
||||
* @returns Mode selection result with reason
|
||||
*/
|
||||
export function selectOutputMode(input: ModeSelectionInput): ModeSelectionResult {
|
||||
// Priority order matters - check most specific flags first
|
||||
|
||||
if (input.yolo) {
|
||||
return { usePlainTextMode: true, reason: "yolo_flag" }
|
||||
}
|
||||
|
||||
if (input.json) {
|
||||
return { usePlainTextMode: true, reason: "json" }
|
||||
}
|
||||
|
||||
if (input.stdinWasPiped) {
|
||||
return { usePlainTextMode: true, reason: "piped_stdin" }
|
||||
}
|
||||
|
||||
if (!input.stdinIsTTY) {
|
||||
return { usePlainTextMode: true, reason: "stdin_redirected" }
|
||||
}
|
||||
|
||||
if (!input.stdoutIsTTY) {
|
||||
return { usePlainTextMode: true, reason: "stdout_redirected" }
|
||||
}
|
||||
|
||||
return { usePlainTextMode: false, reason: "interactive" }
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest"
|
||||
import { emitTaskStartedMessage } from "./task-start-output"
|
||||
|
||||
describe("emitTaskStartedMessage", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it("writes structured task_started JSON to stdout in json mode", () => {
|
||||
const stdoutWriteSpy = vi.spyOn(process.stdout, "write").mockImplementation(() => true)
|
||||
const stderrWriteSpy = vi.spyOn(process.stderr, "write").mockImplementation(() => true)
|
||||
|
||||
emitTaskStartedMessage("task-123", true)
|
||||
|
||||
expect(stdoutWriteSpy).toHaveBeenCalledWith('{"type":"task_started","taskId":"task-123"}\n')
|
||||
expect(stderrWriteSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("writes human-readable task started line to stderr in non-json mode", () => {
|
||||
const stdoutWriteSpy = vi.spyOn(process.stdout, "write").mockImplementation(() => true)
|
||||
const stderrWriteSpy = vi.spyOn(process.stderr, "write").mockImplementation(() => true)
|
||||
|
||||
emitTaskStartedMessage("task-456", false)
|
||||
|
||||
expect(stderrWriteSpy).toHaveBeenCalledWith("Task started: task-456\n")
|
||||
expect(stdoutWriteSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -12,24 +12,18 @@
|
||||
// Console output is intentional here for plain text mode
|
||||
|
||||
import type { ClineMessage, ExtensionState } from "@shared/ExtensionMessage"
|
||||
import { StringRequest } from "@shared/proto/cline/common"
|
||||
import type { Controller } from "@/core/controller"
|
||||
import { getRequestRegistry } from "@/core/controller/grpc-handler"
|
||||
import { subscribeToState } from "@/core/controller/state/subscribeToState"
|
||||
import { showTaskWithId } from "@/core/controller/task/showTaskWithId"
|
||||
import { emitTaskStartedMessage } from "./task-start-output"
|
||||
|
||||
export interface PlainTextTaskOptions {
|
||||
controller: Controller
|
||||
/** Prompt for new task or message to send to resumed task */
|
||||
prompt?: string
|
||||
prompt: string
|
||||
imageDataUrls?: string[]
|
||||
verbose?: boolean
|
||||
jsonOutput?: boolean
|
||||
/** Timeout in seconds (default: 600 = 10 minutes) */
|
||||
timeoutSeconds?: number
|
||||
/** Task ID to resume an existing task */
|
||||
taskId?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,39 +39,17 @@ export interface PlainTextTaskOptions {
|
||||
export async function runPlainTextTask(options: PlainTextTaskOptions): Promise<boolean> {
|
||||
const { controller, prompt, imageDataUrls, verbose, jsonOutput } = options
|
||||
|
||||
let completionResolve: (reason?: any) => void
|
||||
let completionResolve: () => void
|
||||
let completionReject: (reason?: any) => void
|
||||
const completionPromise = new Promise<string>((res, rej) => {
|
||||
const completionPromise = new Promise<void>((res, rej) => {
|
||||
completionResolve = res
|
||||
completionReject = rej
|
||||
})
|
||||
|
||||
let hasError = false
|
||||
let hasEmittedTaskStarted = false
|
||||
// Track which messages have been processed (by timestamp)
|
||||
const processedMessages = new Map<number, string>()
|
||||
|
||||
const isViewTaskOnly = Boolean(options.taskId) && !prompt
|
||||
|
||||
// When resuming a task, we need to ignore completion_result messages that existed
|
||||
// before we sent our new prompt. This timestamp marks the cutoff - only completion
|
||||
// results AFTER this time should trigger task completion.
|
||||
const completionCutoffTs = Date.now()
|
||||
|
||||
const emitTaskStarted = () => {
|
||||
if (hasEmittedTaskStarted) {
|
||||
return
|
||||
}
|
||||
|
||||
const taskId = controller.task?.taskId
|
||||
if (!taskId) {
|
||||
return
|
||||
}
|
||||
|
||||
emitTaskStartedMessage(taskId, Boolean(jsonOutput))
|
||||
hasEmittedTaskStarted = true
|
||||
}
|
||||
|
||||
// Helper to process a message and track completion state
|
||||
const processMessage = (message: ClineMessage) => {
|
||||
const ts = message.ts || 0
|
||||
@@ -95,12 +67,8 @@ export async function runPlainTextTask(options: PlainTextTaskOptions): Promise<b
|
||||
processedMessages.set(ts, message.text ?? "")
|
||||
|
||||
// Check for completion (only on non-partial messages)
|
||||
// When resuming a task, only consider completion_result messages that appeared
|
||||
// AFTER we sent our resume message (ts > completionCutoffTs)
|
||||
if (message.say === "completion_result" || message.ask === "completion_result") {
|
||||
if (isViewTaskOnly || ts > completionCutoffTs) {
|
||||
completionResolve()
|
||||
}
|
||||
completionResolve()
|
||||
} else if (message.say === "error" || message.ask === "api_req_failed") {
|
||||
completionReject(message.text ?? "message.say error || message.ask api_req_failed")
|
||||
}
|
||||
@@ -131,29 +99,8 @@ export async function runPlainTextTask(options: PlainTextTaskOptions): Promise<b
|
||||
)
|
||||
|
||||
try {
|
||||
// Either resume an existing task or start a new one
|
||||
if (options.taskId) {
|
||||
// Load the existing task
|
||||
await showTaskWithId(controller, StringRequest.create({ value: options.taskId }))
|
||||
emitTaskStarted()
|
||||
|
||||
// If a prompt was provided, send it as a message to the resumed task
|
||||
if (prompt && controller.task) {
|
||||
// Wait a moment for the task to fully load
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
// Send the prompt as a response to any pending ask, or as a new message
|
||||
await controller.task.handleWebviewAskResponse("messageResponse", prompt)
|
||||
}
|
||||
} else if (prompt) {
|
||||
// Start a new task with the prompt
|
||||
await controller.initTask(prompt, imageDataUrls)
|
||||
emitTaskStarted()
|
||||
} else {
|
||||
throw new Error("Either taskId or prompt must be provided")
|
||||
}
|
||||
|
||||
// Normal mode: wait for task completion
|
||||
// Start the task
|
||||
await controller.initTask(prompt, imageDataUrls)
|
||||
const timeoutMs = (options.timeoutSeconds ?? 600) * 1000 // default 10 minutes
|
||||
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), timeoutMs))
|
||||
await Promise.race([completionPromise, timeoutPromise])
|
||||
|
||||
@@ -3,10 +3,7 @@
|
||||
* Used by both UI components and CLI commands
|
||||
*/
|
||||
|
||||
import { useMemo } from "react"
|
||||
import { StateManager } from "@/core/storage/StateManager"
|
||||
import providersData from "@/shared/providers/providers.json"
|
||||
import type { RemoteConfigFields } from "@/shared/storage/state-keys"
|
||||
|
||||
// Create a lookup map from provider value to display label
|
||||
const providerLabels: Record<string, string> = Object.fromEntries(
|
||||
@@ -20,7 +17,7 @@ const providerOrder: string[] = providersData.list.map((p: { value: string }) =>
|
||||
* Providers that are not supported in CLI.
|
||||
* - vscode-lm: Requires VS Code's Language Model API (see ENG-1490 for OAuth-based support)
|
||||
*/
|
||||
const CLI_EXCLUDED_PROVIDERS = new Set<string>(["vscode-lm"])
|
||||
export const CLI_EXCLUDED_PROVIDERS = new Set<string>(["vscode-lm"])
|
||||
|
||||
/**
|
||||
* Get the display label for a provider ID
|
||||
@@ -32,7 +29,7 @@ export function getProviderLabel(providerId: string): string {
|
||||
/**
|
||||
* Get the ordered list of all provider IDs (from providers.json)
|
||||
*/
|
||||
function getProviderOrder(): string[] {
|
||||
export function getProviderOrder(): string[] {
|
||||
return providerOrder
|
||||
}
|
||||
|
||||
@@ -49,19 +46,3 @@ export function getValidCliProviders(): string[] {
|
||||
export function isValidCliProvider(providerId: string): boolean {
|
||||
return providerOrder.includes(providerId) && !CLI_EXCLUDED_PROVIDERS.has(providerId)
|
||||
}
|
||||
|
||||
const getValidProviders = (remoteConfig: Partial<RemoteConfigFields> | undefined) => {
|
||||
if (remoteConfig?.remoteConfiguredProviders?.length) {
|
||||
return remoteConfig.remoteConfiguredProviders
|
||||
}
|
||||
|
||||
return getProviderOrder().filter((p: string) => !CLI_EXCLUDED_PROVIDERS.has(p))
|
||||
}
|
||||
|
||||
export const useValidProviders = () => {
|
||||
const remoteConfig = StateManager.get().getRemoteConfigSettings()
|
||||
|
||||
return useMemo(() => {
|
||||
return getValidProviders(remoteConfig)
|
||||
}, [remoteConfig])
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
export function emitTaskStartedMessage(taskId: string, jsonOutput: boolean): void {
|
||||
if (jsonOutput) {
|
||||
process.stdout.write(JSON.stringify({ type: "task_started", taskId }) + "\n")
|
||||
return
|
||||
}
|
||||
|
||||
process.stderr.write(`Task started: ${taskId}\n`)
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
/**
|
||||
* Wait for a condition to become truthy, with a timeout.
|
||||
* Uses Promise.race for clean timeout handling instead of polling.
|
||||
*
|
||||
* @param condition - Function that returns the value to check (truthy = done)
|
||||
* @param timeoutMs - Maximum time to wait in milliseconds
|
||||
* @param pollIntervalMs - How often to check the condition (default: 100ms)
|
||||
* @returns The truthy value if condition is met, or undefined if timeout
|
||||
*/
|
||||
export async function waitFor<T>(
|
||||
condition: () => T | undefined | null,
|
||||
timeoutMs: number,
|
||||
pollIntervalMs: number = 100,
|
||||
): Promise<T | undefined> {
|
||||
// Check immediately first
|
||||
const immediate = condition()
|
||||
if (immediate) {
|
||||
return immediate
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const intervalId = setInterval(() => {
|
||||
const result = condition()
|
||||
if (result) {
|
||||
clearInterval(intervalId)
|
||||
clearTimeout(timeoutId)
|
||||
resolve(result)
|
||||
}
|
||||
}, pollIntervalMs)
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
clearInterval(intervalId)
|
||||
resolve(undefined)
|
||||
}, timeoutMs)
|
||||
})
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { spawn } from "node:child_process"
|
||||
import { realpathSync } from "node:fs"
|
||||
import { exit } from "node:process"
|
||||
import { ClineEndpoint } from "@/config"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { printInfo, printWarning } from "./display"
|
||||
|
||||
@@ -108,7 +107,7 @@ async function getLatestVersion(currentVersion: string): Promise<string | null>
|
||||
* process to install if a newer version is available.
|
||||
*
|
||||
* Supports npm, pnpm, yarn, and bun global installs.
|
||||
* Skipped for npx, local dev, unknown installations, and bundled enterprise packages.
|
||||
* Skipped for npx, local dev, and unknown installations.
|
||||
* Can be disabled with CLINE_NO_AUTO_UPDATE=1 environment variable.
|
||||
*/
|
||||
export function autoUpdateOnStartup(currentVersion: string): void {
|
||||
@@ -122,11 +121,6 @@ export function autoUpdateOnStartup(currentVersion: string): void {
|
||||
return
|
||||
}
|
||||
|
||||
// Skip if using bundled enterprise config (single source of truth)
|
||||
if (ClineEndpoint.isBundledConfig()) {
|
||||
return
|
||||
}
|
||||
|
||||
const { updateCommand } = getInstallationInfo(currentVersion)
|
||||
if (!updateCommand) {
|
||||
return
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
*/
|
||||
|
||||
import { mkdirSync } from "node:fs"
|
||||
import { fileURLToPath } from "node:url"
|
||||
import os from "os"
|
||||
import path from "path"
|
||||
import { ExtensionRegistryInfo } from "@/registry"
|
||||
@@ -12,10 +11,6 @@ import { ClineExtensionContext } from "@/shared/cline"
|
||||
import { ClineFileStorage } from "@/shared/storage"
|
||||
import { EnvironmentVariableCollection, ExtensionKind, ExtensionMode, readJson, URI } from "./vscode-shim"
|
||||
|
||||
// ES module equivalent of __dirname
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
||||
const SETTINGS_SUBFOLDER = "data"
|
||||
|
||||
/**
|
||||
@@ -145,8 +140,8 @@ export function initializeCliContext(config: CliContextConfig = {}): CliContextR
|
||||
mkdirSync(DATA_DIR, { recursive: true })
|
||||
mkdirSync(WORKSPACE_STORAGE_DIR, { recursive: true })
|
||||
|
||||
// For CLI, extension dir is the package root (one level up from dist/)
|
||||
const EXTENSION_DIR = path.resolve(__dirname, "..")
|
||||
// For CLI, extension dir is the root of the project (parent of cli)
|
||||
const EXTENSION_DIR = path.resolve(__dirname, "..", "..")
|
||||
const EXTENSION_MODE = process.env.IS_DEV === "true" ? ExtensionMode.Development : ExtensionMode.Production
|
||||
|
||||
const extension: ClineExtensionContext["extension"] = {
|
||||
|
||||
@@ -22,7 +22,7 @@ If you need to install or update Node.js, visit [nodejs.org](https://nodejs.org)
|
||||
Install globally via npm:
|
||||
|
||||
```bash
|
||||
npm install -g cline
|
||||
bun install -g cline
|
||||
```
|
||||
|
||||
Verify the installation:
|
||||
@@ -32,7 +32,7 @@ cline version
|
||||
```
|
||||
|
||||
<Tip>
|
||||
To install a specific version, use `npm install -g cline@2.0.0`. Check [npm](https://www.npmjs.com/package/cline) for available versions.
|
||||
To install a specific version, use `bun install -g cline@2.0.0`. Check [npm](https://www.npmjs.com/package/cline) for available versions.
|
||||
</Tip>
|
||||
|
||||
## Authenticate
|
||||
@@ -186,7 +186,7 @@ cline update
|
||||
Or update manually via npm:
|
||||
|
||||
```bash
|
||||
npm update -g cline
|
||||
bun update -g cline
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
@@ -195,14 +195,14 @@ npm update -g cline
|
||||
|
||||
If `cline` is not found after installation:
|
||||
|
||||
1. Ensure npm global bin is in your PATH:
|
||||
1. Ensure bun global bin is in your PATH:
|
||||
```bash
|
||||
npm bin -g
|
||||
bun bin -g
|
||||
```
|
||||
|
||||
2. Add the path to your shell configuration (`.bashrc`, `.zshrc`, etc.):
|
||||
```bash
|
||||
export PATH="$PATH:$(npm bin -g)"
|
||||
export PATH="$PATH:$(bun bin -g)"
|
||||
```
|
||||
|
||||
3. Restart your terminal or source your shell config.
|
||||
@@ -215,7 +215,7 @@ If you get permission errors during installation:
|
||||
# Option 1: Use a Node version manager (recommended)
|
||||
# nvm, fnm, or volta handle permissions automatically
|
||||
|
||||
# Option 2: Fix npm permissions
|
||||
# Option 2: Fix bun permissions
|
||||
# See: https://docs.npmjs.com/resolving-eacces-permissions-errors-when-installing-packages-globally
|
||||
```
|
||||
|
||||
@@ -244,7 +244,7 @@ If your API key is rejected:
|
||||
To remove Cline CLI:
|
||||
|
||||
```bash
|
||||
npm uninstall -g cline
|
||||
bun uninstall -g cline
|
||||
```
|
||||
|
||||
To also remove configuration data:
|
||||
|
||||
@@ -164,7 +164,6 @@
|
||||
"features/multiroot-workspace",
|
||||
"features/plan-and-act",
|
||||
"features/skills",
|
||||
"features/subagents",
|
||||
{
|
||||
"group": "Slash Commands",
|
||||
"pages": [
|
||||
|
||||
@@ -1,287 +0,0 @@
|
||||
---
|
||||
title: "Bundled Endpoints Configuration"
|
||||
description: "Enterprise guide for distributing Cline with pre-configured endpoints"
|
||||
---
|
||||
|
||||
# Bundled Endpoints Configuration
|
||||
|
||||
This guide explains how enterprise customers can distribute Cline with pre-configured endpoints bundled directly into the installation packages.
|
||||
|
||||
## Overview
|
||||
|
||||
Cline supports bundling custom endpoint configurations directly into distribution packages (VSIX, NPM, or JetBrains). This eliminates the need for end users to manually configure endpoints, ensuring consistent configuration across your organization.
|
||||
|
||||
### Configuration Priority
|
||||
|
||||
When Cline starts, it checks for endpoints configuration in this order:
|
||||
|
||||
1. **Bundled endpoints.json** (in extension installation directory) - Highest priority
|
||||
2. **User endpoints.json** (`~/.cline/endpoints.json`) - Fallback
|
||||
3. **Built-in endpoints** (standard Cline URLs) - Default
|
||||
|
||||
When a bundled `endpoints.json` is found, Cline automatically switches to self-hosted mode and uses those endpoints exclusively.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Official Cline release package (VSIX, TGZ, or ZIP)
|
||||
- Your `endpoints.json` configuration file
|
||||
- `jq` command-line tool (for JSON validation)
|
||||
- `unzip`, `zip`, `tar` utilities
|
||||
|
||||
## Creating endpoints.json
|
||||
|
||||
Create a JSON file with your organization's endpoints:
|
||||
|
||||
```json
|
||||
{
|
||||
"appBaseUrl": "https://cline.yourcompany.com",
|
||||
"apiBaseUrl": "https://api-cline.yourcompany.com",
|
||||
"mcpBaseUrl": "https://api-cline.yourcompany.com/v1/mcp"
|
||||
}
|
||||
```
|
||||
|
||||
### Required Fields
|
||||
|
||||
All three fields are required and must be valid URLs:
|
||||
|
||||
- **appBaseUrl**: Web application base URL
|
||||
- **apiBaseUrl**: API server base URL
|
||||
- **mcpBaseUrl**: MCP (Model Context Protocol) server URL
|
||||
|
||||
### Validation
|
||||
|
||||
The packaging scripts automatically validate:
|
||||
- Valid JSON syntax
|
||||
- All required fields present
|
||||
- Non-empty string values
|
||||
- Valid URL format (must start with `http://` or `https://`)
|
||||
|
||||
## Packaging Scripts
|
||||
|
||||
Cline provides three scripts for adding bundled endpoints to packages:
|
||||
|
||||
### VSCode Extension (VSIX)
|
||||
|
||||
```bash
|
||||
./scripts/add-endpoints-to-vsix.sh \
|
||||
cline-3.55.0.vsix \
|
||||
cline-3.55.0-enterprise.vsix \
|
||||
endpoints.json
|
||||
```
|
||||
|
||||
This script:
|
||||
1. Extracts the VSIX package
|
||||
2. Adds `endpoints.json` to the `extension/` directory
|
||||
3. Repackages as a new VSIX file
|
||||
|
||||
### NPM Package (CLI)
|
||||
|
||||
```bash
|
||||
./scripts/add-endpoints-to-npm.sh \
|
||||
cline-3.55.0.tgz \
|
||||
cline-3.55.0-enterprise.tgz \
|
||||
endpoints.json
|
||||
```
|
||||
|
||||
This script:
|
||||
1. Extracts the NPM tarball
|
||||
2. Adds `endpoints.json` to the package root
|
||||
3. Repackages as a new tarball
|
||||
|
||||
### JetBrains Plugin (ZIP)
|
||||
|
||||
```bash
|
||||
./scripts/add-endpoints-to-jetbrains.sh \
|
||||
cline-jetbrains-3.55.0.zip \
|
||||
cline-jetbrains-3.55.0-enterprise.zip \
|
||||
endpoints.json
|
||||
```
|
||||
|
||||
This script:
|
||||
1. Extracts the ZIP package
|
||||
2. Adds `endpoints.json` to the plugin directory
|
||||
3. Repackages as a new ZIP file
|
||||
|
||||
## Distribution Workflow
|
||||
|
||||
### 1. Download Official Release
|
||||
|
||||
Download the official Cline package for your platform:
|
||||
|
||||
```bash
|
||||
# VSCode - from marketplace or GitHub releases
|
||||
curl -LO https://github.com/cline/cline/releases/download/v3.55.0/cline-3.55.0.vsix
|
||||
|
||||
# NPM - from npm registry
|
||||
npm pack @cline/cline@3.55.0
|
||||
|
||||
# JetBrains - from marketplace or GitHub releases
|
||||
curl -LO https://github.com/cline/cline/releases/download/v3.55.0/cline-jetbrains-3.55.0.zip
|
||||
```
|
||||
|
||||
### 2. Create Endpoints Configuration
|
||||
|
||||
Create your `endpoints.json` file:
|
||||
|
||||
```json
|
||||
{
|
||||
"appBaseUrl": "https://cline.internal.company.com",
|
||||
"apiBaseUrl": "https://cline-api.internal.company.com",
|
||||
"mcpBaseUrl": "https://cline-api.internal.company.com/v1/mcp"
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Run Packaging Script
|
||||
|
||||
Choose the appropriate script for your platform:
|
||||
|
||||
```bash
|
||||
# VSCode
|
||||
./scripts/add-endpoints-to-vsix.sh \
|
||||
cline-3.55.0.vsix \
|
||||
cline-3.55.0-yourcompany.vsix \
|
||||
endpoints.json
|
||||
|
||||
# CLI
|
||||
./scripts/add-endpoints-to-npm.sh \
|
||||
cline-3.55.0.tgz \
|
||||
cline-3.55.0-yourcompany.tgz \
|
||||
endpoints.json
|
||||
|
||||
# JetBrains
|
||||
./scripts/add-endpoints-to-jetbrains.sh \
|
||||
cline-jetbrains-3.55.0.zip \
|
||||
cline-jetbrains-3.55.0-yourcompany.zip \
|
||||
endpoints.json
|
||||
```
|
||||
|
||||
### 4. Distribute to Users
|
||||
|
||||
Distribute the enterprise package to your users through your internal channels:
|
||||
|
||||
- **VSCode**: Install via `code --install-extension cline-3.55.0-yourcompany.vsix`
|
||||
- **CLI**: Install via `npm install -g cline-3.55.0-yourcompany.tgz`
|
||||
- **JetBrains**: Install through IDE plugin manager from disk
|
||||
|
||||
## Verification
|
||||
|
||||
After installation, verify the configuration is active:
|
||||
|
||||
1. Launch Cline
|
||||
2. Check the logs for: `"Cline running in self-hosted mode with custom endpoints"`
|
||||
3. Confirm that environment switching is disabled (as expected in self-hosted mode)
|
||||
|
||||
## User Experience
|
||||
|
||||
### What Users See
|
||||
|
||||
- Cline automatically uses the bundled endpoints
|
||||
- No manual configuration required
|
||||
- Environment switching is disabled (prevents accidental misconfiguration)
|
||||
- All API calls route to your organization's infrastructure
|
||||
|
||||
### User Override
|
||||
|
||||
Users **cannot** override bundled endpoints through the UI. The bundled configuration takes absolute precedence. This ensures:
|
||||
- Consistent configuration across the organization
|
||||
- No accidental connections to external services
|
||||
- Simplified deployment and support
|
||||
|
||||
If users have a `~/.cline/endpoints.json` file, it will be ignored when bundled configuration is present.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Invalid Configuration Error
|
||||
|
||||
If users see an error about invalid configuration on startup:
|
||||
|
||||
```
|
||||
ClineConfigurationError: Invalid JSON in bundled endpoints configuration file
|
||||
```
|
||||
|
||||
**Solution**: The bundled `endpoints.json` is malformed. Repackage with a valid JSON file.
|
||||
|
||||
### Missing Required Field Error
|
||||
|
||||
```
|
||||
ClineConfigurationError: Missing required field "apiBaseUrl" in endpoints configuration file
|
||||
```
|
||||
|
||||
**Solution**: Ensure all three required fields are present in `endpoints.json`.
|
||||
|
||||
### Invalid URL Error
|
||||
|
||||
```
|
||||
ClineConfigurationError: Field "appBaseUrl" must be a valid URL. Got: "not-a-url"
|
||||
```
|
||||
|
||||
**Solution**: All URLs must start with `http://` or `https://`.
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Bundle Validation**: The packaging scripts validate JSON structure and required fields
|
||||
2. **Read-Only Configuration**: Users cannot modify bundled endpoints through the UI
|
||||
3. **Self-Hosted Mode**: Automatic switch to self-hosted mode prevents external connections
|
||||
4. **Audit Trail**: All endpoint access is logged with configuration source
|
||||
|
||||
## Updating Endpoints
|
||||
|
||||
To update endpoints for existing installations:
|
||||
|
||||
1. Create updated `endpoints.json`
|
||||
2. Repackage the same Cline version with new endpoints
|
||||
3. Distribute updated package
|
||||
4. Users reinstall/update the package
|
||||
|
||||
The version number remains the same since only configuration changed, not the Cline code.
|
||||
|
||||
## Support
|
||||
|
||||
For questions or issues with bundled endpoints:
|
||||
|
||||
1. Verify your `endpoints.json` is valid JSON with all required fields
|
||||
2. Check that URLs are accessible from user networks
|
||||
3. Review Cline logs for configuration loading messages
|
||||
4. Contact your Cline support representative for assistance
|
||||
|
||||
## Example: Complete Workflow
|
||||
|
||||
Here's a complete example for VSCode deployment:
|
||||
|
||||
```bash
|
||||
# 1. Download official release
|
||||
curl -LO https://github.com/cline/cline/releases/download/v3.55.0/cline-3.55.0.vsix
|
||||
|
||||
# 2. Create endpoints configuration
|
||||
cat > endpoints.json << 'EOF'
|
||||
{
|
||||
"appBaseUrl": "https://cline.acme.internal",
|
||||
"apiBaseUrl": "https://cline-api.acme.internal",
|
||||
"mcpBaseUrl": "https://cline-api.acme.internal/v1/mcp"
|
||||
}
|
||||
EOF
|
||||
|
||||
# 3. Validate JSON
|
||||
jq empty endpoints.json # Should succeed silently
|
||||
|
||||
# 4. Run packaging script
|
||||
./scripts/add-endpoints-to-vsix.sh \
|
||||
cline-3.55.0.vsix \
|
||||
cline-3.55.0-acme.vsix \
|
||||
endpoints.json
|
||||
|
||||
# 5. Verify output
|
||||
unzip -l cline-3.55.0-acme.vsix | grep endpoints.json
|
||||
# Should show: extension/endpoints.json
|
||||
|
||||
# 6. Test installation (on test machine)
|
||||
code --install-extension cline-3.55.0-acme.vsix
|
||||
|
||||
# 7. Distribute to organization
|
||||
# Upload to internal package repository
|
||||
# or distribute via configuration management system
|
||||
```
|
||||
|
||||
## Changelog
|
||||
|
||||
- **v3.55.0**: Initial release of bundled endpoints support
|
||||
@@ -1,83 +0,0 @@
|
||||
---
|
||||
title: "Subagents"
|
||||
sidebarTitle: "Subagents"
|
||||
description: "Run parallel research agents to explore your codebase without filling the main agent's context window."
|
||||
---
|
||||
|
||||
Subagents let Cline spawn focused research agents that run in parallel. Each subagent gets its own prompt and context window, explores the codebase independently, and returns a detailed report to the main agent. This keeps the main agent's context clean while gathering broad information fast.
|
||||
|
||||
<Tip>
|
||||
Subagents is an experimental feature. Behavior may change in future releases.
|
||||
</Tip>
|
||||
|
||||
## How It Works
|
||||
|
||||
When Cline uses the `use_subagents` tool, it launches independent agents simultaneously. Each one:
|
||||
|
||||
- Gets its own prompt describing what to investigate
|
||||
- Runs with a separate context window and token budget
|
||||
- Can read files, search code, list directories, run read-only commands, and use skills
|
||||
- Cannot edit files, use the browser, access MCP servers, or spawn nested subagents
|
||||
- Returns a result that includes file paths, line numbers, and recommended files for the main agent to read next
|
||||
|
||||
Subagent costs (tokens and API spend) are tracked separately per subagent and rolled into the task's total cost. You can see per-subagent stats (tool calls, tokens, cost) in the chat UI as they run.
|
||||
|
||||
## Enabling Subagents
|
||||
|
||||
Subagents are disabled by default. To turn them on:
|
||||
|
||||
1. Open Cline Settings (click the gear icon in the Cline panel)
|
||||
2. Go to **Features**
|
||||
3. Under the **Agent** section, toggle **Subagents** on
|
||||
|
||||
This setting applies across all editors (VS Code, JetBrains, CLI).
|
||||
|
||||
## Using Subagents
|
||||
|
||||
Cline does not automatically decide to use subagents. You need to ask for them in your prompt. When the feature is enabled and you mention subagents (or describe a task that benefits from parallel exploration), Cline will use the `use_subagents` tool.
|
||||
|
||||
Example prompts:
|
||||
|
||||
- "Use subagents to explore how authentication works and where the database models are defined"
|
||||
- "Spin up subagents to investigate the API routes, the test setup, and the deployment config"
|
||||
- "I'm new to this codebase. Use subagents to map out the main entry points, the routing layer, and the data access patterns"
|
||||
|
||||
Each subagent prompt should describe a focused research question. Cline will run them in parallel and synthesize the results.
|
||||
|
||||
## Auto-Approve Behavior
|
||||
|
||||
Subagents follow the **Read project files** auto-approve permission. If you have "Read project files" enabled in [Auto Approve](/features/auto-approve), subagent launches will be auto-approved.
|
||||
|
||||
In [YOLO mode](/features/yolo-mode), subagents are always auto-approved.
|
||||
|
||||
If auto-approve is off, Cline will ask for your approval before launching subagents, showing you the prompts it plans to send.
|
||||
|
||||
## What Subagents Can Do
|
||||
|
||||
Subagents are read-only research agents. Here is what they have access to:
|
||||
|
||||
| Tool | Purpose |
|
||||
|------|---------|
|
||||
| `read_file` | Read file contents |
|
||||
| `list_files` | List directory contents |
|
||||
| `search_files` | Regex search across files |
|
||||
| `list_code_definition_names` | List top-level classes, functions, and methods |
|
||||
| `execute_command` | Run read-only commands (`ls`, `grep`, `git log`, `git diff`, etc.) |
|
||||
| `use_skill` | Load and activate skills |
|
||||
|
||||
Subagents cannot write files, apply patches, use the browser, access MCP servers, or perform web searches. They also cannot spawn their own subagents.
|
||||
|
||||
<Note>
|
||||
Commands run by subagents execute in the background and are restricted to read-only operations. Subagents will not run commands that modify files or system state.
|
||||
</Note>
|
||||
|
||||
## When to Use Subagents
|
||||
|
||||
Subagents work best when you need broad context from multiple areas of a codebase at once:
|
||||
|
||||
- **Onboarding to an unfamiliar project**: Ask subagents to map out the architecture, key entry points, and data flow in parallel.
|
||||
- **Investigating cross-cutting concerns**: Have separate subagents trace authentication, logging, and error handling simultaneously.
|
||||
- **Pre-edit research**: Before making changes, use subagents to gather context from related files so the main agent can make informed edits without burning through its context window.
|
||||
- **Large codebases**: When reading many files sequentially would consume too much of the main agent's context, subagents let you explore broadly without that tradeoff.
|
||||
|
||||
For small, focused tasks where you already know which files to look at, subagents add unnecessary overhead. Just ask Cline directly.
|
||||
@@ -281,7 +281,7 @@ We began by bootstrapping the project:
|
||||
```bash
|
||||
npx @modelcontextprotocol/create-server alphaadvantage-mcp
|
||||
cd alphaadvantage-mcp
|
||||
npm install axios node-cache
|
||||
bun install axios node-cache
|
||||
```
|
||||
|
||||
Next, we structured our project with:
|
||||
|
||||
@@ -53,7 +53,7 @@ Vertex AI supports multiple regions. Select a region that meets your latency, co
|
||||
- **asia-southeast1 (Singapore)**
|
||||
- **global (Global)**
|
||||
|
||||
The Global endpoint may offer higher availability and reduce resource exhausted errors. Gemini models and supported Claude models can use it, depending on model availability in your project.
|
||||
The Global endpoint may offer higher availability and reduce resource exhausted errors. Only Gemini models are supported.
|
||||
|
||||
#### 2.2 Enable the Claude 3.5 Sonnet v2 Model
|
||||
|
||||
|
||||
@@ -0,0 +1,342 @@
|
||||
# Implementation Plan: Cline CLI Documentation Update
|
||||
|
||||
[Overview]
|
||||
Update the Cline CLI documentation to reflect the new CLI 2.0 architecture that removes instances, adds a rich TUI experience, and introduces streamlined authentication options.
|
||||
|
||||
The Cline CLI 2.0 has undergone significant changes. The previous architecture used explicit instance management (`cline instance new`, `cline instance list`, etc.) which has been completely removed. The new architecture simplifies the user experience:
|
||||
|
||||
1. **TUI Mode**: Running `cline` without arguments launches a full-featured terminal UI built with React Ink, featuring an animated robot, file mentions (@), slash commands (/), session summaries, and inline settings panels. This provides a "Claude Code-like" experience.
|
||||
|
||||
2. **CLI Mode**: Running `cline "prompt"` executes tasks directly. With `--yolo` flag, it runs non-interactively with output to stdout, making it ideal for CI/CD, piping, and bash scripts.
|
||||
|
||||
3. **Authentication**: Multiple options including Cline account OAuth, ChatGPT subscription OAuth (via Codex), import from existing CLI tools (Codex CLI, OpenCode), and BYO API keys. Supports all providers from the VS Code extension (superset).
|
||||
|
||||
The documentation must clearly separate these two user journeys (TUI interactive vs CLI automation) while documenting deprecated features for users migrating from older versions.
|
||||
|
||||
**Note:** The CLI is now generally available (no longer preview) and supports macOS, Linux, and Windows.
|
||||
|
||||
[Types]
|
||||
No code type changes required - this is a documentation-only update.
|
||||
|
||||
This implementation plan only covers documentation files (`.mdx` files in `docs/cline-cli/`). No TypeScript interfaces, types, or code modifications are needed.
|
||||
|
||||
[Files]
|
||||
Update existing files and create new documentation pages for comprehensive coverage.
|
||||
|
||||
**Files to UPDATE (in-place):**
|
||||
- `docs/cline-cli/overview.mdx` - Remove instance references, reframe around TUI vs CLI modes
|
||||
- `docs/cline-cli/installation.mdx` - Expand with prerequisites, post-install steps, authentication
|
||||
- `docs/cline-cli/three-core-flows.mdx` - Complete rewrite to remove instances, replace with TUI/CLI/Automation flows
|
||||
- `docs/cline-cli/cli-reference.mdx` - Replace outdated man page content with current man page from `cli/man/cline.1.md`
|
||||
|
||||
**Files to CREATE:**
|
||||
- `docs/cline-cli/tui-guide.mdx` - New comprehensive guide for the TUI experience
|
||||
- `docs/cline-cli/authentication.mdx` - New guide covering all auth options
|
||||
- `docs/cline-cli/configuration.mdx` - New guide for `cline config` and settings management
|
||||
|
||||
**Files to MODIFY:**
|
||||
- `docs/docs.json` - Add new pages to navigation under CLI group
|
||||
|
||||
[Functions]
|
||||
No function changes required - documentation only.
|
||||
|
||||
This is a documentation update with no code changes to functions, methods, or handlers.
|
||||
|
||||
[Classes]
|
||||
No class changes required - documentation only.
|
||||
|
||||
This is a documentation update with no code changes to classes or components.
|
||||
|
||||
[Dependencies]
|
||||
No dependency changes required.
|
||||
|
||||
This is a documentation update with no package changes.
|
||||
|
||||
[Testing]
|
||||
Documentation should be verified for accuracy by cross-referencing with source code.
|
||||
|
||||
**Verification steps:**
|
||||
1. Cross-reference all documented features against `cli/src/index.ts` entry point
|
||||
2. Verify keyboard shortcuts against `cli/src/components/ChatView.tsx`
|
||||
3. Verify auth options against `cli/src/components/AuthView.tsx`
|
||||
4. Verify slash commands against `cli/src/components/HelpPanelContent.tsx`
|
||||
5. Verify config options against `cli/src/components/ConfigView.tsx` and `SettingsPanelContent.tsx`
|
||||
6. Verify import sources against `cli/src/utils/import-configs.ts`
|
||||
7. Run `bun run docs:dev` (if available) to preview documentation locally
|
||||
|
||||
**Content accuracy checks:**
|
||||
- [ ] All keyboard shortcuts match source code
|
||||
- [ ] All command flags match `cli/src/index.ts`
|
||||
- [ ] Auth provider list matches `AuthView.tsx`
|
||||
- [ ] Import sources correctly documented (Codex CLI, OpenCode - NOT "Claude Code")
|
||||
- [ ] Deprecated features clearly marked
|
||||
|
||||
[Implementation Order]
|
||||
Execute documentation updates in dependency order to ensure consistency.
|
||||
|
||||
1. **Update `docs/docs.json`** - Add new page entries to navigation first so links work
|
||||
2. **Create `docs/cline-cli/authentication.mdx`** - Auth is foundational, other docs reference it
|
||||
3. **Create `docs/cline-cli/tui-guide.mdx`** - Core new content for TUI users
|
||||
4. **Create `docs/cline-cli/configuration.mdx`** - Config management guide
|
||||
5. **Update `docs/cline-cli/overview.mdx`** - Reframe overview with new architecture
|
||||
6. **Update `docs/cline-cli/installation.mdx`** - Expand installation guide
|
||||
7. **Update `docs/cline-cli/three-core-flows.mdx`** - Rewrite as TUI/CLI/Automation flows
|
||||
8. **Update `docs/cline-cli/cli-reference.mdx`** - Replace with current man page content
|
||||
9. **Verify all cross-references and links work correctly**
|
||||
|
||||
---
|
||||
|
||||
## Detailed File Specifications
|
||||
|
||||
### 1. `docs/docs.json` (UPDATE)
|
||||
|
||||
Add new pages to the CLI navigation group:
|
||||
|
||||
```json
|
||||
{
|
||||
"group": "CLI",
|
||||
"pages": [
|
||||
"cline-cli/overview",
|
||||
"cline-cli/installation",
|
||||
"cline-cli/authentication",
|
||||
"cline-cli/tui-guide",
|
||||
"cline-cli/configuration",
|
||||
"cline-cli/three-core-flows",
|
||||
{
|
||||
"group": "CLI Samples",
|
||||
"pages": [
|
||||
"cline-cli/samples/overview",
|
||||
"cline-cli/samples/github-issue-rca",
|
||||
"cline-cli/samples/github-integration"
|
||||
]
|
||||
},
|
||||
"cline-cli/cli-reference"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 2. `docs/cline-cli/authentication.mdx` (CREATE)
|
||||
|
||||
**Purpose:** Comprehensive guide to all authentication options
|
||||
|
||||
**Sections:**
|
||||
- Quick start (sign in with Cline - recommended)
|
||||
- Sign in with ChatGPT subscription (OpenAI Codex OAuth)
|
||||
- Import from existing CLI tools:
|
||||
- Import from Codex CLI (`~/.codex/auth.json`)
|
||||
- Import from OpenCode (`~/.local/share/opencode/auth.json`)
|
||||
- Bring your own API keys (manual provider configuration)
|
||||
- Supported providers list with examples
|
||||
- Switching providers (`cline auth`)
|
||||
- Quick setup flags (`cline auth -p <provider> -k <key> -m <model>`)
|
||||
|
||||
**Key corrections from user input:**
|
||||
- User said "import from Claude Code" - INCORRECT. Actual sources are:
|
||||
- Codex CLI (OpenAI's CLI tool)
|
||||
- OpenCode
|
||||
- Document the actual import sources from `cli/src/utils/import-configs.ts`
|
||||
|
||||
### 3. `docs/cline-cli/tui-guide.mdx` (CREATE)
|
||||
|
||||
**Purpose:** Guide to the interactive terminal UI experience
|
||||
|
||||
**Sections:**
|
||||
- Launching the TUI (`cline` without arguments)
|
||||
- The welcome screen and robot animation
|
||||
- Input field and message display
|
||||
- Keyboard shortcuts:
|
||||
- `Tab` - Toggle Plan/Act mode
|
||||
- `Shift+Tab` - Toggle auto-approve all
|
||||
- `Enter` - Submit message
|
||||
- `Esc` - Exit/cancel
|
||||
- `↑/↓` - Navigate history
|
||||
- `Home/End` - Cursor movement
|
||||
- `Ctrl+A/E/W/U` - Text editing
|
||||
- File mentions with `@`:
|
||||
- Type `@` to search workspace files
|
||||
- Uses ripgrep for fast searching
|
||||
- Slash commands with `/`:
|
||||
- `/settings` - Open settings panel
|
||||
- `/models` - Quick model switching
|
||||
- `/history` - Browse task history
|
||||
- `/clear` - Start fresh task
|
||||
- `/help` - Show help
|
||||
- `/exit` - Exit CLI
|
||||
- Workflow commands
|
||||
- Settings panel (`/settings`):
|
||||
- API tab (provider, model, thinking)
|
||||
- Auto-approve tab
|
||||
- Features tab
|
||||
- Account tab
|
||||
- Other tab
|
||||
- Session summary on exit
|
||||
- Running multiple instances with `--config`:
|
||||
- Default: settings shared across all instances
|
||||
- Use `cline --config /path/to/config` for isolated configs
|
||||
- Recommend tmux/terminal multiplexing for parallel work
|
||||
|
||||
### 4. `docs/cline-cli/configuration.mdx` (CREATE)
|
||||
|
||||
**Purpose:** Guide to `cline config` command and settings management
|
||||
|
||||
**Sections:**
|
||||
- Running `cline config`
|
||||
- Configuration tabs:
|
||||
- Settings (global state, workspace state)
|
||||
- Rules (`.clinerules` files, Cursor rules, Windsurf rules)
|
||||
- Workflows
|
||||
- Hooks (if enabled)
|
||||
- Skills (if enabled)
|
||||
- Keyboard navigation in config view
|
||||
- Editing configuration values
|
||||
- Configuration directory structure (`~/.cline/data/`)
|
||||
- Environment variables (`CLINE_DIR`, `CLINE_COMMAND_PERMISSIONS`)
|
||||
- Using `--config` flag for separate configurations
|
||||
|
||||
### 5. `docs/cline-cli/overview.mdx` (UPDATE)
|
||||
|
||||
**Changes:**
|
||||
- Remove all references to instances (`cline instance new/list/kill`)
|
||||
- Reframe around two modes: TUI (interactive) and CLI (automation)
|
||||
- Update "What you can build" section to remove multi-instance examples
|
||||
- Add section about new TUI features
|
||||
- Link to new authentication and TUI guide pages
|
||||
- Note deprecation of instance commands
|
||||
|
||||
**New structure:**
|
||||
1. What is Cline CLI?
|
||||
2. Two ways to use Cline CLI:
|
||||
- TUI Mode (interactive development)
|
||||
- CLI Mode (automation and scripting)
|
||||
3. Supported Model Providers
|
||||
4. What you can build
|
||||
5. Learn more (links)
|
||||
|
||||
### 6. `docs/cline-cli/installation.mdx` (UPDATE)
|
||||
|
||||
**Changes:**
|
||||
- Remove "Preview Release - macOS and Linux Only" warning (CLI is now GA and supports Windows)
|
||||
- Add note that CLI supports macOS, Linux, and Windows
|
||||
- Add Node.js version requirement (20+, recommend 22)
|
||||
- Add version specification (`bun install -g cline@2.0.0`)
|
||||
- Add more detail on post-install authentication
|
||||
- Link to new authentication guide
|
||||
- Add troubleshooting tips
|
||||
- Add verification steps
|
||||
|
||||
**New structure:**
|
||||
1. Prerequisites (Node.js version)
|
||||
2. Installation: `bun install -g cline` (or `bun install -g cline@2.0.0`)
|
||||
3. Authentication (`cline auth` - link to auth guide)
|
||||
4. Quick Start (two paths: TUI and CLI)
|
||||
5. Next Steps (links to guides)
|
||||
|
||||
### 7. `docs/cline-cli/three-core-flows.mdx` (UPDATE - Major Rewrite)
|
||||
|
||||
**Complete rewrite removing all instance references.**
|
||||
|
||||
**New title suggestion:** "CLI Workflows" or "Getting Started Workflows"
|
||||
|
||||
**New structure:**
|
||||
1. **Interactive TUI Mode** (replaces old "Interactive mode")
|
||||
- Launch with `cline`
|
||||
- Plan/Act mode toggle (Tab key)
|
||||
- Using slash commands and file mentions
|
||||
- Auto-approve toggle (Shift+Tab)
|
||||
- Session summary on exit (Ctrl+C)
|
||||
2. **Direct Task Execution** (replaces old "Headless single-shot")
|
||||
- `cline "prompt"` syntax
|
||||
- Piping context (`cat file | cline "explain"`)
|
||||
- Piping cline into cline: `git diff | cline -y "explain" | cline -y "write poem"`
|
||||
- Image attachments
|
||||
3. **Automation & CI/CD** (replaces old "Multi-instance")
|
||||
- `--yolo` / `-y` flag for non-interactive mode (also called "yes mode")
|
||||
- `--json` output for parsing (same format as `~/.cline/data/tasks/<id>/ui_messages.json`)
|
||||
- `--timeout` for long-running tasks
|
||||
- Environment variables:
|
||||
- `CLINE_DIR` - custom config directory
|
||||
- `CLINE_COMMAND_PERMISSIONS` - restrict allowed shell commands
|
||||
- Example GitHub Actions workflow for PR review
|
||||
|
||||
**Creative use cases from engineer demo:**
|
||||
- Chain cline commands: `git diff | cline -y "explain" | cline -y "write a poem about this"`
|
||||
- GitHub PR review workflow with `gh` CLI integration
|
||||
|
||||
**Deprecation notice:**
|
||||
Add a callout at the top noting that instance commands (`cline instance new/list/kill`) have been removed in favor of the simpler architecture.
|
||||
|
||||
### 8. `docs/cline-cli/cli-reference.mdx` (UPDATE)
|
||||
|
||||
**Changes:**
|
||||
- Replace the outdated embedded man page with content from `cli/man/cline.1.md`
|
||||
- The current man page in the docs references old instance commands
|
||||
- The actual man page (`cli/man/cline.1.md`) has correct, updated content
|
||||
- Convert man page markdown format to mdx documentation format
|
||||
- Add JSON output schema section
|
||||
- Add environment variables section
|
||||
- Remove all instance command references
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## Additional Features from Engineer Demo
|
||||
|
||||
### Man Page
|
||||
- `man cline` - View in-depth documentation in terminal
|
||||
|
||||
### Dev Tools
|
||||
- `cline dev log` - Opens log file for debugging
|
||||
- `cline update` - Check for and install updates
|
||||
|
||||
### JSON Output Format
|
||||
- Same format as saved task files: `~/.cline/data/tasks/<id>/ui_messages.json`
|
||||
- Useful for programmatic use cases
|
||||
- Pipe through `jq` for easier parsing
|
||||
- Example: `cline --json "prompt" | jq '.text'`
|
||||
|
||||
---
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
After implementation, verify these user requirements are documented:
|
||||
|
||||
- [x] New TUI experience explained
|
||||
- [x] bun installation covered
|
||||
- [x] Authorization options:
|
||||
- [x] Sign in with Cline
|
||||
- [x] Sign in with ChatGPT Subscription (Codex OAuth)
|
||||
- [x] Import from Codex CLI (CORRECTED from "Claude Code")
|
||||
- [x] Import from OpenCode
|
||||
- [x] Bring your own API keys
|
||||
- [x] Bedrock support mentioned
|
||||
- [x] `cline auth` for changing providers
|
||||
- [x] Basic CLI usage:
|
||||
- [x] `cline "task"` syntax
|
||||
- [x] Piping context
|
||||
- [x] `--yolo` / `-y` for CI/CD (also called "yes mode")
|
||||
- [x] TUI features:
|
||||
- [x] `cline` alone launches TUI
|
||||
- [x] Tab to toggle Plan/Act mode
|
||||
- [x] Shift+Tab for auto-approve all
|
||||
- [x] Session summary on exit (Ctrl+C)
|
||||
- [x] `--config` for separate configs
|
||||
- [x] Instance deprecation noted
|
||||
- [x] `cline config` for rules, workflows, hooks, skills
|
||||
- [x] @ file mentions with autocomplete (fuzzy search)
|
||||
- [x] / slash commands with autocomplete
|
||||
- [x] `/settings` documented
|
||||
- [x] `/models` documented
|
||||
- [x] `/history` documented
|
||||
- [x] Workflows generate slash commands
|
||||
- [x] /settings panel sections documented (arrow keys to navigate tabs)
|
||||
- [x] Environment variables:
|
||||
- [x] `CLINE_DIR` documented
|
||||
- [x] `CLINE_COMMAND_PERMISSIONS` documented (security measure)
|
||||
- [x] Dev tools:
|
||||
- [x] `cline dev log` documented
|
||||
- [x] `cline update` documented
|
||||
- [x] `man cline` documented
|
||||
- [x] JSON output format documented
|
||||
- [x] Piping cline into cline documented
|
||||
- [x] GitHub Actions PR review example included
|
||||
Generated
-23609
File diff suppressed because it is too large
Load Diff
+51
-50
@@ -2,10 +2,11 @@
|
||||
"name": "claude-dev",
|
||||
"displayName": "Cline",
|
||||
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
|
||||
"version": "3.58.0",
|
||||
"version": "3.56.2",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"workspaces": [
|
||||
"cli"
|
||||
"cli",
|
||||
"webview-ui"
|
||||
],
|
||||
"engines": {
|
||||
"vscode": "^1.84.0"
|
||||
@@ -379,74 +380,73 @@
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"vscode:prepublish": "npm run package",
|
||||
"compile": "npm run check-types && npm run lint && node esbuild.mjs",
|
||||
"compile-standalone": "npm run check-types && npm run lint && node esbuild.mjs --standalone",
|
||||
"compile-standalone-npm": "npm run protos && npm run check-types && npm run lint && node esbuild.mjs --standalone",
|
||||
"cli:link": "cd cli && npm run link",
|
||||
"cli:build": "npm run protos && cd cli && npm run build",
|
||||
"cli:run": "node cli/dist/cli.mjs",
|
||||
"cli:build:production": "cd cli && npm run build:production",
|
||||
"cli:watch": "cd cli && npm run watch",
|
||||
"cli:test": "cd cli && npm run test",
|
||||
"vscode:prepublish": "bun run package",
|
||||
"compile": "bun run check-types && bun run lint && bun esbuild.mjs",
|
||||
"compile-standalone": "bun run protos && (bunx tsc --noEmit & cd webview-ui && bunx tsc --noEmit & cd cli && bunx tsc --noEmit & wait)",
|
||||
"compile-standalone-npm": "bun run protos && bun run check-types && bun run lint && bun esbuild.mjs --standalone",
|
||||
"cli:link": "cd cli && bun run link",
|
||||
"cli:build": "bun run protos && cd cli && bun run build",
|
||||
"cli:build:production": "cd cli && bun run build:production",
|
||||
"cli:watch": "cd cli && bun run watch",
|
||||
"cli:test": "cd cli && bun run test",
|
||||
"test:install": "bash scripts/test-install.sh",
|
||||
"cli:dev": "cd cli && npm run dev",
|
||||
"postcompile-standalone": "node scripts/package-standalone.mjs",
|
||||
"postcompile-standalone-npm": "node scripts/package-npm.mjs",
|
||||
"dev": "npm run protos && npm run watch",
|
||||
"watch": "npx npm-run-all -p watch:*",
|
||||
"watch:esbuild": "node esbuild.mjs --watch",
|
||||
"cli:dev": "cd cli && bun run dev",
|
||||
"postcompile-standalone": "bun scripts/package-standalone.mjs",
|
||||
"postcompile-standalone-npm": "bun scripts/package-npm.mjs",
|
||||
"dev": "bun run protos && bun run watch",
|
||||
"watch": "bunx npm-run-all -p watch:*",
|
||||
"watch:esbuild": "bun esbuild.mjs --watch",
|
||||
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
|
||||
"package": "npm run check-types && npm run build:webview && npm run lint && node esbuild.mjs --production",
|
||||
"protos": "node scripts/build-proto.mjs",
|
||||
"protos-python": "node scripts/build-python-proto.mjs",
|
||||
"download-ripgrep": "node scripts/download-ripgrep.mjs",
|
||||
"package": "bun run check-types && bun run build:webview && bun run lint && bun esbuild.mjs --production",
|
||||
"protos": "bun scripts/build-proto.mjs",
|
||||
"protos-python": "bun scripts/build-python-proto.mjs",
|
||||
"download-ripgrep": "bun scripts/download-ripgrep.mjs",
|
||||
"postprotos": "biome format src/shared/proto src/core/controller src/hosts/ webview-ui/src/services src/generated --write --no-errors-on-unmatched",
|
||||
"clean:build": "rimraf dist dist-standalone webview-ui/build src/generated out/",
|
||||
"clean:deps": "rimraf node_modules webview-ui/node_modules",
|
||||
"clean:all": "npm run clean:build && npm run clean:deps",
|
||||
"compile-tests": "node ./scripts/build-tests.js",
|
||||
"clean:deps": "rimraf node_modules webview-ui/node_modules cli/node_modules",
|
||||
"clean:all": "bun run clean:build && bun run clean:deps",
|
||||
"compile-tests": "bun ./scripts/build-tests.js",
|
||||
"watch-tests": "tsc -p . -w --outDir out",
|
||||
"check-types": "npm run protos && npx tsc --noEmit && cd webview-ui && npx tsc --noEmit && cd ../cli && npx tsc --noEmit",
|
||||
"lint": "biome lint --no-errors-on-unmatched --files-ignore-unknown=true --diagnostic-level=error && npm run lint:proto",
|
||||
"check-types": "bun run protos && bunx tsc --noEmit && cd webview-ui && bunx tsc --noEmit && cd ../cli && bunx tsc --noEmit",
|
||||
"lint": "biome lint --no-errors-on-unmatched --files-ignore-unknown=true --diagnostic-level=error && bun run lint:proto",
|
||||
"lint:proto": "bash ./scripts/proto-lint.sh",
|
||||
"format": "biome format --changed --no-errors-on-unmatched --files-ignore-unknown=true --diagnostic-level=error",
|
||||
"format:fix": "biome check --changed --no-errors-on-unmatched --files-ignore-unknown=true --write",
|
||||
"fix:all": "biome check --no-errors-on-unmatched --files-ignore-unknown=true --write --diagnostic-level=error --unsafe",
|
||||
"ci:check-all": "npx npm-run-all -p check-types lint format",
|
||||
"ci:build": "npm run protos && npm run build:webview && node esbuild.mjs && npm run compile-tests",
|
||||
"pretest": "npm run compile && npm run compile-tests && npm run compile-standalone && npm run lint",
|
||||
"test": "npx npm-run-all test:unit test:integration",
|
||||
"ci:check-all": "bunx npm-run-all -p check-types lint format",
|
||||
"ci:build": "bun run protos && bun run build:webview && bun esbuild.mjs && bun run compile-tests",
|
||||
"pretest": "bun run compile && bun run compile-tests && bun run compile-standalone && bun run lint",
|
||||
"test": "bunx npm-run-all test:unit test:integration",
|
||||
"test:integration": "vscode-test",
|
||||
"test:unit": "cross-env TS_NODE_PROJECT=./tsconfig.unit-test.json mocha",
|
||||
"test:coverage": "vscode-test --coverage",
|
||||
"test:sca-server": "npx tsx watch scripts/test-standalone-core-api-server.ts",
|
||||
"test:tp-orchestrator": "npx tsx scripts/testing-platform-orchestrator.ts",
|
||||
"test:sca-server": "bunx tsx watch scripts/test-standalone-core-api-server.ts",
|
||||
"test:tp-orchestrator": "bunx tsx scripts/testing-platform-orchestrator.ts",
|
||||
"e2e": "playwright test -c playwright.config.ts",
|
||||
"test:e2e:build": "vsce package --allow-package-secrets sendgrid --out dist/e2e.vsix",
|
||||
"test:e2e": "playwright install && npm run test:e2e:build && node src/test/e2e/utils/build.mjs && playwright test",
|
||||
"test:e2e:optimal": "npm run test:e2e:build && node src/test/e2e/utils/build.mjs && playwright test",
|
||||
"test:e2e:ui": "npx tsx scripts/interactive-playwright.ts",
|
||||
"install:all": "npm install && cd webview-ui && npm install",
|
||||
"dev:webview": "cd webview-ui && npm run dev",
|
||||
"build:webview": "cd webview-ui && npm run build",
|
||||
"test:webview": "cd webview-ui && npm run test",
|
||||
"test:e2e": "playwright install && bun run test:e2e:build && bun src/test/e2e/utils/build.mjs && playwright test",
|
||||
"test:e2e:optimal": "bun run test:e2e:build && bun src/test/e2e/utils/build.mjs && playwright test",
|
||||
"test:e2e:ui": "bunx tsx scripts/interactive-playwright.ts",
|
||||
"install:all": "bun install",
|
||||
"dev:webview": "cd webview-ui && bun run dev",
|
||||
"build:webview": "cd webview-ui && bun run build",
|
||||
"test:webview": "cd webview-ui && bun run test",
|
||||
"publish:marketplace": "vsce publish --allow-package-secrets sendgrid && ovsx publish",
|
||||
"publish:marketplace:prerelease": "vsce publish --allow-package-secrets sendgrid --pre-release && ovsx publish --pre-release",
|
||||
"publish:marketplace:nightly": "node ./scripts/publish-nightly.mjs",
|
||||
"prepare": "npx husky",
|
||||
"publish:marketplace:nightly": "bun ./scripts/publish-nightly.mjs",
|
||||
"prepare": "bunx husky",
|
||||
"changeset": "changeset",
|
||||
"version-packages": "changeset version",
|
||||
"docs": "cd docs && npm run dev",
|
||||
"docs:check-links": "cd docs && npm run check",
|
||||
"docs:rename-file": "cd docs && npm run rename",
|
||||
"report-issue": "node scripts/report-issue.js",
|
||||
"storybook": "cd webview-ui && npm run storybook",
|
||||
"cli:unlink": "cd cli && npm run unlink"
|
||||
"docs": "cd docs && bun run dev",
|
||||
"docs:check-links": "cd docs && bun run check",
|
||||
"docs:rename-file": "cd docs && bun run rename",
|
||||
"report-issue": "bun scripts/report-issue.js",
|
||||
"storybook": "cd webview-ui && bun run storybook",
|
||||
"cli:unlink": "cd cli && bun run unlink"
|
||||
},
|
||||
"lint-staged": {
|
||||
"src/shared/storage/state-keys.ts": [
|
||||
"node scripts/generate-state-proto.mjs",
|
||||
"bun scripts/generate-state-proto.mjs",
|
||||
"git add proto/cline/state.proto"
|
||||
],
|
||||
"*": [
|
||||
@@ -454,10 +454,11 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.3.14",
|
||||
"@biomejs/biome": "^2.1.4",
|
||||
"@bufbuild/buf": "^1.54.0",
|
||||
"@changesets/cli": "^2.27.12",
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"@types/bun": "^1.3.8",
|
||||
"@types/chai": "^5.0.1",
|
||||
"@types/clone-deep": "^4.0.4",
|
||||
"@types/diff": "^5.2.1",
|
||||
|
||||
@@ -9,7 +9,7 @@ export default defineConfig({
|
||||
forbidOnly: isCI,
|
||||
testDir: "src/test/e2e",
|
||||
testMatch: /.*\.test\.ts/,
|
||||
timeout: isCI || isWindow ? 60000 : 20000,
|
||||
timeout: isCI || isWindow ? 40000 : 20000,
|
||||
expect: {
|
||||
timeout: isCI || isWindow ? 5000 : 2000,
|
||||
},
|
||||
|
||||
@@ -42,8 +42,6 @@ service AccountService {
|
||||
|
||||
rpc requestyAuthClicked(StringRequest) returns (Empty);
|
||||
|
||||
rpc hicapAuthClicked(EmptyRequest) returns (Empty);
|
||||
|
||||
// Returns a link the webview can use to redirect back to the user's IDE.
|
||||
rpc getRedirectUrl(EmptyRequest) returns (String);
|
||||
|
||||
|
||||
+3
-16
@@ -34,9 +34,6 @@ service StateService {
|
||||
rpc checkCliInstallation(EmptyRequest) returns (Boolean);
|
||||
rpc getProcessInfo(EmptyRequest) returns (ProcessInfo);
|
||||
rpc flushPendingState(EmptyRequest) returns (Empty);
|
||||
rpc refreshRemoteConfig(EmptyRequest) returns (Empty);
|
||||
rpc testOtelConnection(EmptyRequest) returns (TestConnectionResult);
|
||||
rpc testPromptUploading(EmptyRequest) returns (TestConnectionResult);
|
||||
}
|
||||
|
||||
message AutoApprovalActions {
|
||||
@@ -111,8 +108,6 @@ message Secrets {
|
||||
// in src/shared/storage/state-keys.ts and use the scripts/generate-state-proto.mjs
|
||||
// script to regenerate this list.
|
||||
message Settings {
|
||||
reserved 146; // was openai_reasoning_effort (moved to mode-scoped reasoning effort)
|
||||
|
||||
optional string lite_llm_base_url = 1;
|
||||
optional bool lite_llm_use_prompt_cache = 2;
|
||||
optional string anthropic_base_url = 4;
|
||||
@@ -257,6 +252,7 @@ message Settings {
|
||||
optional bool use_auto_condense = 143;
|
||||
optional bool cline_web_tools_enabled = 144;
|
||||
optional string preferred_language = 145;
|
||||
optional OpenaiReasoningEffort openai_reasoning_effort = 146;
|
||||
optional PlanActMode mode = 147;
|
||||
optional DictationSettings dictation_settings = 148;
|
||||
optional FocusChainSettings focus_chain_settings = 149;
|
||||
@@ -283,7 +279,6 @@ message Settings {
|
||||
optional bool worktrees_enabled = 172;
|
||||
optional bool auto_approve_all_toggled = 174;
|
||||
map<string, string> open_ai_headers = 175;
|
||||
optional bool double_check_completion_enabled = 176;
|
||||
}
|
||||
|
||||
message DictationSettings {
|
||||
@@ -327,7 +322,7 @@ enum OpenaiReasoningEffort {
|
||||
LOW = 0;
|
||||
MEDIUM = 1;
|
||||
HIGH = 2;
|
||||
reserved 3; // was MINIMAL
|
||||
MINIMAL = 3;
|
||||
}
|
||||
|
||||
enum McpDisplayMode {
|
||||
@@ -390,7 +385,6 @@ message UpdateTaskSettingsRequest {
|
||||
|
||||
// Message for updating settings
|
||||
message UpdateSettingsRequest {
|
||||
reserved 15; // was openai_reasoning_effort (moved to mode-scoped reasoning effort)
|
||||
reserved 26; // was hooks_enabled (removed - now always enabled on macOS/Linux)
|
||||
reserved 38; // was skills_enabled (removed - now always enabled)
|
||||
|
||||
@@ -407,6 +401,7 @@ message UpdateSettingsRequest {
|
||||
optional int32 terminal_output_line_limit = 12;
|
||||
optional PlanActMode mode = 13;
|
||||
optional string preferred_language = 14;
|
||||
optional OpenaiReasoningEffort openai_reasoning_effort = 15;
|
||||
optional bool strict_plan_mode_enabled = 16;
|
||||
optional FocusChainSettings focus_chain_settings = 17;
|
||||
optional bool use_auto_condense = 18;
|
||||
@@ -430,7 +425,6 @@ message UpdateSettingsRequest {
|
||||
optional string oca_reasoning_effort = 37;
|
||||
optional bool opt_out_of_remote_config = 39;
|
||||
optional bool worktrees_enabled = 40;
|
||||
optional bool double_check_completion_enabled = 41;
|
||||
}
|
||||
|
||||
message UpdateTerminalConnectionTimeoutRequest {
|
||||
@@ -477,10 +471,3 @@ message TrackBannerEventRequest {
|
||||
string banner_id = 1;
|
||||
string event_type = 2;
|
||||
}
|
||||
|
||||
// Result of a connection test (OTEL, prompt uploading, etc.)
|
||||
message TestConnectionResult {
|
||||
bool success = 1;
|
||||
optional string message = 2;
|
||||
optional string error = 3;
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ enum ClineAsk {
|
||||
REPORT_BUG = 14;
|
||||
SUMMARIZE_TASK = 15;
|
||||
ACT_MODE_RESPOND = 16;
|
||||
USE_SUBAGENTS = 17;
|
||||
}
|
||||
|
||||
// Enum for ClineSay types
|
||||
@@ -72,9 +71,6 @@ enum ClineSay {
|
||||
HOOK_OUTPUT_STREAM = 31;
|
||||
COMMAND_PERMISSION_DENIED = 32;
|
||||
CONDITIONAL_RULES_APPLIED = 33;
|
||||
SUBAGENT_STATUS = 34;
|
||||
USE_SUBAGENTS_SAY = 35;
|
||||
SUBAGENT_USAGE = 36;
|
||||
}
|
||||
|
||||
// Enum for ClineSayTool tool types
|
||||
|
||||
@@ -36,11 +36,6 @@ service EnvService {
|
||||
|
||||
// Logs a debug message to the host environment's log/output console.
|
||||
rpc debugLog(cline.StringRequest) returns (cline.Empty);
|
||||
|
||||
// Opens an external URL in the default browser.
|
||||
// In remote environments (VS Code Server, SSH, etc.), this routes the URL
|
||||
// to the user's local machine to open in their local browser.
|
||||
rpc openExternal(cline.StringRequest) returns (cline.Empty);
|
||||
}
|
||||
|
||||
message GetHostVersionResponse {
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Script to add endpoints.json to a JetBrains plugin ZIP for enterprise distribution
|
||||
# Usage: ./add-endpoints-to-jetbrains.sh <source.zip> <output.zip> <endpoints.json>
|
||||
|
||||
if [ "$#" -ne 3 ]; then
|
||||
echo "Error: Invalid number of arguments"
|
||||
echo "Usage: $0 <source.zip> <output.zip> <endpoints.json>"
|
||||
echo ""
|
||||
echo "Example:"
|
||||
echo " $0 cline-jetbrains-3.55.0.zip cline-jetbrains-3.55.0-enterprise.zip endpoints.json"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SOURCE_ZIP="$1"
|
||||
OUTPUT_ZIP="$2"
|
||||
ENDPOINTS_JSON="$3"
|
||||
|
||||
# Validate inputs
|
||||
if [ ! -f "$SOURCE_ZIP" ]; then
|
||||
echo "Error: Source ZIP file not found: $SOURCE_ZIP"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$ENDPOINTS_JSON" ]; then
|
||||
echo "Error: endpoints.json file not found: $ENDPOINTS_JSON"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate endpoints.json is valid JSON
|
||||
if ! jq empty "$ENDPOINTS_JSON" 2>/dev/null; then
|
||||
echo "Error: $ENDPOINTS_JSON is not valid JSON"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate required fields exist
|
||||
REQUIRED_FIELDS=("appBaseUrl" "apiBaseUrl" "mcpBaseUrl")
|
||||
for field in "${REQUIRED_FIELDS[@]}"; do
|
||||
if ! jq -e ".$field" "$ENDPOINTS_JSON" > /dev/null 2>&1; then
|
||||
echo "Error: Missing required field '$field' in $ENDPOINTS_JSON"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate field is a non-empty string
|
||||
value=$(jq -r ".$field" "$ENDPOINTS_JSON")
|
||||
if [ -z "$value" ] || [ "$value" = "null" ]; then
|
||||
echo "Error: Field '$field' must be a non-empty string"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate URL format (basic check)
|
||||
if ! [[ "$value" =~ ^https?:// ]]; then
|
||||
echo "Error: Field '$field' must be a valid URL (got: $value)"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo "✓ Validated endpoints.json"
|
||||
|
||||
# Create temp directory
|
||||
TEMP_DIR=$(mktemp -d)
|
||||
trap "rm -rf $TEMP_DIR" EXIT
|
||||
|
||||
echo "Extracting JetBrains plugin ZIP..."
|
||||
unzip -q "$SOURCE_ZIP" -d "$TEMP_DIR"
|
||||
|
||||
# Find the plugin lib directory (where the main JAR is located)
|
||||
# JetBrains plugins typically have a structure like: cline/lib/
|
||||
# We need to add endpoints.json to the root of the plugin directory
|
||||
PLUGIN_DIR="$TEMP_DIR/cline"
|
||||
if [ ! -d "$PLUGIN_DIR" ]; then
|
||||
# Try to find any directory that looks like a plugin root
|
||||
PLUGIN_DIR=$(find "$TEMP_DIR" -maxdepth 1 -type d ! -path "$TEMP_DIR" | head -n 1)
|
||||
if [ -z "$PLUGIN_DIR" ] || [ ! -d "$PLUGIN_DIR" ]; then
|
||||
echo "Warning: Could not find plugin directory, adding to ZIP root"
|
||||
PLUGIN_DIR="$TEMP_DIR"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Adding endpoints.json to plugin directory..."
|
||||
cp "$ENDPOINTS_JSON" "$PLUGIN_DIR/endpoints.json"
|
||||
|
||||
# Repackage ZIP
|
||||
echo "Repackaging ZIP..."
|
||||
cd "$TEMP_DIR"
|
||||
zip -q -r "$(basename "$OUTPUT_ZIP")" .
|
||||
cd - > /dev/null
|
||||
|
||||
# Move to final location
|
||||
mv "$TEMP_DIR/$(basename "$OUTPUT_ZIP")" "$OUTPUT_ZIP"
|
||||
|
||||
echo "✓ Successfully created $OUTPUT_ZIP with bundled endpoints.json"
|
||||
echo ""
|
||||
echo "The package is ready for enterprise distribution."
|
||||
echo "When installed in JetBrains IDEs, Cline will automatically use the bundled configuration."
|
||||
@@ -1,85 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Script to add endpoints.json to an NPM tarball for enterprise distribution
|
||||
# Usage: ./add-endpoints-to-npm.sh <source.tgz> <output.tgz> <endpoints.json>
|
||||
|
||||
if [ "$#" -ne 3 ]; then
|
||||
echo "Error: Invalid number of arguments"
|
||||
echo "Usage: $0 <source.tgz> <output.tgz> <endpoints.json>"
|
||||
echo ""
|
||||
echo "Example:"
|
||||
echo " $0 cline-3.55.0.tgz cline-3.55.0-enterprise.tgz endpoints.json"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SOURCE_TGZ="$1"
|
||||
OUTPUT_TGZ="$2"
|
||||
ENDPOINTS_JSON="$3"
|
||||
|
||||
# Validate inputs
|
||||
if [ ! -f "$SOURCE_TGZ" ]; then
|
||||
echo "Error: Source tarball file not found: $SOURCE_TGZ"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$ENDPOINTS_JSON" ]; then
|
||||
echo "Error: endpoints.json file not found: $ENDPOINTS_JSON"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate endpoints.json is valid JSON
|
||||
if ! jq empty "$ENDPOINTS_JSON" 2>/dev/null; then
|
||||
echo "Error: $ENDPOINTS_JSON is not valid JSON"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate required fields exist
|
||||
REQUIRED_FIELDS=("appBaseUrl" "apiBaseUrl" "mcpBaseUrl")
|
||||
for field in "${REQUIRED_FIELDS[@]}"; do
|
||||
if ! jq -e ".$field" "$ENDPOINTS_JSON" > /dev/null 2>&1; then
|
||||
echo "Error: Missing required field '$field' in $ENDPOINTS_JSON"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate field is a non-empty string
|
||||
value=$(jq -r ".$field" "$ENDPOINTS_JSON")
|
||||
if [ -z "$value" ] || [ "$value" = "null" ]; then
|
||||
echo "Error: Field '$field' must be a non-empty string"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate URL format (basic check)
|
||||
if ! [[ "$value" =~ ^https?:// ]]; then
|
||||
echo "Error: Field '$field' must be a valid URL (got: $value)"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo "✓ Validated endpoints.json"
|
||||
|
||||
# Resolve absolute path for output file before changing directories
|
||||
OUTPUT_TGZ_ABS=$(cd "$(dirname "$OUTPUT_TGZ")" && pwd)/$(basename "$OUTPUT_TGZ")
|
||||
|
||||
# Create temp directory
|
||||
TEMP_DIR=$(mktemp -d)
|
||||
trap "rm -rf $TEMP_DIR" EXIT
|
||||
|
||||
echo "Extracting NPM tarball..."
|
||||
tar -xzf "$SOURCE_TGZ" -C "$TEMP_DIR"
|
||||
|
||||
# Copy endpoints.json to package root
|
||||
# NPM tarballs extract to a 'package' directory
|
||||
echo "Adding endpoints.json to package root..."
|
||||
cp "$ENDPOINTS_JSON" "$TEMP_DIR/package/endpoints.json"
|
||||
|
||||
# Repackage tarball
|
||||
echo "Repackaging tarball..."
|
||||
cd "$TEMP_DIR"
|
||||
tar -czf "$OUTPUT_TGZ_ABS" package
|
||||
cd - > /dev/null
|
||||
|
||||
echo "✓ Successfully created $OUTPUT_TGZ with bundled endpoints.json"
|
||||
echo ""
|
||||
echo "The package is ready for enterprise distribution."
|
||||
echo "When installed via npm, Cline will automatically use the bundled configuration."
|
||||
@@ -1,84 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Script to add endpoints.json to a VSIX package for enterprise distribution
|
||||
# Usage: ./add-endpoints-to-vsix.sh <source.vsix> <output.vsix> <endpoints.json>
|
||||
|
||||
if [ "$#" -ne 3 ]; then
|
||||
echo "Error: Invalid number of arguments"
|
||||
echo "Usage: $0 <source.vsix> <output.vsix> <endpoints.json>"
|
||||
echo ""
|
||||
echo "Example:"
|
||||
echo " $0 cline-3.55.0.vsix cline-3.55.0-enterprise.vsix endpoints.json"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SOURCE_VSIX="$1"
|
||||
OUTPUT_VSIX="$2"
|
||||
ENDPOINTS_JSON="$3"
|
||||
|
||||
# Validate inputs
|
||||
if [ ! -f "$SOURCE_VSIX" ]; then
|
||||
echo "Error: Source VSIX file not found: $SOURCE_VSIX"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$ENDPOINTS_JSON" ]; then
|
||||
echo "Error: endpoints.json file not found: $ENDPOINTS_JSON"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate endpoints.json is valid JSON
|
||||
if ! jq empty "$ENDPOINTS_JSON" 2>/dev/null; then
|
||||
echo "Error: $ENDPOINTS_JSON is not valid JSON"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate required fields exist
|
||||
REQUIRED_FIELDS=("appBaseUrl" "apiBaseUrl" "mcpBaseUrl")
|
||||
for field in "${REQUIRED_FIELDS[@]}"; do
|
||||
if ! jq -e ".$field" "$ENDPOINTS_JSON" > /dev/null 2>&1; then
|
||||
echo "Error: Missing required field '$field' in $ENDPOINTS_JSON"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate field is a non-empty string
|
||||
value=$(jq -r ".$field" "$ENDPOINTS_JSON")
|
||||
if [ -z "$value" ] || [ "$value" = "null" ]; then
|
||||
echo "Error: Field '$field' must be a non-empty string"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate URL format (basic check)
|
||||
if ! [[ "$value" =~ ^https?:// ]]; then
|
||||
echo "Error: Field '$field' must be a valid URL (got: $value)"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo "✓ Validated endpoints.json"
|
||||
|
||||
# Resolve absolute path for output file before changing directories
|
||||
OUTPUT_VSIX_ABS=$(cd "$(dirname "$OUTPUT_VSIX")" && pwd)/$(basename "$OUTPUT_VSIX")
|
||||
|
||||
# Create temp directory
|
||||
TEMP_DIR=$(mktemp -d)
|
||||
trap "rm -rf $TEMP_DIR" EXIT
|
||||
|
||||
echo "Extracting VSIX..."
|
||||
unzip -q "$SOURCE_VSIX" -d "$TEMP_DIR"
|
||||
|
||||
# Copy endpoints.json to extension directory
|
||||
echo "Adding endpoints.json to extension/..."
|
||||
cp "$ENDPOINTS_JSON" "$TEMP_DIR/extension/endpoints.json"
|
||||
|
||||
# Repackage VSIX
|
||||
echo "Repackaging VSIX..."
|
||||
cd "$TEMP_DIR"
|
||||
zip -q -r "$OUTPUT_VSIX_ABS" .
|
||||
cd - > /dev/null
|
||||
|
||||
echo "✓ Successfully created $OUTPUT_VSIX with bundled endpoints.json"
|
||||
echo ""
|
||||
echo "The package is ready for enterprise distribution."
|
||||
echo "When installed, Cline will automatically use the bundled configuration."
|
||||
@@ -1,44 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Build CLI release for a specific ref/commit using GitHub Actions
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/build-cli-artifact.sh [ref] [pr_number]
|
||||
#
|
||||
# Examples:
|
||||
# ./scripts/build-cli-artifact.sh # Build from current branch
|
||||
# ./scripts/build-cli-artifact.sh main # Build from main branch
|
||||
# ./scripts/build-cli-artifact.sh abc123 # Build from commit abc123
|
||||
# ./scripts/build-cli-artifact.sh feature/new 1234 # Build from branch and comment on PR #1234
|
||||
|
||||
set -e
|
||||
|
||||
REF="${1:-$(git rev-parse --abbrev-ref HEAD)}"
|
||||
PR_NUMBER="${2:-}"
|
||||
|
||||
echo "🚀 Triggering CLI build workflow..."
|
||||
echo " Branch/commit: $REF"
|
||||
|
||||
# Build args array
|
||||
ARGS=(-f "ref=$REF")
|
||||
|
||||
if [ -n "$PR_NUMBER" ]; then
|
||||
ARGS+=(-f "pr_number=$PR_NUMBER")
|
||||
echo " Will comment on PR #$PR_NUMBER"
|
||||
fi
|
||||
|
||||
# Trigger the workflow
|
||||
gh workflow run pack-cli.yml "${ARGS[@]}"
|
||||
|
||||
echo ""
|
||||
echo "✅ Workflow triggered!"
|
||||
echo ""
|
||||
echo "The workflow will create a GitHub Release with a public download URL."
|
||||
echo ""
|
||||
echo "To monitor the workflow:"
|
||||
echo " gh run list --workflow=pack-cli.yml --limit 5"
|
||||
echo ""
|
||||
echo "Once complete, find the release:"
|
||||
echo " gh release list --limit 10"
|
||||
echo ""
|
||||
echo "Install from the release URL (no authentication required):"
|
||||
echo " npm install -g https://github.com/cline/cline/releases/download/cli-build-<commit>/cline-<version>.tgz"
|
||||
@@ -33,7 +33,7 @@ async function main() {
|
||||
|
||||
console.log("\n✅ Build complete!")
|
||||
console.log(`\n📦 NPM package ready in ${BUILD_DIR}/`)
|
||||
console.log(`To publish: cd ${BUILD_DIR} && npm publish`)
|
||||
console.log(`To publish: cd ${BUILD_DIR} && bun publish`)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -55,11 +55,11 @@ async function buildTypeScriptCli() {
|
||||
// Install dependencies if needed
|
||||
if (!fs.existsSync(path.join(CLI_DIR, "node_modules"))) {
|
||||
console.log("Installing cli dependencies...")
|
||||
execSync("npm install", { stdio: "inherit", cwd: CLI_DIR })
|
||||
execSync("bun install", { stdio: "inherit", cwd: CLI_DIR })
|
||||
}
|
||||
|
||||
// Build production bundle
|
||||
execSync("npm run build:production", { stdio: "inherit", cwd: CLI_DIR })
|
||||
execSync("bun run build:production", { stdio: "inherit", cwd: CLI_DIR })
|
||||
console.log("✓ TypeScript CLI built")
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ async function copyCliDist() {
|
||||
|
||||
if (!fs.existsSync(distSource)) {
|
||||
console.error(`Error: CLI dist not found at ${distSource}`)
|
||||
console.error(`Please run: cd cli && npm run build:production`)
|
||||
console.error(`Please run: cd cli && bun run build:production`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
|
||||
@@ -46,8 +46,8 @@ async function installNodeDependencies() {
|
||||
|
||||
await cpr(RUNTIME_DEPS_DIR, BUILD_DIR)
|
||||
|
||||
console.log("Running npm install in distribution directory...")
|
||||
execSync("npm install", { stdio: "inherit", cwd: BUILD_DIR })
|
||||
console.log("Running bun install in distribution directory...")
|
||||
execSync("bun install", { stdio: "inherit", cwd: BUILD_DIR })
|
||||
|
||||
// Move the vscode directory into node_modules.
|
||||
// It can't be installed using npm because it will create a symlink which cannot be unzipped correctly on windows.
|
||||
@@ -93,9 +93,13 @@ async function packageAllBinaryDeps() {
|
||||
|
||||
// Download the binary libs
|
||||
const v = IS_VERBOSE ? "--verbose" : ""
|
||||
const cmd = `npx prebuild-install --platform=${platform} --arch=${arch} --target=${TARGET_NODE_VERSION} ${v}`
|
||||
const cmd = `bunx prebuild-install --platform=${platform} --arch=${arch} --target=${TARGET_NODE_VERSION} ${v}`
|
||||
log_verbose(`${module}: ${cmd}`)
|
||||
execSync(cmd, { cwd: dest, stdio: "inherit" })
|
||||
execSync(cmd, {
|
||||
cwd: dest,
|
||||
stdio: "inherit",
|
||||
env: { ...process.env, NODE_NO_WARNINGS: "1" },
|
||||
})
|
||||
log_verbose("")
|
||||
}
|
||||
// Remove the original module with the host platform binaries installed directly into node_modules.
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
* 6. Restores the original package.json
|
||||
*
|
||||
* Usage:
|
||||
* npm run publish:marketplace:nightly
|
||||
* npm run publish:marketplace:nightly -- --dry-run
|
||||
* bun run publish:marketplace:nightly
|
||||
* bun run publish:marketplace:nightly -- --dry-run
|
||||
*
|
||||
* Environment variables:
|
||||
* VSCE_PAT - Personal Access Token for VS Code Marketplace
|
||||
@@ -361,7 +361,7 @@ if (showHelp) {
|
||||
Nightly publish script for VS Code extension
|
||||
|
||||
Usage:
|
||||
npm run publish:marketplace:nightly [options]
|
||||
bun run publish:marketplace:nightly [options]
|
||||
|
||||
Options:
|
||||
--dry-run, -n Run without actually publishing (package only)
|
||||
@@ -372,9 +372,9 @@ Environment variables:
|
||||
OVSX_PAT Personal Access Token for OpenVSX Registry
|
||||
|
||||
Examples:
|
||||
npm run publish:marketplace:nightly # Full publish
|
||||
npm run publish:marketplace:nightly -- --dry-run # Package only
|
||||
VSCE_PAT="token" npm run publish:marketplace:nightly # Publish to VS Code only
|
||||
bun run publish:marketplace:nightly # Full publish
|
||||
bun run publish:marketplace:nightly -- --dry-run # Package only
|
||||
VSCE_PAT="token" bun run publish:marketplace:nightly # Publish to VS Code only
|
||||
`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Test script to build VSIX and CLI packages with bundled staging endpoints
|
||||
# This demonstrates the complete workflow for enterprise distribution
|
||||
|
||||
echo "🔨 Cline Bundled Endpoints Build & Test Script"
|
||||
echo "==============================================="
|
||||
echo ""
|
||||
|
||||
# Configuration
|
||||
STAGING_CONFIG=$(cat <<'EOF'
|
||||
{
|
||||
"appBaseUrl": "https://staging-app.cline.bot",
|
||||
"apiBaseUrl": "https://core-api.staging.int.cline.bot",
|
||||
"mcpBaseUrl": "https://core-api.staging.int.cline.bot/v1/mcp"
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
# Output directory for built packages
|
||||
OUTPUT_DIR="./dist-bundled"
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
echo "📁 Output directory: $OUTPUT_DIR"
|
||||
echo ""
|
||||
|
||||
# Create temp directory for intermediate artifacts
|
||||
TEST_DIR=$(mktemp -d)
|
||||
trap "rm -rf $TEST_DIR" EXIT
|
||||
|
||||
echo "📁 Temp directory: $TEST_DIR"
|
||||
echo ""
|
||||
|
||||
# Step 1: Clean up existing binaries
|
||||
echo "1️⃣ Cleaning up existing binaries..."
|
||||
rm -f ./*.vsix
|
||||
rm -f ./cli/*.tgz
|
||||
echo "✓ Removed old VSIX and TGZ files"
|
||||
echo ""
|
||||
|
||||
# Step 2: Create staging endpoints.json
|
||||
echo "2️⃣ Creating staging endpoints.json..."
|
||||
echo "$STAGING_CONFIG" > "$TEST_DIR/endpoints-staging.json"
|
||||
echo "✓ Created $TEST_DIR/endpoints-staging.json"
|
||||
cat "$TEST_DIR/endpoints-staging.json"
|
||||
echo ""
|
||||
|
||||
# Step 3: Build VSIX package
|
||||
echo "3️⃣ Building VSCode extension (VSIX)..."
|
||||
echo ""
|
||||
npx @vscode/vsce package --no-dependencies
|
||||
echo ""
|
||||
|
||||
# Find the newly built VSIX
|
||||
VSIX_FILE=$(find . -maxdepth 1 -name "*.vsix" -type f | head -n 1)
|
||||
if [ -z "$VSIX_FILE" ]; then
|
||||
echo "❌ Error: VSIX build failed or file not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✓ Built VSIX: $VSIX_FILE"
|
||||
VSIX_BASENAME=$(basename "$VSIX_FILE")
|
||||
VSIX_NAME="${VSIX_BASENAME%.vsix}"
|
||||
|
||||
# Copy original VSIX to output directory
|
||||
cp "$VSIX_FILE" "$OUTPUT_DIR/"
|
||||
echo "✓ Copied original to: $OUTPUT_DIR/$VSIX_BASENAME"
|
||||
echo ""
|
||||
|
||||
# Step 4: Build CLI package
|
||||
echo "4️⃣ Building CLI package (TGZ)..."
|
||||
echo ""
|
||||
cd cli
|
||||
echo "Building CLI code..."
|
||||
npm run typecheck && yes y | npx tsx esbuild.mts || npx tsx esbuild.mts
|
||||
echo ""
|
||||
echo "Packaging CLI..."
|
||||
npm pack
|
||||
cd ..
|
||||
echo ""
|
||||
|
||||
# Find the newly built TGZ
|
||||
TGZ_FILE=$(find ./cli -maxdepth 1 -name "*.tgz" -type f | head -n 1)
|
||||
if [ -z "$TGZ_FILE" ]; then
|
||||
echo "❌ Error: CLI package build failed or file not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✓ Built TGZ: $TGZ_FILE"
|
||||
TGZ_BASENAME=$(basename "$TGZ_FILE")
|
||||
TGZ_NAME="${TGZ_BASENAME%.tgz}"
|
||||
|
||||
# Copy original TGZ to output directory
|
||||
cp "$TGZ_FILE" "$OUTPUT_DIR/"
|
||||
echo "✓ Copied original to: $OUTPUT_DIR/$TGZ_BASENAME"
|
||||
echo ""
|
||||
|
||||
# Step 5: Create VSIX with bundled endpoints
|
||||
echo "5️⃣ Creating VSIX with bundled endpoints..."
|
||||
OUTPUT_VSIX="$OUTPUT_DIR/${VSIX_NAME}-with-endpoints.vsix"
|
||||
|
||||
./scripts/add-endpoints-to-vsix.sh \
|
||||
"$VSIX_FILE" \
|
||||
"$OUTPUT_VSIX" \
|
||||
"$TEST_DIR/endpoints-staging.json"
|
||||
|
||||
echo "✓ Created: $OUTPUT_VSIX"
|
||||
echo ""
|
||||
|
||||
# Step 6: Create TGZ with bundled endpoints
|
||||
echo "6️⃣ Creating TGZ with bundled endpoints..."
|
||||
OUTPUT_TGZ="$OUTPUT_DIR/${TGZ_NAME}-with-endpoints.tgz"
|
||||
|
||||
./scripts/add-endpoints-to-npm.sh \
|
||||
"$TGZ_FILE" \
|
||||
"$OUTPUT_TGZ" \
|
||||
"$TEST_DIR/endpoints-staging.json"
|
||||
|
||||
echo "✓ Created: $OUTPUT_TGZ"
|
||||
echo ""
|
||||
|
||||
# Step 7: Verify VSIX bundled file
|
||||
echo "7️⃣ Verifying bundled endpoints in VSIX..."
|
||||
TEMP_EXTRACT_VSIX="$TEST_DIR/extracted-vsix"
|
||||
mkdir -p "$TEMP_EXTRACT_VSIX"
|
||||
unzip -q "$OUTPUT_VSIX" -d "$TEMP_EXTRACT_VSIX"
|
||||
|
||||
if [ -f "$TEMP_EXTRACT_VSIX/extension/endpoints.json" ]; then
|
||||
echo "✓ Found extension/endpoints.json in VSIX"
|
||||
else
|
||||
echo "❌ Error: endpoints.json not found in VSIX"
|
||||
exit 1
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Step 8: Verify TGZ bundled file
|
||||
echo "8️⃣ Verifying bundled endpoints in TGZ..."
|
||||
TEMP_EXTRACT_TGZ="$TEST_DIR/extracted-tgz"
|
||||
mkdir -p "$TEMP_EXTRACT_TGZ"
|
||||
tar -xzf "$OUTPUT_TGZ" -C "$TEMP_EXTRACT_TGZ"
|
||||
|
||||
if [ -f "$TEMP_EXTRACT_TGZ/package/endpoints.json" ]; then
|
||||
echo "✓ Found package/endpoints.json in TGZ"
|
||||
echo ""
|
||||
echo "📄 TGZ Contents:"
|
||||
cat "$TEMP_EXTRACT_TGZ/package/endpoints.json" | jq .
|
||||
echo ""
|
||||
else
|
||||
echo "❌ Error: endpoints.json not found in TGZ"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Summary
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "✅ Build complete!"
|
||||
echo ""
|
||||
ls -lh "$OUTPUT_DIR" | grep -E '\.(vsix|tgz)$' || true
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "Installation Commands:"
|
||||
echo ""
|
||||
echo "# VSCode Extension (original)"
|
||||
echo "code --uninstall-extension saoudrizwan.claude-dev"
|
||||
echo "code --install-extension $OUTPUT_DIR/$VSIX_BASENAME"
|
||||
echo ""
|
||||
echo "# VSCode Extension (with bundled endpoints)"
|
||||
echo "code --uninstall-extension saoudrizwan.claude-dev"
|
||||
echo "code --install-extension $OUTPUT_DIR/${VSIX_NAME}-with-endpoints.vsix"
|
||||
echo ""
|
||||
echo "# CLI (original)"
|
||||
echo "npm uninstall -g cline"
|
||||
echo "npm install -g $OUTPUT_DIR/$TGZ_BASENAME"
|
||||
echo ""
|
||||
echo "# CLI (with bundled endpoints)"
|
||||
echo "npm uninstall -g cline"
|
||||
echo "npm install -g $OUTPUT_DIR/${TGZ_NAME}-with-endpoints.tgz"
|
||||
echo ""
|
||||
+31
-183
@@ -28,7 +28,6 @@ describe("ClineEndpoint configuration", () => {
|
||||
// Reset the singleton state using internal method
|
||||
;(ClineEndpoint as any)._instance = null
|
||||
;(ClineEndpoint as any)._initialized = false
|
||||
;(ClineEndpoint as any)._extensionFsPath = undefined
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -53,7 +52,7 @@ describe("ClineEndpoint configuration", () => {
|
||||
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(validConfig), "utf8")
|
||||
|
||||
await ClineEndpoint.initialize(tempDir)
|
||||
await ClineEndpoint.initialize()
|
||||
|
||||
const config = ClineEndpoint.config
|
||||
config.appBaseUrl.should.equal("https://app.enterprise.com")
|
||||
@@ -65,7 +64,7 @@ describe("ClineEndpoint configuration", () => {
|
||||
it("should work without endpoints.json (standard mode)", async () => {
|
||||
// No endpoints.json file exists
|
||||
|
||||
await ClineEndpoint.initialize(tempDir)
|
||||
await ClineEndpoint.initialize()
|
||||
|
||||
const config = ClineEndpoint.config
|
||||
config.environment.should.not.equal(Environment.selfHosted)
|
||||
@@ -83,7 +82,7 @@ describe("ClineEndpoint configuration", () => {
|
||||
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(validConfig), "utf8")
|
||||
|
||||
await ClineEndpoint.initialize(tempDir)
|
||||
await ClineEndpoint.initialize()
|
||||
|
||||
const config = ClineEndpoint.config
|
||||
config.appBaseUrl.should.equal("http://localhost:3000")
|
||||
@@ -100,7 +99,7 @@ describe("ClineEndpoint configuration", () => {
|
||||
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(validConfig), "utf8")
|
||||
|
||||
await ClineEndpoint.initialize(tempDir)
|
||||
await ClineEndpoint.initialize()
|
||||
|
||||
const config = ClineEndpoint.config
|
||||
config.appBaseUrl.should.equal("https://proxy.enterprise.com/cline/app")
|
||||
@@ -112,7 +111,7 @@ describe("ClineEndpoint configuration", () => {
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), "{ invalid json }", "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize(tempDir)
|
||||
await ClineEndpoint.initialize()
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
@@ -124,7 +123,7 @@ describe("ClineEndpoint configuration", () => {
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), '{"appBaseUrl": "https://test.com"', "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize(tempDir)
|
||||
await ClineEndpoint.initialize()
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
@@ -136,7 +135,7 @@ describe("ClineEndpoint configuration", () => {
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), "", "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize(tempDir)
|
||||
await ClineEndpoint.initialize()
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
@@ -147,7 +146,7 @@ describe("ClineEndpoint configuration", () => {
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), '"just a string"', "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize(tempDir)
|
||||
await ClineEndpoint.initialize()
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
@@ -159,7 +158,7 @@ describe("ClineEndpoint configuration", () => {
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), "[]", "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize(tempDir)
|
||||
await ClineEndpoint.initialize()
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
@@ -172,7 +171,7 @@ describe("ClineEndpoint configuration", () => {
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), "null", "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize(tempDir)
|
||||
await ClineEndpoint.initialize()
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
@@ -191,7 +190,7 @@ describe("ClineEndpoint configuration", () => {
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize(tempDir)
|
||||
await ClineEndpoint.initialize()
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
@@ -208,7 +207,7 @@ describe("ClineEndpoint configuration", () => {
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize(tempDir)
|
||||
await ClineEndpoint.initialize()
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
@@ -225,7 +224,7 @@ describe("ClineEndpoint configuration", () => {
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize(tempDir)
|
||||
await ClineEndpoint.initialize()
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
@@ -237,7 +236,7 @@ describe("ClineEndpoint configuration", () => {
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), "{}", "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize(tempDir)
|
||||
await ClineEndpoint.initialize()
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
@@ -255,7 +254,7 @@ describe("ClineEndpoint configuration", () => {
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize(tempDir)
|
||||
await ClineEndpoint.initialize()
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
@@ -273,7 +272,7 @@ describe("ClineEndpoint configuration", () => {
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize(tempDir)
|
||||
await ClineEndpoint.initialize()
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
@@ -291,7 +290,7 @@ describe("ClineEndpoint configuration", () => {
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize(tempDir)
|
||||
await ClineEndpoint.initialize()
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
@@ -309,7 +308,7 @@ describe("ClineEndpoint configuration", () => {
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize(tempDir)
|
||||
await ClineEndpoint.initialize()
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
@@ -329,7 +328,7 @@ describe("ClineEndpoint configuration", () => {
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize(tempDir)
|
||||
await ClineEndpoint.initialize()
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
@@ -347,7 +346,7 @@ describe("ClineEndpoint configuration", () => {
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize(tempDir)
|
||||
await ClineEndpoint.initialize()
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
@@ -365,7 +364,7 @@ describe("ClineEndpoint configuration", () => {
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize(tempDir)
|
||||
await ClineEndpoint.initialize()
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
@@ -384,7 +383,7 @@ describe("ClineEndpoint configuration", () => {
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize(tempDir)
|
||||
await ClineEndpoint.initialize()
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
@@ -403,7 +402,7 @@ describe("ClineEndpoint configuration", () => {
|
||||
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
|
||||
|
||||
await ClineEndpoint.initialize(tempDir)
|
||||
await ClineEndpoint.initialize()
|
||||
|
||||
// Verify we're in self-hosted mode
|
||||
ClineEndpoint.config.environment.should.equal(Environment.selfHosted)
|
||||
@@ -426,7 +425,7 @@ describe("ClineEndpoint configuration", () => {
|
||||
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
|
||||
|
||||
await ClineEndpoint.initialize(tempDir)
|
||||
await ClineEndpoint.initialize()
|
||||
|
||||
const environments = ["staging", "local", "production", "anything"]
|
||||
for (const env of environments) {
|
||||
@@ -442,7 +441,7 @@ describe("ClineEndpoint configuration", () => {
|
||||
it("should allow environment switching in standard mode", async () => {
|
||||
// No endpoints.json file - standard mode
|
||||
|
||||
await ClineEndpoint.initialize(tempDir)
|
||||
await ClineEndpoint.initialize()
|
||||
|
||||
// Verify we're NOT in self-hosted mode
|
||||
ClineEndpoint.config.environment.should.not.equal(Environment.selfHosted)
|
||||
@@ -469,7 +468,7 @@ describe("ClineEndpoint configuration", () => {
|
||||
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
|
||||
|
||||
await ClineEndpoint.initialize(tempDir)
|
||||
await ClineEndpoint.initialize()
|
||||
|
||||
const envConfig = ClineEndpoint.config
|
||||
envConfig.environment.should.equal(Environment.selfHosted)
|
||||
@@ -484,7 +483,7 @@ describe("ClineEndpoint configuration", () => {
|
||||
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(customConfig), "utf8")
|
||||
|
||||
await ClineEndpoint.initialize(tempDir)
|
||||
await ClineEndpoint.initialize()
|
||||
|
||||
const config = ClineEndpoint.config
|
||||
config.appBaseUrl.should.equal("https://custom-app.internal")
|
||||
@@ -495,11 +494,11 @@ describe("ClineEndpoint configuration", () => {
|
||||
|
||||
describe("initialization behavior", () => {
|
||||
it("should only initialize once", async () => {
|
||||
await ClineEndpoint.initialize(tempDir)
|
||||
await ClineEndpoint.initialize()
|
||||
ClineEndpoint.isInitialized().should.be.true()
|
||||
|
||||
// Second initialize should be a no-op
|
||||
await ClineEndpoint.initialize(tempDir)
|
||||
await ClineEndpoint.initialize()
|
||||
ClineEndpoint.isInitialized().should.be.true()
|
||||
})
|
||||
|
||||
@@ -528,167 +527,16 @@ describe("ClineEndpoint configuration", () => {
|
||||
mcpBaseUrl: "https://mcp.enterprise.com",
|
||||
}
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
|
||||
await ClineEndpoint.initialize(tempDir)
|
||||
await ClineEndpoint.initialize()
|
||||
|
||||
ClineEndpoint.isSelfHosted().should.be.true()
|
||||
})
|
||||
|
||||
it("should return false when in normal mode (no endpoints.json)", async () => {
|
||||
// No endpoints.json file exists
|
||||
await ClineEndpoint.initialize(tempDir)
|
||||
await ClineEndpoint.initialize()
|
||||
|
||||
ClineEndpoint.isSelfHosted().should.be.false()
|
||||
})
|
||||
})
|
||||
|
||||
describe("bundled endpoints.json behavior", () => {
|
||||
let bundledDir: string
|
||||
let setVscodeHostProviderMock: (mock: { extensionFsPath: string; globalStorageFsPath: string }) => void
|
||||
|
||||
beforeEach(async () => {
|
||||
// Create a separate directory for bundled config
|
||||
bundledDir = path.join(os.tmpdir(), `config-bundled-test-${Date.now()}-${Math.random().toString(36).slice(2)}`)
|
||||
await fs.mkdir(bundledDir, { recursive: true })
|
||||
|
||||
// Import HostProvider utilities
|
||||
const hostProviderModule = await import("../test/host-provider-test-utils")
|
||||
setVscodeHostProviderMock = hostProviderModule.setVscodeHostProviderMock
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await fs.rm(bundledDir, { recursive: true, force: true })
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
})
|
||||
|
||||
it("should use bundled endpoints.json when available", async () => {
|
||||
const bundledConfig = {
|
||||
appBaseUrl: "https://bundled.enterprise.com",
|
||||
apiBaseUrl: "https://bundled-api.enterprise.com",
|
||||
mcpBaseUrl: "https://bundled-mcp.enterprise.com",
|
||||
}
|
||||
|
||||
// Set up bundled config
|
||||
await fs.writeFile(path.join(bundledDir, "endpoints.json"), JSON.stringify(bundledConfig), "utf8")
|
||||
|
||||
await ClineEndpoint.initialize(bundledDir)
|
||||
|
||||
const config = ClineEndpoint.config
|
||||
config.appBaseUrl.should.equal("https://bundled.enterprise.com")
|
||||
config.apiBaseUrl.should.equal("https://bundled-api.enterprise.com")
|
||||
config.mcpBaseUrl.should.equal("https://bundled-mcp.enterprise.com")
|
||||
config.environment.should.equal(Environment.selfHosted)
|
||||
})
|
||||
|
||||
it("should prefer bundled endpoints.json over user file", async () => {
|
||||
const bundledConfig = {
|
||||
appBaseUrl: "https://bundled.enterprise.com",
|
||||
apiBaseUrl: "https://bundled-api.enterprise.com",
|
||||
mcpBaseUrl: "https://bundled-mcp.enterprise.com",
|
||||
}
|
||||
|
||||
const userConfig = {
|
||||
appBaseUrl: "https://user.enterprise.com",
|
||||
apiBaseUrl: "https://user-api.enterprise.com",
|
||||
mcpBaseUrl: "https://user-mcp.enterprise.com",
|
||||
}
|
||||
|
||||
// Set up both configs
|
||||
await fs.writeFile(path.join(bundledDir, "endpoints.json"), JSON.stringify(bundledConfig), "utf8")
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(userConfig), "utf8")
|
||||
|
||||
await ClineEndpoint.initialize(bundledDir)
|
||||
|
||||
// Should use bundled config, not user config
|
||||
const config = ClineEndpoint.config
|
||||
config.appBaseUrl.should.equal("https://bundled.enterprise.com")
|
||||
config.apiBaseUrl.should.equal("https://bundled-api.enterprise.com")
|
||||
config.mcpBaseUrl.should.equal("https://bundled-mcp.enterprise.com")
|
||||
})
|
||||
|
||||
it("should fall back to user endpoints.json when bundled is not present", async () => {
|
||||
const userConfig = {
|
||||
appBaseUrl: "https://user.enterprise.com",
|
||||
apiBaseUrl: "https://user-api.enterprise.com",
|
||||
mcpBaseUrl: "https://user-mcp.enterprise.com",
|
||||
}
|
||||
|
||||
// Only create user config, no bundled config
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(userConfig), "utf8")
|
||||
|
||||
await ClineEndpoint.initialize(bundledDir)
|
||||
|
||||
// Should use user config
|
||||
const config = ClineEndpoint.config
|
||||
config.appBaseUrl.should.equal("https://user.enterprise.com")
|
||||
config.apiBaseUrl.should.equal("https://user-api.enterprise.com")
|
||||
config.mcpBaseUrl.should.equal("https://user-mcp.enterprise.com")
|
||||
})
|
||||
|
||||
it("should use standard mode when neither bundled nor user file exists", async () => {
|
||||
// No config files at all
|
||||
|
||||
await ClineEndpoint.initialize(bundledDir)
|
||||
|
||||
// Should use production defaults
|
||||
const config = ClineEndpoint.config
|
||||
config.environment.should.not.equal(Environment.selfHosted)
|
||||
config.appBaseUrl.should.equal("https://app.cline.bot")
|
||||
config.apiBaseUrl.should.equal("https://api.cline.bot")
|
||||
})
|
||||
|
||||
it("should throw ClineConfigurationError for invalid bundled file", async () => {
|
||||
const invalidConfig = {
|
||||
appBaseUrl: "not-a-url",
|
||||
apiBaseUrl: "https://api.enterprise.com",
|
||||
mcpBaseUrl: "https://mcp.enterprise.com",
|
||||
}
|
||||
|
||||
// Set up invalid bundled config
|
||||
await fs.writeFile(path.join(bundledDir, "endpoints.json"), JSON.stringify(invalidConfig), "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize(bundledDir)
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
error.message.should.containEql("must be a valid URL")
|
||||
error.message.should.containEql("bundled")
|
||||
}
|
||||
})
|
||||
|
||||
it("should throw ClineConfigurationError for invalid JSON in bundled file", async () => {
|
||||
// Set up invalid JSON in bundled file
|
||||
await fs.writeFile(path.join(bundledDir, "endpoints.json"), "{ invalid json }", "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize(bundledDir)
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
error.message.should.containEql("Invalid JSON")
|
||||
error.message.should.containEql("bundled")
|
||||
}
|
||||
})
|
||||
|
||||
it("should indicate bundled source in error messages", async () => {
|
||||
const incompleteConfig = {
|
||||
appBaseUrl: "https://bundled.enterprise.com",
|
||||
// Missing apiBaseUrl and mcpBaseUrl
|
||||
}
|
||||
|
||||
await fs.writeFile(path.join(bundledDir, "endpoints.json"), JSON.stringify(incompleteConfig), "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize(bundledDir)
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
error.message.should.containEql("Missing required field")
|
||||
error.message.should.containEql(path.join(bundledDir, "endpoints.json"))
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
+2
-2
@@ -38,11 +38,11 @@ export async function initialize(context: vscode.ExtensionContext): Promise<Webv
|
||||
Logger.subscribe((msg: string) => HostProvider.get().logToChannel(msg)) // File system logging
|
||||
Logger.subscribe((msg: string) => HostProvider.env.debugLog({ value: msg })) // Host debug logging
|
||||
|
||||
// Initialize ClineEndpoint configuration (reads bundled and ~/.cline/endpoints.json if present)
|
||||
// Initialize ClineEndpoint configuration first (reads ~/.cline/endpoints.json if present)
|
||||
// This must be done before any other code that calls ClineEnv.config()
|
||||
// Throws ClineConfigurationError if config file exists but is invalid
|
||||
const { ClineEndpoint } = await import("./config")
|
||||
await ClineEndpoint.initialize(HostProvider.get().extensionFsPath)
|
||||
await ClineEndpoint.initialize()
|
||||
|
||||
// Set the distinct ID for logging and telemetry
|
||||
await initializeDistinctId(context)
|
||||
|
||||
+10
-64
@@ -30,13 +30,10 @@ export class ClineConfigurationError extends Error {
|
||||
class ClineEndpoint {
|
||||
private static _instance: ClineEndpoint | null = null
|
||||
private static _initialized = false
|
||||
private static _extensionFsPath: string
|
||||
|
||||
// On-premise config loaded from file (null if not on-premise)
|
||||
private onPremiseConfig: EndpointsFileSchema | null = null
|
||||
private environment: Environment = Environment.production
|
||||
// Track if config came from bundled file (enterprise distribution)
|
||||
private isBundled: boolean = false
|
||||
|
||||
private constructor() {
|
||||
// Set environment at module load. Use override if provided.
|
||||
@@ -51,15 +48,13 @@ class ClineEndpoint {
|
||||
* Must be called before any other methods.
|
||||
* Reads the endpoints.json file if it exists and validates its schema.
|
||||
*
|
||||
* @param extensionFsPath Path to the extension installation directory (for checking bundled endpoints.json)
|
||||
* @throws ClineConfigurationError if the endpoints.json file exists but is invalid
|
||||
*/
|
||||
public static async initialize(extensionFsPath: string): Promise<void> {
|
||||
public static async initialize(): Promise<void> {
|
||||
if (ClineEndpoint._initialized) {
|
||||
return
|
||||
}
|
||||
|
||||
ClineEndpoint._extensionFsPath = extensionFsPath
|
||||
ClineEndpoint._instance = new ClineEndpoint()
|
||||
|
||||
// Try to load on-premise config from file
|
||||
@@ -92,18 +87,6 @@ class ClineEndpoint {
|
||||
return ClineEndpoint.config.environment === Environment.selfHosted
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the current configuration was loaded from a bundled endpoints.json file.
|
||||
* This indicates an enterprise distribution that should not auto-update.
|
||||
* @throws Error if not initialized
|
||||
*/
|
||||
public static isBundledConfig(): boolean {
|
||||
if (!ClineEndpoint._initialized || !ClineEndpoint._instance) {
|
||||
throw new Error("ClineEndpoint not initialized. Call ClineEndpoint.initialize() first.")
|
||||
}
|
||||
return ClineEndpoint._instance.isBundled
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the singleton instance.
|
||||
* @throws Error if not initialized
|
||||
@@ -131,53 +114,16 @@ class ClineEndpoint {
|
||||
return path.join(os.homedir(), ".cline", "endpoints.json")
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to the bundled endpoints.json configuration file.
|
||||
* Located in the extension installation directory.
|
||||
*/
|
||||
private static getBundledEndpointsFilePath(): string {
|
||||
return path.join(ClineEndpoint._extensionFsPath, "endpoints.json")
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads and validates the endpoints.json file.
|
||||
* Checks bundled location first, then falls back to user directory.
|
||||
* Priority: bundled endpoints.json → ~/.cline/endpoints.json → null (standard mode)
|
||||
* @returns The validated endpoints config, or null if no file exists
|
||||
* @throws ClineConfigurationError if a file exists but is invalid
|
||||
* @returns The validated endpoints config, or null if the file doesn't exist
|
||||
* @throws ClineConfigurationError if the file exists but is invalid
|
||||
*/
|
||||
private static async loadEndpointsFile(): Promise<EndpointsFileSchema | null> {
|
||||
// 1. Try bundled file
|
||||
const bundledPath = ClineEndpoint.getBundledEndpointsFilePath()
|
||||
const filePath = ClineEndpoint.getEndpointsFilePath()
|
||||
|
||||
try {
|
||||
await fs.access(bundledPath)
|
||||
// File exists, load and validate it
|
||||
const fileContent = await fs.readFile(bundledPath, "utf8")
|
||||
let data: unknown
|
||||
|
||||
try {
|
||||
data = JSON.parse(fileContent)
|
||||
} catch (parseError) {
|
||||
throw new ClineConfigurationError(
|
||||
`Invalid JSON in bundled endpoints configuration file (${bundledPath}): ${parseError instanceof Error ? parseError.message : String(parseError)}`,
|
||||
)
|
||||
}
|
||||
|
||||
const config = ClineEndpoint.validateEndpointsSchema(data, bundledPath)
|
||||
// Mark as bundled enterprise distribution
|
||||
ClineEndpoint._instance!.isBundled = true
|
||||
return config
|
||||
} catch (error) {
|
||||
if (error instanceof ClineConfigurationError) {
|
||||
throw error
|
||||
}
|
||||
// Bundled file doesn't exist or is not accessible, try user file
|
||||
}
|
||||
|
||||
// 2. Try ~/.cline/endpoints.json
|
||||
const userPath = ClineEndpoint.getEndpointsFilePath()
|
||||
try {
|
||||
await fs.access(userPath)
|
||||
await fs.access(filePath)
|
||||
} catch {
|
||||
// File doesn't exist - not on-premise mode
|
||||
return null
|
||||
@@ -185,24 +131,24 @@ class ClineEndpoint {
|
||||
|
||||
// File exists, must be valid or we fail
|
||||
try {
|
||||
const fileContent = await fs.readFile(userPath, "utf8")
|
||||
const fileContent = await fs.readFile(filePath, "utf8")
|
||||
let data: unknown
|
||||
|
||||
try {
|
||||
data = JSON.parse(fileContent)
|
||||
} catch (parseError) {
|
||||
throw new ClineConfigurationError(
|
||||
`Invalid JSON in user endpoints configuration file (${userPath}): ${parseError instanceof Error ? parseError.message : String(parseError)}`,
|
||||
`Invalid JSON in endpoints configuration file (${filePath}): ${parseError instanceof Error ? parseError.message : String(parseError)}`,
|
||||
)
|
||||
}
|
||||
|
||||
return ClineEndpoint.validateEndpointsSchema(data, userPath)
|
||||
return ClineEndpoint.validateEndpointsSchema(data, filePath)
|
||||
} catch (error) {
|
||||
if (error instanceof ClineConfigurationError) {
|
||||
throw error
|
||||
}
|
||||
throw new ClineConfigurationError(
|
||||
`Failed to read user endpoints configuration file (${userPath}): ${error instanceof Error ? error.message : String(error)}`,
|
||||
`Failed to read endpoints configuration file (${filePath}): ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +98,7 @@ function createHandlerForProvider(
|
||||
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
|
||||
thinkingBudgetTokens:
|
||||
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
|
||||
geminiThinkingLevel: mode === "plan" ? options.geminiPlanModeThinkingLevel : options.geminiActModeThinkingLevel,
|
||||
})
|
||||
case "bedrock":
|
||||
return new AwsBedrockHandler({
|
||||
@@ -132,7 +133,7 @@ function createHandlerForProvider(
|
||||
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
|
||||
geminiApiKey: options.geminiApiKey,
|
||||
geminiBaseUrl: options.geminiBaseUrl,
|
||||
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
|
||||
thinkingLevel: mode === "plan" ? options.geminiPlanModeThinkingLevel : options.geminiActModeThinkingLevel,
|
||||
ulid: options.ulid,
|
||||
})
|
||||
case "openai":
|
||||
@@ -172,7 +173,7 @@ function createHandlerForProvider(
|
||||
geminiBaseUrl: options.geminiBaseUrl,
|
||||
thinkingBudgetTokens:
|
||||
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
|
||||
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
|
||||
thinkingLevel: mode === "plan" ? options.geminiPlanModeThinkingLevel : options.geminiActModeThinkingLevel,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
ulid: options.ulid,
|
||||
})
|
||||
@@ -266,6 +267,7 @@ function createHandlerForProvider(
|
||||
openRouterProviderSorting: options.openRouterProviderSorting,
|
||||
openRouterModelId: mode === "plan" ? options.planModeOpenRouterModelId : options.actModeOpenRouterModelId,
|
||||
openRouterModelInfo: mode === "plan" ? options.planModeOpenRouterModelInfo : options.actModeOpenRouterModelInfo,
|
||||
geminiThinkingLevel: mode === "plan" ? options.geminiPlanModeThinkingLevel : options.geminiActModeThinkingLevel,
|
||||
})
|
||||
case "litellm":
|
||||
return new LiteLlmHandler({
|
||||
@@ -390,6 +392,7 @@ function createHandlerForProvider(
|
||||
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
|
||||
thinkingBudgetTokens:
|
||||
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
|
||||
geminiThinkingLevel: mode === "plan" ? options.geminiPlanModeThinkingLevel : options.geminiActModeThinkingLevel,
|
||||
})
|
||||
case "zai":
|
||||
return new ZAiHandler({
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import "should"
|
||||
import { ConverseStreamCommand } from "@aws-sdk/client-bedrock-runtime"
|
||||
import should from "should"
|
||||
import { Readable } from "stream"
|
||||
import type { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import type { AwsBedrockHandlerOptions } from "../bedrock"
|
||||
import { AwsBedrockHandler } from "../bedrock"
|
||||
|
||||
@@ -239,6 +238,11 @@ describe("AwsBedrockHandler", () => {
|
||||
handler = new AwsBedrockHandler(mockOptions)
|
||||
})
|
||||
|
||||
describe("reasoning content handling (deprecated)", () => {
|
||||
// These tests are for the old reasoningContent API that may be deprecated
|
||||
// Keep them for backward compatibility but they may fail with new API
|
||||
})
|
||||
|
||||
describe("thinking response handling (new API structure)", () => {
|
||||
it("should handle thinking response in additionalModelResponseFields", async () => {
|
||||
const mockChunks = [
|
||||
@@ -607,269 +611,6 @@ describe("AwsBedrockHandler", () => {
|
||||
results[1].cacheWriteTokens.should.equal(30)
|
||||
})
|
||||
})
|
||||
|
||||
describe("tool use handling", () => {
|
||||
it("should handle tool use content blocks", async () => {
|
||||
const mockChunks = [
|
||||
{ messageStart: { role: "assistant" } },
|
||||
{
|
||||
contentBlockStart: {
|
||||
contentBlockIndex: 1,
|
||||
start: { toolUse: { toolUseId: "tool-1", name: "read_file" } },
|
||||
},
|
||||
},
|
||||
{
|
||||
contentBlockDelta: {
|
||||
contentBlockIndex: 1,
|
||||
delta: { toolUse: { input: '{"path":' } },
|
||||
},
|
||||
},
|
||||
{
|
||||
contentBlockDelta: {
|
||||
contentBlockIndex: 1,
|
||||
delta: { toolUse: { input: '"test.ts"}' } },
|
||||
},
|
||||
},
|
||||
{ contentBlockStop: { contentBlockIndex: 1 } },
|
||||
{ messageStop: { stopReason: "tool_use" } },
|
||||
]
|
||||
|
||||
const mockClient = new MockBedrockClient(mockChunks)
|
||||
const command = new ConverseStreamCommand({ modelId: "test-model", messages: [] })
|
||||
|
||||
const originalGetBedrockClient = handler["getBedrockClient"]
|
||||
handler["getBedrockClient"] = async () => mockClient as any
|
||||
|
||||
const generator = handler["executeConverseStream"](command, mockModelInfo)
|
||||
const results = await collectGeneratorResults(generator)
|
||||
|
||||
handler["getBedrockClient"] = originalGetBedrockClient
|
||||
|
||||
results.should.have.length(2)
|
||||
results[0].type.should.equal("tool_calls")
|
||||
results[0].tool_call.function.id.should.equal("tool-1")
|
||||
results[0].tool_call.function.name.should.equal("read_file")
|
||||
results[0].tool_call.function.arguments.should.equal('{"path":')
|
||||
results[1].type.should.equal("tool_calls")
|
||||
results[1].tool_call.function.arguments.should.equal('"test.ts"}')
|
||||
})
|
||||
|
||||
it("should handle multiple tool calls", async () => {
|
||||
const mockChunks = [
|
||||
{ messageStart: { role: "assistant" } },
|
||||
{
|
||||
contentBlockStart: {
|
||||
contentBlockIndex: 1,
|
||||
start: { toolUse: { toolUseId: "tool-1", name: "read_file" } },
|
||||
},
|
||||
},
|
||||
{
|
||||
contentBlockDelta: {
|
||||
contentBlockIndex: 1,
|
||||
delta: { toolUse: { input: '{"path":"a.ts"}' } },
|
||||
},
|
||||
},
|
||||
{ contentBlockStop: { contentBlockIndex: 1 } },
|
||||
{
|
||||
contentBlockStart: {
|
||||
contentBlockIndex: 2,
|
||||
start: { toolUse: { toolUseId: "tool-2", name: "read_file" } },
|
||||
},
|
||||
},
|
||||
{
|
||||
contentBlockDelta: {
|
||||
contentBlockIndex: 2,
|
||||
delta: { toolUse: { input: '{"path":"b.ts"}' } },
|
||||
},
|
||||
},
|
||||
{ contentBlockStop: { contentBlockIndex: 2 } },
|
||||
{ messageStop: { stopReason: "tool_use" } },
|
||||
]
|
||||
|
||||
const mockClient = new MockBedrockClient(mockChunks)
|
||||
const command = new ConverseStreamCommand({ modelId: "test-model", messages: [] })
|
||||
|
||||
const originalGetBedrockClient = handler["getBedrockClient"]
|
||||
handler["getBedrockClient"] = async () => mockClient as any
|
||||
|
||||
const generator = handler["executeConverseStream"](command, mockModelInfo)
|
||||
const results = await collectGeneratorResults(generator)
|
||||
|
||||
handler["getBedrockClient"] = originalGetBedrockClient
|
||||
|
||||
results.should.have.length(2)
|
||||
results[0].tool_call.function.id.should.equal("tool-1")
|
||||
results[1].tool_call.function.id.should.equal("tool-2")
|
||||
})
|
||||
|
||||
it("should handle text and tool use interleaving", async () => {
|
||||
const mockChunks = [
|
||||
{ messageStart: { role: "assistant" } },
|
||||
{ contentBlockDelta: { delta: { text: "Checking" }, contentBlockIndex: 0 } },
|
||||
{ contentBlockStop: { contentBlockIndex: 0 } },
|
||||
{
|
||||
contentBlockStart: {
|
||||
contentBlockIndex: 1,
|
||||
start: { toolUse: { toolUseId: "tool-1", name: "read_file" } },
|
||||
},
|
||||
},
|
||||
{
|
||||
contentBlockDelta: {
|
||||
contentBlockIndex: 1,
|
||||
delta: { toolUse: { input: '{"path":"test.ts"}' } },
|
||||
},
|
||||
},
|
||||
{ contentBlockStop: { contentBlockIndex: 1 } },
|
||||
{ messageStop: { stopReason: "tool_use" } },
|
||||
]
|
||||
|
||||
const mockClient = new MockBedrockClient(mockChunks)
|
||||
const command = new ConverseStreamCommand({ modelId: "test-model", messages: [] })
|
||||
|
||||
const originalGetBedrockClient = handler["getBedrockClient"]
|
||||
handler["getBedrockClient"] = async () => mockClient as any
|
||||
|
||||
const generator = handler["executeConverseStream"](command, mockModelInfo)
|
||||
const results = await collectGeneratorResults(generator)
|
||||
|
||||
handler["getBedrockClient"] = originalGetBedrockClient
|
||||
|
||||
results.should.have.length(2)
|
||||
results[0].type.should.equal("text")
|
||||
results[0].text.should.equal("Checking")
|
||||
results[1].type.should.equal("tool_calls")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("tool config mapping", () => {
|
||||
it("should map Anthropic tools to Bedrock toolConfig", () => {
|
||||
const handler = new AwsBedrockHandler(mockOptions)
|
||||
const toolConfig = handler["mapClineToolsToBedrockToolConfig"]([
|
||||
{
|
||||
name: "read_file",
|
||||
description: "Read a file",
|
||||
input_schema: {
|
||||
type: "object",
|
||||
properties: { path: { type: "string" } },
|
||||
required: ["path"],
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
toolConfig?.tools?.should.have.length(1)
|
||||
const spec = toolConfig?.tools?.[0]?.toolSpec
|
||||
spec?.should.not.be.undefined()
|
||||
spec?.name?.should.equal("read_file")
|
||||
spec?.description?.should.equal("Read a file")
|
||||
;(spec as any).inputSchema.json.should.deepEqual({
|
||||
type: "object",
|
||||
properties: { path: { type: "string" } },
|
||||
required: ["path"],
|
||||
})
|
||||
})
|
||||
|
||||
it("should return undefined when tools is undefined or empty", () => {
|
||||
const handler = new AwsBedrockHandler(mockOptions)
|
||||
should.not.exist(handler["mapClineToolsToBedrockToolConfig"](undefined))
|
||||
should.not.exist(handler["mapClineToolsToBedrockToolConfig"]([]))
|
||||
})
|
||||
|
||||
it("should silently drop tools without input_schema", () => {
|
||||
const handler = new AwsBedrockHandler(mockOptions)
|
||||
// A tool missing input_schema doesn't match the AnthropicTool type guard
|
||||
const toolConfig = handler["mapClineToolsToBedrockToolConfig"]([
|
||||
{ name: "bad_tool", description: "No schema" } as any,
|
||||
])
|
||||
// All tools filtered out → undefined
|
||||
should.not.exist(toolConfig)
|
||||
})
|
||||
})
|
||||
|
||||
describe("formatMessagesForConverseAPI", () => {
|
||||
it("should format tool_use and tool_result blocks", () => {
|
||||
const handler = new AwsBedrockHandler(mockOptions)
|
||||
const messages: ClineStorageMessage[] = [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "tool_use",
|
||||
id: "tool-1",
|
||||
name: "read_file",
|
||||
input: { path: "test.ts" },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "tool_result",
|
||||
tool_use_id: "tool-1",
|
||||
content: "ok",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const formatted = handler["formatMessagesForConverseAPI"](messages)
|
||||
const toolUseBlock = formatted[0].content?.[0]?.toolUse
|
||||
const toolResultBlock = formatted[1].content?.[0]?.toolResult
|
||||
toolUseBlock?.should.not.be.undefined()
|
||||
toolResultBlock?.should.not.be.undefined()
|
||||
toolUseBlock?.toolUseId?.should.equal("tool-1")
|
||||
toolResultBlock?.toolUseId?.should.equal("tool-1")
|
||||
toolResultBlock?.content?.[0]?.text?.should.equal("ok")
|
||||
})
|
||||
|
||||
it("should format tool_result with array content", () => {
|
||||
const handler = new AwsBedrockHandler(mockOptions)
|
||||
const messages: ClineStorageMessage[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "tool_result",
|
||||
tool_use_id: "tool-2",
|
||||
content: [
|
||||
{ type: "text", text: "line 1" },
|
||||
{ type: "text", text: "line 2" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const formatted = handler["formatMessagesForConverseAPI"](messages)
|
||||
const toolResult = formatted[0].content?.[0]?.toolResult
|
||||
toolResult?.toolUseId?.should.equal("tool-2")
|
||||
toolResult?.content?.should.have.length(2)
|
||||
toolResult?.content?.[0]?.text?.should.equal("line 1")
|
||||
toolResult?.content?.[1]?.text?.should.equal("line 2")
|
||||
})
|
||||
|
||||
it("should map is_error to error status on tool_result", () => {
|
||||
const handler = new AwsBedrockHandler(mockOptions)
|
||||
const messages: ClineStorageMessage[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "tool_result",
|
||||
tool_use_id: "tool-3",
|
||||
content: "something went wrong",
|
||||
is_error: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const formatted = handler["formatMessagesForConverseAPI"](messages)
|
||||
const toolResult = formatted[0].content?.[0]?.toolResult
|
||||
toolResult?.status?.should.equal("error")
|
||||
toolResult?.content?.[0]?.text?.should.equal("something went wrong")
|
||||
})
|
||||
})
|
||||
|
||||
describe("getModelId", () => {
|
||||
@@ -1009,146 +750,4 @@ describe("AwsBedrockHandler", () => {
|
||||
modelId.should.not.match(/%3A/)
|
||||
})
|
||||
})
|
||||
|
||||
describe("native tool calling integration", () => {
|
||||
it("should be recognized as a next-gen provider eligible for native tool calling", () => {
|
||||
// This is the integration gap: if Bedrock is removed from isNextGenModelProvider(),
|
||||
// native tool calling silently stops working and falls back to XML tools.
|
||||
// Note: requires a Claude 4+ model — Claude 3.x is NOT in the next-gen model family.
|
||||
const { isNativeToolCallingConfig } = require("@utils/model-utils")
|
||||
|
||||
const claude4Options: AwsBedrockHandlerOptions = {
|
||||
...mockOptions,
|
||||
apiModelId: "anthropic.claude-sonnet-4-5-20250929-v1:0",
|
||||
}
|
||||
const handler = new AwsBedrockHandler(claude4Options)
|
||||
const model = handler.getModel()
|
||||
const providerInfo = {
|
||||
providerId: "bedrock",
|
||||
model: { id: model.id, info: model.info },
|
||||
}
|
||||
|
||||
const result = isNativeToolCallingConfig(providerInfo, true)
|
||||
result.should.be.true("Bedrock + Claude 4 should qualify for native tool calling")
|
||||
})
|
||||
|
||||
it("should not use native tool calling for pre-4.0 Claude models", () => {
|
||||
// Claude 3.x models are NOT in the next-gen family and should use XML tools
|
||||
const { isNativeToolCallingConfig } = require("@utils/model-utils")
|
||||
|
||||
const handler = new AwsBedrockHandler(mockOptions) // uses Claude 3.7
|
||||
const model = handler.getModel()
|
||||
const providerInfo = {
|
||||
providerId: "bedrock",
|
||||
model: { id: model.id, info: model.info },
|
||||
}
|
||||
|
||||
const result = isNativeToolCallingConfig(providerInfo, true)
|
||||
result.should.be.false("Bedrock + Claude 3.x should NOT use native tool calling")
|
||||
})
|
||||
|
||||
it("should not use native tool calling when the setting is disabled", () => {
|
||||
const { isNativeToolCallingConfig } = require("@utils/model-utils")
|
||||
|
||||
const claude4Options: AwsBedrockHandlerOptions = {
|
||||
...mockOptions,
|
||||
apiModelId: "anthropic.claude-sonnet-4-5-20250929-v1:0",
|
||||
}
|
||||
const handler = new AwsBedrockHandler(claude4Options)
|
||||
const model = handler.getModel()
|
||||
const providerInfo = {
|
||||
providerId: "bedrock",
|
||||
model: { id: model.id, info: model.info },
|
||||
}
|
||||
|
||||
const result = isNativeToolCallingConfig(providerInfo, false)
|
||||
result.should.be.false("Native tool calling should be disabled when setting is off")
|
||||
})
|
||||
|
||||
it("should pass toolConfig to ConverseStreamCommand when tools are provided", async () => {
|
||||
const handler = new AwsBedrockHandler(mockOptions)
|
||||
|
||||
// Capture the command passed to executeConverseStream
|
||||
let capturedCommand: any = null
|
||||
const originalExecuteConverseStream = handler["executeConverseStream"].bind(handler)
|
||||
handler["executeConverseStream"] = async function* (command: any, modelInfo: any) {
|
||||
capturedCommand = command
|
||||
// Yield nothing — we just want to capture the command
|
||||
}
|
||||
|
||||
const tools = [
|
||||
{
|
||||
name: "read_file",
|
||||
description: "Read a file",
|
||||
input_schema: {
|
||||
type: "object" as const,
|
||||
properties: { path: { type: "string" } },
|
||||
required: ["path"],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
// Consume the generator to trigger createAnthropicMessage
|
||||
const gen = handler["createAnthropicMessage"]("system prompt", [], "test-model", handler.getModel(), false, tools)
|
||||
for await (const _ of gen) {
|
||||
// drain
|
||||
}
|
||||
|
||||
// Verify the command includes toolConfig
|
||||
should.exist(capturedCommand, "ConverseStreamCommand should have been created")
|
||||
const input = capturedCommand.input
|
||||
should.exist(input.toolConfig, "toolConfig should be present in the command")
|
||||
input.toolConfig.tools.should.have.length(1)
|
||||
input.toolConfig.tools[0].toolSpec.name.should.equal("read_file")
|
||||
})
|
||||
|
||||
it("should format a complete tool call round-trip correctly", () => {
|
||||
// Simulates the full cycle: model returns tool_use → Cline executes → sends tool_result back
|
||||
const handler = new AwsBedrockHandler(mockOptions)
|
||||
|
||||
// Turn 1: assistant calls a tool
|
||||
// Turn 2: user sends tool result
|
||||
// Turn 3: assistant calls another tool (proves multi-turn works)
|
||||
// Turn 4: user sends second tool result
|
||||
const conversation: ClineStorageMessage[] = [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "text", text: "I'll read the file." },
|
||||
{ type: "tool_use", id: "call-1", name: "read_file", input: { path: "a.ts" } },
|
||||
],
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "tool_result", tool_use_id: "call-1", content: "export const a = 1" }],
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "tool_use", id: "call-2", name: "read_file", input: { path: "b.ts" } }],
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "tool_result", tool_use_id: "call-2", content: "export const b = 2", is_error: false }],
|
||||
},
|
||||
]
|
||||
|
||||
const formatted = handler["formatMessagesForConverseAPI"](conversation)
|
||||
|
||||
// Turn 1: text + toolUse
|
||||
formatted[0].content?.should.have.length(2)
|
||||
formatted[0].content?.[0]?.text?.should.equal("I'll read the file.")
|
||||
formatted[0].content?.[1]?.toolUse?.toolUseId?.should.equal("call-1")
|
||||
formatted[0].content?.[1]?.toolUse?.name?.should.equal("read_file")
|
||||
|
||||
// Turn 2: toolResult
|
||||
formatted[1].content?.[0]?.toolResult?.toolUseId?.should.equal("call-1")
|
||||
formatted[1].content?.[0]?.toolResult?.status?.should.equal("success")
|
||||
|
||||
// Turn 3: toolUse
|
||||
formatted[2].content?.[0]?.toolUse?.toolUseId?.should.equal("call-2")
|
||||
|
||||
// Turn 4: toolResult
|
||||
formatted[3].content?.[0]?.toolResult?.toolUseId?.should.equal("call-2")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -236,46 +236,6 @@ describe("ClaudeCodeHandler", () => {
|
||||
model.id.should.equal("claude-sonnet-4-5-20250929")
|
||||
})
|
||||
|
||||
it("should support Opus 4.6 1m model id", () => {
|
||||
const handler = new ClaudeCodeHandler({
|
||||
apiModelId: "claude-opus-4-6[1m]",
|
||||
})
|
||||
|
||||
const model = handler.getModel()
|
||||
model.id.should.equal("claude-opus-4-6[1m]")
|
||||
model.info.contextWindow.should.equal(1_000_000)
|
||||
})
|
||||
|
||||
it("should support Opus 1m alias model id", () => {
|
||||
const handler = new ClaudeCodeHandler({
|
||||
apiModelId: "opus[1m]",
|
||||
})
|
||||
|
||||
const model = handler.getModel()
|
||||
model.id.should.equal("opus[1m]")
|
||||
model.info.contextWindow.should.equal(1_000_000)
|
||||
})
|
||||
|
||||
it("should support Sonnet 1m alias model id", () => {
|
||||
const handler = new ClaudeCodeHandler({
|
||||
apiModelId: "sonnet[1m]",
|
||||
})
|
||||
|
||||
const model = handler.getModel()
|
||||
model.id.should.equal("sonnet[1m]")
|
||||
model.info.contextWindow.should.equal(1_000_000)
|
||||
})
|
||||
|
||||
it("should support Sonnet 4.5 1m model id", () => {
|
||||
const handler = new ClaudeCodeHandler({
|
||||
apiModelId: "claude-sonnet-4-5-20250929[1m]",
|
||||
})
|
||||
|
||||
const model = handler.getModel()
|
||||
model.id.should.equal("claude-sonnet-4-5-20250929[1m]")
|
||||
model.info.contextWindow.should.equal(1_000_000)
|
||||
})
|
||||
|
||||
it("should return default model when not specified", () => {
|
||||
const handler = new ClaudeCodeHandler({})
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ describe("LiteLlmHandler", () => {
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockFetchForTesting(mockFetch, () => {
|
||||
mockFetchForTesting(mockFetch as unknown as typeof globalThis.fetch, () => {
|
||||
return new Promise((resolve) => {
|
||||
doneMockingFetch = resolve
|
||||
})
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user