PNPM + Turbo monorepo + Nightly releases (#3407)

This commit is contained in:
Chris Estreich 2025-05-21 21:01:20 -07:00 committed by hannesrudolph
parent 12e26345a5
commit 1480ca2e1d
114 changed files with 25419 additions and 50906 deletions

1
.gitattributes vendored
View file

@ -1,2 +1,3 @@
demo.gif filter=lfs diff=lfs merge=lfs -text
assets/docs/demo.gif filter=lfs diff=lfs merge=lfs -text
src/assets/docs/demo.gif filter=lfs diff=lfs merge=lfs -text

View file

@ -1,83 +0,0 @@
name: AI Release Notes
description: Generate AI release notes using git and openai, outputs 'RELEASE_NOTES' and 'OPENAI_PROMPT'
inputs:
OPENAI_API_KEY:
required: true
type: string
GHA_PAT:
required: true
type: string
model_name:
required: false
type: string
default: gpt-4o-mini
repo_path:
required: false
type: string
custom_prompt:
required: false
default: ''
type: string
git_ref:
required: true
type: string
head_ref:
required: true
type: string
base_ref:
required: true
type: string
outputs:
RELEASE_NOTES:
description: "AI generated release notes"
value: ${{ steps.ai_release_notes.outputs.RELEASE_NOTES }}
OPENAI_PROMPT:
description: "Prompt used to generate release notes"
value: ${{ steps.ai_prompt.outputs.OPENAI_PROMPT }}
env:
GITHUB_REF: ${{ inputs.git_ref }}
BASE_REF: ${{ inputs.base_ref }}
HEAD_REF: ${{ inputs.head_ref }}
runs:
using: "composite"
steps:
- uses: actions/checkout@v4
with:
repository: ${{ inputs.repo_path }}
token: ${{ inputs.GHA_PAT }}
ref: ${{ env.GITHUB_REF }}
fetch-depth: 0
- name: Set Workspace
shell: bash
run: |
pip install tiktoken
pip install pytz
# Github outputs: 'OPENAI_PROMPT'
- name: Add Git Info to base prompt
id: ai_prompt
shell: bash
env:
BASE_REF: ${{ env.BASE_REF }}
HEAD_SHA: ${{ env.HEAD_SHA }}
PR_TITLE: ${{ github.event.pull_request.title }}
PR_BODY: ${{ github.event.pull_request.body }}
MODEL_NAME: ${{ inputs.model_name }}
CUSTOM_PROMPT: ${{ inputs.custom_prompt }} # Default: ''
run: python .github/scripts/release-notes-prompt.py
# Github outputs: 'RELEASE_NOTES'
- name: Generate AI release notes
id: ai_release_notes
shell: bash
env:
OPENAI_API_KEY: ${{ inputs.OPENAI_API_KEY }}
CUSTOM_PROMPT: ${{ steps.ai_prompt.outputs.OPENAI_PROMPT }}
MODEL_NAME: ${{ inputs.model_name }}
run: python .github/scripts/ai-release-notes.py

View file

@ -1,123 +0,0 @@
"""
AI-powered release notes generator that creates concise and informative release notes from git changes.
This script uses OpenAI's API to analyze git changes (summary, diff, and commit log) and generate
well-formatted release notes in markdown. It focuses on important changes and their impact,
particularly highlighting new types and schemas while avoiding repetitive information.
Environment Variables Required:
OPENAI_API_KEY: OpenAI API key for authentication
CHANGE_SUMMARY: Summary of changes made (optional if CUSTOM_PROMPT provided)
CHANGE_DIFF: Git diff of changes (optional if CUSTOM_PROMPT provided)
CHANGE_LOG: Git commit log (optional if CUSTOM_PROMPT provided)
GITHUB_OUTPUT: Path to GitHub output file
CUSTOM_PROMPT: Custom prompt to override default (optional)
"""
import os
import requests # type: ignore
import json
import tiktoken # type: ignore
OPENAI_API_KEY = os.environ["OPENAI_API_KEY"]
CHANGE_SUMMARY = os.environ.get('CHANGE_SUMMARY', '')
CHANGE_DIFF = os.environ.get('CHANGE_DIFF', '')
CHANGE_LOG = os.environ.get('CHANGE_LOG', '')
GITHUB_OUTPUT = os.getenv("GITHUB_OUTPUT")
OPEN_AI_BASE_URL = "https://api.openai.com/v1"
OPEN_API_HEADERS = {"Authorization": f"Bearer {OPENAI_API_KEY}", "Content-Type": "application/json"}
CUSTOM_PROMPT = os.environ.get('CUSTOM_PROMPT', '')
MODEL_NAME = os.environ.get('MODEL_NAME', 'gpt-3.5-turbo-16k')
def num_tokens_from_string(string: str, model_name: str) -> int:
"""
Calculate the number of tokens in a text string for a specific model.
Args:
string: The input text to count tokens for
model_name: Name of the OpenAI model to use for token counting
Returns:
int: Number of tokens in the input string
"""
encoding = tiktoken.encoding_for_model(model_name)
num_tokens = len(encoding.encode(string))
return num_tokens
def truncate_to_token_limit(text, max_tokens, model_name):
"""
Truncate text to fit within a maximum token limit for a specific model.
Args:
text: The input text to truncate
max_tokens: Maximum number of tokens allowed
model_name: Name of the OpenAI model to use for tokenization
Returns:
str: Truncated text that fits within the token limit
"""
encoding = tiktoken.encoding_for_model(model_name)
encoded = encoding.encode(text)
truncated = encoded[:max_tokens]
return encoding.decode(truncated)
def generate_release_notes(model_name):
"""
Generate release notes using OpenAI's API based on git changes.
Uses the GPT-3.5-turbo model to analyze change summary, commit log, and code diff
to generate concise and informative release notes in markdown format. The notes
focus on important changes and their impact, with sections for new types/schemas
and other updates.
Returns:
str: Generated release notes in markdown format
Raises:
requests.exceptions.RequestException: If the OpenAI API request fails
"""
max_tokens = 14000 # Reserve some tokens for the response
# Truncate inputs if necessary to fit within token limits
change_summary = '' if CUSTOM_PROMPT else truncate_to_token_limit(CHANGE_SUMMARY, 1000, model_name)
change_log = '' if CUSTOM_PROMPT else truncate_to_token_limit(CHANGE_LOG, 2000, model_name)
change_diff = '' if CUSTOM_PROMPT else truncate_to_token_limit(CHANGE_DIFF, max_tokens - num_tokens_from_string(change_summary, model_name) - num_tokens_from_string(change_log, model_name) - 1000, model_name)
url = f"{OPEN_AI_BASE_URL}/chat/completions"
# Construct prompt for OpenAI API
openai_prompt = CUSTOM_PROMPT if CUSTOM_PROMPT else f"""Based on the following summary of changes, commit log and code diff, please generate concise and informative release notes:
Summary of changes:
{change_summary}
Commit log:
{change_log}
Code Diff:
{json.dumps(change_diff)}
"""
data = {
"model": model_name,
"messages": [{"role": "user", "content": openai_prompt}],
"temperature": 0.7,
"max_tokens": 1000,
}
print("----------------------------------------------------------------------------------------------------------")
print("POST request to OpenAI")
print("----------------------------------------------------------------------------------------------------------")
ai_response = requests.post(url, headers=OPEN_API_HEADERS, json=data)
print(f"Status Code: {str(ai_response.status_code)}")
print(f"Response: {ai_response.text}")
ai_response.raise_for_status()
return ai_response.json()["choices"][0]["message"]["content"]
release_notes = generate_release_notes(MODEL_NAME)
print("----------------------------------------------------------------------------------------------------------")
print("OpenAI generated release notes")
print("----------------------------------------------------------------------------------------------------------")
print(release_notes)
# Write the release notes to GITHUB_OUTPUT
with open(GITHUB_OUTPUT, "a") as outputs_file:
outputs_file.write(f"RELEASE_NOTES<<EOF\n{release_notes}\nEOF")

View file

@ -1,52 +0,0 @@
import os
import re
import subprocess
def run_git_command(command):
result = subprocess.getoutput(command)
print(f"Git Command: {command}")
print(f"Git Output: {result}")
return result
def parse_merge_commit(line):
# Parse merge commit messages like:
# "355dc82 Merge pull request #71 from RooCodeInc/better-error-handling"
pattern = r"([a-f0-9]+)\s+Merge pull request #(\d+) from (.+)"
match = re.match(pattern, line)
if match:
sha, pr_number, branch = match.groups()
return {
'sha': sha,
'pr_number': pr_number,
'branch': branch
}
return None
def get_version_refs():
# Get the merge commits with full message
command = 'git log --merges --pretty=oneline -n 3'
result = run_git_command(command)
if result:
commits = result.split('\n')
if len(commits) >= 3:
# Parse HEAD~1 (PR to generate notes for)
head_info = parse_merge_commit(commits[1])
# Parse HEAD~2 (previous PR to compare against)
base_info = parse_merge_commit(commits[2])
if head_info and base_info:
# Set output for GitHub Actions
with open(os.environ['GITHUB_OUTPUT'], 'a') as gha_outputs:
gha_outputs.write(f"head_ref={head_info['sha']}\n")
gha_outputs.write(f"base_ref={base_info['sha']}")
print(f"Head ref (PR #{head_info['pr_number']}): {head_info['sha']}")
print(f"Base ref (PR #{base_info['pr_number']}): {base_info['sha']}")
return head_info, base_info
print("Could not find or parse sufficient merge history")
return None, None
if __name__ == "__main__":
head_info, base_info = get_version_refs()

View file

@ -1,62 +0,0 @@
"""
This script updates a specific version's release notes section in CHANGELOG.md with new content
or reformats existing content.
The script:
1. Takes a version number, changelog path, and optionally new content as input from environment variables
2. Finds the section in the changelog for the specified version
3. Either:
a) Replaces the content with new content if provided, or
b) Reformats existing content by:
- Removing the first two lines of the changeset format
- Ensuring version numbers are wrapped in square brackets
4. Writes the updated changelog back to the file
Environment Variables:
CHANGELOG_PATH: Path to the changelog file (defaults to 'CHANGELOG.md')
VERSION: The version number to update/format
PREV_VERSION: The previous version number (used to locate section boundaries)
NEW_CONTENT: Optional new content to insert for this version
"""
#!/usr/bin/env python3
import os
CHANGELOG_PATH = os.environ.get("CHANGELOG_PATH", "CHANGELOG.md")
VERSION = os.environ['VERSION']
PREV_VERSION = os.environ.get("PREV_VERSION", "")
NEW_CONTENT = os.environ.get("NEW_CONTENT", "")
def overwrite_changelog_section(changelog_text: str, new_content: str):
# Find the section for the specified version
version_pattern = f"## {VERSION}\n"
prev_version_pattern = f"## [{PREV_VERSION}]\n"
print(f"latest version: {VERSION}")
print(f"prev_version: {PREV_VERSION}")
notes_start_index = changelog_text.find(version_pattern) + len(version_pattern)
notes_end_index = changelog_text.find(prev_version_pattern, notes_start_index) if PREV_VERSION and prev_version_pattern in changelog_text else len(changelog_text)
if new_content:
return changelog_text[:notes_start_index] + f"{new_content}\n" + changelog_text[notes_end_index:]
else:
changeset_lines = changelog_text[notes_start_index:notes_end_index].split("\n")
# Remove the first two lines from the regular changeset format, ex: \n### Patch Changes
parsed_lines = "\n".join(changeset_lines[2:])
updated_changelog = changelog_text[:notes_start_index] + parsed_lines + changelog_text[notes_end_index:]
updated_changelog = updated_changelog.replace(f"## {VERSION}", f"## [{VERSION}]")
return updated_changelog
with open(CHANGELOG_PATH, 'r') as f:
changelog_content = f.read()
new_changelog = overwrite_changelog_section(changelog_content, NEW_CONTENT)
print("----------------------------------------------------------------------------------")
print(new_changelog)
print("----------------------------------------------------------------------------------")
# Write back to CHANGELOG.md
with open(CHANGELOG_PATH, 'w') as f:
f.write(new_changelog)
print(f"{CHANGELOG_PATH} updated successfully!")

View file

@ -1,64 +0,0 @@
"""
This script extracts the release notes section for a specific version from CHANGELOG.md.
The script:
1. Takes a version number and changelog path as input from environment variables
2. Finds the section in the changelog for the specified version
3. Extracts the content between the current version header and the next version header
(or end of file if it's the latest version)
4. Outputs the extracted release notes to GITHUB_OUTPUT for use in creating GitHub releases
Environment Variables:
GITHUB_OUTPUT: Path to GitHub Actions output file
CHANGELOG_PATH: Path to the changelog file (defaults to 'CHANGELOG.md')
VERSION: The version number to extract notes for
"""
#!/usr/bin/env python3
import sys
import os
import subprocess
GITHUB_OUTPUT = os.getenv("GITHUB_OUTPUT")
CHANGELOG_PATH = os.environ.get("CHANGELOG_PATH", "CHANGELOG.md")
VERSION = os.environ['VERSION']
def parse_changelog_section(content: str):
"""Parse a specific version section from the changelog content.
Args:
content: The full changelog content as a string
Returns:
The formatted content for this version, or None if version not found
Example:
>>> content = "## 1.2.0\\nChanges\\n## 1.1.0\\nOld changes"
>>> parse_changelog_section(content)
'Changes\\n'
"""
# Find the section for the specified version
version_pattern = f"## {VERSION}\n"
print(f"latest version: {VERSION}")
notes_start_index = content.find(version_pattern) + len(version_pattern)
prev_version = subprocess.getoutput("git show origin/main:package.json | grep '\"version\":' | cut -d'\"' -f4")
print(f"prev_version: {prev_version}")
prev_version_pattern = f"## {prev_version}\n"
notes_end_index = content.find(prev_version_pattern, notes_start_index) if prev_version_pattern in content else len(content)
return content[notes_start_index:notes_end_index]
with open(CHANGELOG_PATH, 'r') as f:
content = f.read()
formatted_content = parse_changelog_section(content)
if not formatted_content:
print(f"Version {VERSION} not found in changelog", file=sys.stderr)
sys.exit(1)
print(formatted_content)
# Write the extracted release notes to GITHUB_OUTPUT
with open(GITHUB_OUTPUT, "a") as gha_output:
gha_output.write(f"release-notes<<EOF\n{formatted_content}\nEOF")

View file

@ -1,125 +0,0 @@
import os
import subprocess
import json
import re
import tiktoken # type: ignore
from datetime import datetime;
from pytz import timezone
GITHUB_OUTPUT = os.getenv("GITHUB_OUTPUT")
BASE_REF = os.getenv("BASE_REF", "main")
HEAD_SHA = os.environ["HEAD_SHA"]
PR_TITLE = os.environ["PR_TITLE"]
PR_BODY = os.environ["PR_BODY"]
EXISTING_NOTES = os.environ.get("EXISTING_NOTES", "null")
MODEL_NAME = os.environ.get('MODEL_NAME', 'gpt-3.5-turbo-16k')
CUSTOM_PROMPT = os.environ.get('CUSTOM_PROMPT', '')
def extract_description_section(pr_body):
# Find content between ## Description and the next ## or end of text
description_match = re.search(r'## Description\s*\n(.*?)(?=\n##|$)', pr_body, re.DOTALL)
if description_match:
content = description_match.group(1).strip()
# Remove the comment line if it exists
comment_pattern = r'\[comment\]:.+?\n'
content = re.sub(comment_pattern, '', content)
return content.strip()
return ""
def extract_ellipsis_important(pr_body):
# Find content between <!-- ELLIPSIS_HIDDEN --> and <!-- ELLIPSIS_HIDDEN --> that contains [!IMPORTANT]
ellipsis_match = re.search(r'<!--\s*ELLIPSIS_HIDDEN\s*-->(.*?)<!--\s*ELLIPSIS_HIDDEN\s*-->', pr_body, re.DOTALL)
if ellipsis_match:
content = ellipsis_match.group(1).strip()
important_match = re.search(r'\[!IMPORTANT\](.*?)(?=\[!|$)', content, re.DOTALL)
if important_match:
important_text = important_match.group(1).strip()
important_text = re.sub(r'^-+\s*', '', important_text)
return important_text.strip()
return ""
def extract_coderabbit_summary(pr_body):
# Find content between ## Summary by CodeRabbit and the next ## or end of text
summary_match = re.search(r'## Summary by CodeRabbit\s*\n(.*?)(?=\n##|$)', pr_body, re.DOTALL)
return summary_match.group(1).strip() if summary_match else ""
def num_tokens_from_string(string: str, model_name: str) -> int:
"""
Calculate the number of tokens in a text string for a specific model.
Args:
string: The input text to count tokens for
model_name: Name of the OpenAI model to use for token counting
Returns:
int: Number of tokens in the input string
"""
encoding = tiktoken.encoding_for_model(model_name)
num_tokens = len(encoding.encode(string))
return num_tokens
def truncate_to_token_limit(text, max_tokens, model_name):
"""
Truncate text to fit within a maximum token limit for a specific model.
Args:
text: The input text to truncate
max_tokens: Maximum number of tokens allowed
model_name: Name of the OpenAI model to use for tokenization
Returns:
str: Truncated text that fits within the token limit
"""
encoding = tiktoken.encoding_for_model(model_name)
encoded = encoding.encode(text)
truncated = encoded[:max_tokens]
return encoding.decode(truncated)
# Extract sections and combine into PR_OVERVIEW
description = extract_description_section(PR_BODY)
important = extract_ellipsis_important(PR_BODY)
summary = extract_coderabbit_summary(PR_BODY)
PR_OVERVIEW = "\n\n".join(filter(None, [description, important, summary]))
# Get git information
base_sha = subprocess.getoutput(f"git rev-parse origin/{BASE_REF}") if BASE_REF == 'main' else BASE_REF
diff_overview = subprocess.getoutput(f"git diff {base_sha}..{HEAD_SHA} --name-status | awk '{{print $2}}' | sort | uniq -c | awk '{{print $2 \": \" $1 \" files changed\"}}'")
git_log = subprocess.getoutput(f"git log {base_sha}..{HEAD_SHA} --pretty=format:'%h - %s (%an)' --reverse | head -n 50")
git_diff = subprocess.getoutput(f"git diff {base_sha}..{HEAD_SHA} --minimal --abbrev --ignore-cr-at-eol --ignore-space-at-eol --ignore-space-change --ignore-all-space --ignore-blank-lines --unified=0 --diff-filter=ACDMRT")
max_tokens = 14000 # Reserve some tokens for the response
changes_summary = truncate_to_token_limit(diff_overview, 1000, MODEL_NAME)
git_logs = truncate_to_token_limit(git_log, 2000, MODEL_NAME)
changes_diff = truncate_to_token_limit(git_diff, max_tokens - num_tokens_from_string(changes_summary, MODEL_NAME) - num_tokens_from_string(git_logs, MODEL_NAME) - 1000, MODEL_NAME)
# Get today's existing changelog if any
existing_changelog = EXISTING_NOTES if EXISTING_NOTES != "null" else None
existing_changelog_text = f"\nAdditional context:\n{existing_changelog}" if existing_changelog else ""
TODAY = datetime.now(timezone('US/Eastern')).isoformat(sep=' ', timespec='seconds')
BASE_PROMPT = CUSTOM_PROMPT if CUSTOM_PROMPT else f"""Based on the following 'PR Information', please generate concise and informative release notes to be read by developers.
Format the release notes with markdown, and always use this structure: a descriptive and very short title (no more than 8 words) with heading level 2, a paragraph with a summary of changes (no header), and if applicable, sections for '🚀 New Features & Improvements', '🐛 Bugs Fixed' and '🔧 Other Updates', with heading level 3, skip respectively the sections if not applicable.
Finally include the following markdown comment with the PR merged date: <!-- PR_DATE: {TODAY} -->.
Avoid being repetitive and focus on the most important changes and their impact, discard any mention of version bumps/updates, changeset files, environment variables or syntax updates.
PR Information:"""
OPENAI_PROMPT = f"""{BASE_PROMPT}
Git log summary:
{changes_summary}
Commit Messages:
{git_logs}
PR Title:
{PR_TITLE}
PR Overview:
{PR_OVERVIEW}{existing_changelog_text}
Code Diff:
{json.dumps(changes_diff)}"""
print("OpenAI Prompt")
print("----------------------------------------------------------------")
print(OPENAI_PROMPT)
# Write the prompt to GITHUB_OUTPUT
with open(GITHUB_OUTPUT, "a") as outputs_file:
outputs_file.write(f"OPENAI_PROMPT<<EOF\n{OPENAI_PROMPT}\nEOF")

View file

@ -1,45 +0,0 @@
name: Build VSIX
on:
pull_request:
types: [labeled]
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
if: github.event.label.name == 'build'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version-file: 'package.json'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Install all dependencies
run: npm run install:all
- name: Build Extension
run: npm run build
- name: Upload VSIX artifact
uses: actions/upload-artifact@v4
with:
name: extension-vsix
path: bin/*.vsix
- name: Comment PR with artifact link
if: github.event_name == 'pull_request'
uses: peter-evans/create-or-update-comment@v4
with:
issue-number: ${{ github.event.pull_request.number }}
body: |
Build successful! 🚀
You can download the VSIX extension [here](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}).

View file

@ -10,6 +10,7 @@ env:
REPO_PATH: ${{ github.repository }}
GIT_REF: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || 'main' }}
NODE_VERSION: 20.18.1
PNPM_VERSION: 10.8.1
jobs:
# Job 1: Create version bump PR when changesets are merged to main
@ -30,15 +31,18 @@ jobs:
with:
fetch-depth: 0
ref: ${{ env.GIT_REF }}
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
cache: 'pnpm'
- name: Install Dependencies
run: npm run install:all
run: pnpm install
# Check if there are any new changesets to process
- name: Check for changesets
@ -56,7 +60,7 @@ jobs:
with:
commit: "changeset version bump"
title: "Changeset version bump"
version: npm run version-packages # This performs the changeset version bump
version: pnpm --filter roo-cline version-packages # This performs the changeset version bump
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View file

@ -10,6 +10,7 @@ on:
env:
NODE_VERSION: 20.18.1
PNPM_VERSION: 10.8.1
jobs:
compile:
@ -17,32 +18,38 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
cache: 'pnpm'
- name: Install dependencies
run: npm run install:all
- name: Compile
run: npm run compile
run: pnpm install
- name: Check types
run: npm run check-types
run: pnpm check-types
- name: Lint
run: npm run lint
run: pnpm lint
check-translations:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'
- name: Install dependencies
run: npm run install:all
run: pnpm install
- name: Verify all translations are complete
run: node scripts/find-missing-translations.js
@ -51,15 +58,19 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'
- name: Install dependencies
run: npm run install:all
run: pnpm install
- name: Run knip checks
run: npm run knip
run: pnpm knip
test-extension:
runs-on: ${{ matrix.os }}
@ -69,19 +80,20 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
cache: 'pnpm'
- name: Install dependencies
run: npm run install:all
- name: Compile (to build and copy WASM files)
run: npm run compile
- name: Run jest unit tests
run: npx jest --silent
- name: Run vitest unit tests
run: npx vitest run --silent
run: pnpm install
- name: Run unit tests
working-directory: src
run: pnpm test
test-webview:
runs-on: ${{ matrix.os }}
@ -91,16 +103,20 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'
- name: Install dependencies
run: npm run install:all
run: pnpm install
- name: Run unit tests
working-directory: webview-ui
run: npx jest --silent
run: pnpm test
unit-test:
needs: [test-extension, test-webview]
@ -131,16 +147,20 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
cache: 'pnpm'
- name: Install dependencies
run: npm run install:all
run: pnpm install
- name: Create .env.local file
working-directory: e2e
run: echo "OPENROUTER_API_KEY=${{ secrets.OPENROUTER_API_KEY }}" > .env.local
- name: Run integration tests
working-directory: e2e
run: xvfb-run -a npm run ci
run: xvfb-run -a pnpm test:ci

View file

@ -1,14 +1,3 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL Advanced"
on:

View file

@ -29,20 +29,21 @@ jobs:
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Install Dependencies
run: npm run install:all
run: pnpm install
- name: Create .env file
run: echo "POSTHOG_API_KEY=${{ secrets.POSTHOG_API_KEY }}" >> .env
- name: Package Extension
run: |
current_package_version=$(node -p "require('./package.json').version")
npm run vsix
pnpm vsix
package=$(unzip -l bin/roo-cline-${current_package_version}.vsix)
echo "$package" | grep -q "extension/package.json" || exit 1
echo "$package" | grep -q "extension/package.nls.json" || exit 1
echo "$package" | grep -q "extension/dist/extension.js" || exit 1
echo "$package" | grep -q "extension/webview-ui/audio/celebration.wav" || exit 1
echo "$package" | grep -q "extension/webview-ui/build/assets/index.js" || exit 1
echo "$package" | grep -q "extension/node_modules/@vscode/codicons/dist/codicon.ttf" || exit 1
echo "$package" | grep -q "extension/assets/codicons/codicon.ttf" || exit 1
echo "$package" | grep -q "extension/assets/vscode-material-icons/icons/3d.svg" || exit 1
echo "$package" | grep -q ".env" || exit 1
- name: Create and Push Git Tag
run: |
@ -56,7 +57,7 @@ jobs:
OVSX_PAT: ${{ secrets.OVSX_PAT }}
run: |
current_package_version=$(node -p "require('./package.json').version")
npm run publish:marketplace
pnpm publish:marketplace
echo "Successfully published version $current_package_version to VS Code Marketplace"
- name: Create GitHub Release
env:

67
.github/workflows/nightly-publish.yml vendored Normal file
View file

@ -0,0 +1,67 @@
name: Nightly Publish
on:
# push:
# branches: [main]
workflow_run:
workflows: ["Code QA Roo Code"]
types:
- completed
branches: [main]
workflow_dispatch: # Allows manual triggering.
env:
NODE_VERSION: 20.18.1
PNPM_VERSION: 10.8.1
jobs:
publish-nightly:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
permissions:
contents: read # No tags pushed → read is enough.
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Count commits
id: count
run: echo "total=$(git rev-list --all --count)" >> $GITHUB_OUTPUT
- name: Patch package.json version
env:
COMMIT_COUNT: ${{ steps.count.outputs.total }}
run: |
node <<'EOF'
const fs = require('fs');
const path = require('path');
const pkgPath = path.join(__dirname, 'apps', 'vscode-nightly', 'package.nightly.json');
const pkg = JSON.parse(fs.readFileSync(pkgPath,'utf8'));
const [maj, min] = pkg.version.split('.');
pkg.version = `${maj}.${min}.${process.env.COMMIT_COUNT}`;
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
console.log(`🔖 Nightly version set to ${pkg.version}`);
EOF
- name: Build VSIX
run: pnpm build:nightly # Produces bin/roo-code-nightly-0.0.[count].vsix
- name: Publish to VS Code Marketplace
env:
VSCE_PAT: ${{ secrets.VSCE_PAT }}
run: npx vsce publish --packagePath "bin/$(/bin/ls bin | head -n1)"
- name: Publish to Open VSX Registry
env:
OVSX_PAT: ${{ secrets.OVSX_PAT }}
run: npx ovsx publish "bin/$(ls bin | head -n1)"

View file

@ -6,6 +6,10 @@ on:
- main
workflow_dispatch: # Allows manual triggering
env:
NODE_VERSION: 20.18.1
PNPM_VERSION: 10.8.1
jobs:
update-contributors:
runs-on: ubuntu-latest
@ -15,30 +19,29 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'
- name: Disable Husky
run: |
echo "HUSKY=0" >> $GITHUB_ENV
git config --global core.hooksPath /dev/null
- name: Install dependencies
run: npm ci
run: pnpm install
- name: Update contributors and format
run: |
npm run update-contributors
pnpm update-contributors
npx prettier --write README.md
if git diff --quiet; then echo "changes=false" >> $GITHUB_OUTPUT; else echo "changes=true" >> $GITHUB_OUTPUT; fi
id: check-changes
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create Pull Request
if: steps.check-changes.outputs.changes == 'true'
uses: peter-evans/create-pull-request@v5
@ -51,6 +54,6 @@ jobs:
title: "Update contributors list"
body: |
Automated update of contributors list and related files
This PR was created automatically by a GitHub Action workflow and includes all changed files.
base: main

7
.gitignore vendored
View file

@ -31,15 +31,18 @@ docs/_site/
!.env.*.sample
#Local lint config
# Local lint config
.eslintrc.local.json
#Logging
# Logging
logs
# Vite development
.vite-port
# Turborepo
.turbo
# IntelliJ and Qodo plugin folders
.idea/
.qodo/

View file

@ -5,19 +5,26 @@ if [ "$branch" = "main" ]; then
exit 1
fi
# Detect if running on Windows and use npx.cmd, otherwise use npx
# Detect if running on Windows and use pnpm.cmd, otherwise use pnpm.
if [ "$OS" = "Windows_NT" ]; then
pnpm_cmd="pnpm.cmd"
else
pnpm_cmd="pnpm"
fi
"$pnpm_cmd" --filter roo-cline generate-types
if [ -n "$(git diff --name-only src/exports/roo-code.d.ts)" ]; then
echo "Error: There are unstaged changes to roo-code.d.ts after running 'pnpm --filter roo-cline generate-types'."
echo "Please review and stage the changes before committing."
exit 1
fi
# Detect if running on Windows and use npx.cmd, otherwise use npx.
if [ "$OS" = "Windows_NT" ]; then
npx_cmd="npx.cmd"
else
npx_cmd="npx"
fi
npm run generate-types
if [ -n "$(git diff --name-only src/exports/roo-code.d.ts)" ]; then
echo "Error: There are unstaged changes to roo-code.d.ts after running 'npm run generate-types'."
echo "Please review and stage the changes before committing."
exit 1
fi
"$npx_cmd" lint-staged

View file

@ -5,14 +5,14 @@ if [ "$branch" = "main" ]; then
exit 1
fi
# Detect if running on Windows and use npm.cmd, otherwise use npm
# Detect if running on Windows and use pnpm.cmd, otherwise use pnpm.
if [ "$OS" = "Windows_NT" ]; then
npm_cmd="npm.cmd"
pnpm_cmd="pnpm.cmd"
else
npm_cmd="npm"
pnpm_cmd="pnpm"
fi
"$npm_cmd" run compile
"$pnpm_cmd" run lint check-types
# Check for new changesets.
NEW_CHANGESETS=$(find .changeset -name "*.md" ! -name "README.md" | wc -l | tr -d ' ')
@ -20,6 +20,6 @@ echo "Changeset files: $NEW_CHANGESETS"
if [ "$NEW_CHANGESETS" == "0" ]; then
echo "-------------------------------------------------------------------------------------"
echo "Changes detected. Please run 'npm run changeset' to create a changeset if applicable."
echo "Changes detected. Please run 'pnpm changeset' to create a changeset if applicable."
echo "-------------------------------------------------------------------------------------"
fi

1
.npmrc
View file

@ -1 +0,0 @@
registry=https://registry.npmjs.org/

View file

@ -1,5 +0,0 @@
dist/
node_modules
webview-ui/build/
CHANGELOG.md
package-lock.json

4
.vscode/launch.json vendored
View file

@ -10,9 +10,9 @@
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
"args": ["--extensionDevelopmentPath=${workspaceFolder}/src"],
"sourceMaps": true,
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
"outFiles": ["${workspaceFolder}/src/dist/**/*.js"],
"preLaunchTask": "${defaultBuildTask}",
"env": {
"NODE_ENV": "development",

26
.vscode/tasks.json vendored
View file

@ -5,7 +5,7 @@
"tasks": [
{
"label": "watch",
"dependsOn": ["npm: dev", "npm: watch:tsc", "npm: watch:esbuild"],
"dependsOn": ["webview", "watch:tsc", "watch:esbuild"],
"presentation": {
"reveal": "never"
},
@ -15,9 +15,9 @@
}
},
{
"label": "npm: dev",
"type": "npm",
"script": "dev",
"label": "webview",
"type": "shell",
"command": "pnpm --filter @roo-code/vscode-webview dev",
"group": "build",
"problemMatcher": {
"owner": "vite",
@ -32,14 +32,14 @@
},
"isBackground": true,
"presentation": {
"group": "webview-ui",
"group": "watch",
"reveal": "always"
}
},
{
"label": "npm: watch:esbuild",
"type": "npm",
"script": "watch:esbuild",
"label": "watch:esbuild",
"type": "shell",
"command": "pnpm --filter roo-cline watch:esbuild",
"group": "build",
"problemMatcher": {
"owner": "esbuild",
@ -48,8 +48,8 @@
},
"background": {
"activeOnStart": true,
"beginsPattern": "\\[watch\\] build started",
"endsPattern": "\\[watch\\] build finished"
"beginsPattern": "esbuild-problem-matcher#onStart",
"endsPattern": "esbuild-problem-matcher#onEnd"
}
},
"isBackground": true,
@ -59,9 +59,9 @@
}
},
{
"label": "npm: watch:tsc",
"type": "npm",
"script": "watch:tsc",
"label": "watch:tsc",
"type": "shell",
"command": "pnpm --filter roo-cline watch:tsc",
"group": "build",
"problemMatcher": "$tsc-watch",
"isBackground": true,

View file

@ -1,74 +0,0 @@
# Default
.changeset/**
.github/**
.husky/**
.vscode/**
coverage/**
node_modules/**
src/**
scripts/**
.gitignore
esbuild.js
jest.*
**/tsconfig.json
**/.eslintrc.json
.prettierignore
**/*.map
**/*.ts
**/.gitignore
# Custom
.env.sample
.git-blame-ignore-revs
.gitconfig
.gitattributes
.tool-versions
.vite-port
.nvmrc
.clinerules*
.roomodes
.rooignore
.roo/**
benchmark/**
docs/**
e2e/**
evals/**
locales/**
out/**
ellipsis.yaml
knip.json
# Ignore all webview-ui files except the build directory.
# https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/frameworks/hello-world-react-cra/.vscodeignore
webview-ui/src/**
webview-ui/public/**
webview-ui/scripts/**
webview-ui/index.html
webview-ui/README.md
webview-ui/package.json
webview-ui/package-lock.json
webview-ui/node_modules/**
# Include codicons
!node_modules/@vscode/codicons/dist/codicon.css
!node_modules/@vscode/codicons/dist/codicon.ttf
# Include material icons
!node_modules/vscode-material-icons/generated/**
# Include default themes JSON files used in getTheme
!src/integrations/theme/default-themes/**
# Ignore doc assets
assets/docs/**
# Include icons and images
!assets/icons/**
!assets/images/**
# Include .env file for telemetry
!.env
# Ignore IntelliJ and Qodo plugin folders
.idea/**
.qodo/**

View file

@ -92,7 +92,7 @@ git clone https://github.com/YOUR_USERNAME/Roo-Code.git
2. **Install Dependencies:**
```
npm run install:all
pnpm install
```
3. **Debugging:** Open with VS Code (`F5`).

29
MONOREPO.md Normal file
View file

@ -0,0 +1,29 @@
# Monorepo Guide
Roo Code has transitioned to a monorepo powered by [PNPM workspaces](https://pnpm.io/workspaces) and [Turborepo](https://turborepo.com).
When you first pull down the monorepo changes from git you'll need to re-install all packages using pnpm. You can install pnpm using [these](https://pnpm.io/installation) instructions. If you're on MacOS the easiest option is to use Homebrew:
```sh
brew install pnpm
```
Once pnpm is installed you should wipe out your existing node_modules directories for a fresh start:
```sh
# This is optional, but recommended.
find . -name node_modules | xargs rm -rvf
```
And then install your packages:
```sh
pnpm install
```
If things are in good working order then you should be able to build a vsix and install it in VSCode:
```sh
pnpm build --out ../bin/roo-code-main.vsix && \
code --install-extension bin/roo-code-main.vsix
```

View file

@ -133,24 +133,19 @@ git clone https://github.com/RooCodeInc/Roo-Code.git
2. **Install dependencies**:
```sh
npm run install:all
pnpm install
```
3. **Start the webview (Vite/React app with HMR)**:
3. **Run the extension**:
```sh
npm run dev
```
4. **Debug**:
Press `F5` (or **Run****Start Debugging**) in VSCode to open a new session with Roo Code loaded.
Press `F5` (or **Run****Start Debugging**) in VSCode to open a new window with Roo Code running.
Changes to the webview will appear immediately. Changes to the core extension will require a restart of the extension host.
Alternatively you can build a .vsix and install it directly in VSCode:
```sh
npm run build
pnpm build
```
A `.vsix` file will appear in the `bin/` directory which can be installed with:

View file

@ -0,0 +1,25 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module"
},
"plugins": ["@typescript-eslint"],
"rules": {
"@typescript-eslint/naming-convention": [
"warn",
{
"selector": "import",
"format": ["camelCase", "PascalCase"]
}
],
"@typescript-eslint/semi": "off",
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error", { "varsIgnorePattern": "^_", "argsIgnorePattern": "^_" }],
"eqeqeq": "warn",
"no-throw-literal": "warn",
"semi": "off"
},
"ignorePatterns": ["dist"]
}

1
apps/vscode-nightly/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
build

View file

@ -0,0 +1,163 @@
import * as esbuild from "esbuild"
import * as fs from "fs"
import * as path from "path"
import { fileURLToPath } from "url"
import { getGitSha, copyPaths, copyLocales, copyWasms, generatePackageJson } from "@roo-code/build"
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
async function main() {
const production = process.argv.includes("--production")
const minify = production
const sourcemap = !production
const overrideJson = JSON.parse(fs.readFileSync(path.join(__dirname, "package.nightly.json"), "utf8"))
console.log(`[main] name: ${overrideJson.name}`)
console.log(`[main] version: ${overrideJson.version}`)
const gitSha = getGitSha()
console.log(`[main] gitSha: ${gitSha}`)
/**
* @type {import('esbuild').BuildOptions}
*/
const buildOptions = {
bundle: true,
minify,
sourcemap,
logLevel: "silent",
format: "cjs",
sourcesContent: false,
platform: "node",
define: {
"process.env.PKG_NAME": '"roo-code-nightly"',
"process.env.PKG_VERSION": `"${overrideJson.version}"`,
"process.env.PKG_OUTPUT_CHANNEL": '"Roo-Code-Nightly"',
...(gitSha ? { "process.env.PKG_SHA": `"${gitSha}"` } : {}),
},
}
const srcDir = path.join(__dirname, "..", "..", "src")
const buildDir = path.join(__dirname, "build")
const distDir = path.join(buildDir, "dist")
/**
* @type {import('esbuild').Plugin[]}
*/
const plugins = [
{
name: "copy-files",
setup(build) {
build.onEnd(() => {
copyPaths(
[
["../README.md", "README.md"],
["../CHANGELOG.md", "CHANGELOG.md"],
["../LICENSE", "LICENSE"],
[".vscodeignore", ".vscodeignore"],
["assets", "assets"],
["integrations", "integrations"],
["node_modules/vscode-material-icons/generated", "assets/vscode-material-icons"],
["../webview-ui/audio", "webview-ui/audio"],
],
srcDir,
buildDir,
)
})
},
},
{
name: "generate-package-json",
setup(build) {
build.onEnd(() => {
const packageJson = JSON.parse(fs.readFileSync(path.join(srcDir, "package.json"), "utf8"))
const generatedPackageJson = generatePackageJson({
packageJson,
overrideJson,
substitution: ["roo-cline", "roo-code-nightly"],
})
fs.writeFileSync(path.join(buildDir, "package.json"), JSON.stringify(generatedPackageJson, null, 2))
console.log(`[generate-package-json] Generated package.json`)
let count = 0
fs.readdirSync(path.join(srcDir)).forEach((file) => {
if (file.startsWith("package.nls")) {
fs.copyFileSync(path.join(srcDir, file), path.join(buildDir, file))
count++
}
})
console.log(`[copy-src] Copied ${count} package.nls*.json files to ${buildDir}`)
const nlsPkg = JSON.parse(fs.readFileSync(path.join(srcDir, "package.nls.json"), "utf8"))
const nlsNightlyPkg = JSON.parse(
fs.readFileSync(path.join(__dirname, "package.nls.nightly.json"), "utf8"),
)
fs.writeFileSync(
path.join(buildDir, "package.nls.json"),
JSON.stringify({ ...nlsPkg, ...nlsNightlyPkg }, null, 2),
)
console.log(`[copy-src] Generated package.nls.json`)
})
},
},
{
name: "copy-wasms",
setup(build) {
build.onEnd(() => copyWasms(srcDir, distDir))
},
},
{
name: "copy-locales",
setup(build) {
build.onEnd(() => copyLocales(srcDir, distDir))
},
},
]
/**
* @type {import('esbuild').BuildOptions}
*/
const extensionBuildOptions = {
...buildOptions,
plugins,
entryPoints: [path.join(srcDir, "extension.ts")],
outfile: path.join(distDir, "extension.js"),
external: ["vscode"],
}
/**
* @type {import('esbuild').BuildOptions}
*/
const workerBuildOptions = {
...buildOptions,
entryPoints: [path.join(srcDir, "workers", "countTokens.ts")],
outdir: path.join(distDir, "workers"),
}
const [extensionBuildContext, workerBuildContext] = await Promise.all([
esbuild.context(extensionBuildOptions),
esbuild.context(workerBuildOptions),
])
await Promise.all([
extensionBuildContext.rebuild(),
extensionBuildContext.dispose(),
workerBuildContext.rebuild(),
workerBuildContext.dispose(),
])
}
main().catch((e) => {
console.error(e)
process.exit(1)
})

View file

@ -0,0 +1,15 @@
{
"name": "@roo-code/vscode-nightly",
"description": "Nightly build for the Roo Code VSCode extension.",
"private": true,
"packageManager": "pnpm@10.8.1",
"scripts": {
"bundle": "pnpm clean && pnpm --filter @roo-code/build build && node esbuild.mjs",
"build": "pnpm bundle --production && pnpm --filter @roo-code/vscode-webview build --mode nightly",
"vsix": "pnpm build && cd build && mkdirp ../../../bin && npx vsce package --no-dependencies --out ../../../bin",
"clean": "rimraf build .turbo"
},
"devDependencies": {
"@roo-code/build": "workspace:^"
}
}

View file

@ -0,0 +1,6 @@
{
"name": "roo-code-nightly",
"version": "0.0.1",
"icon": "assets/icons/icon-nightly.png",
"scripts": {}
}

View file

@ -0,0 +1,7 @@
{
"extension.displayName": "Roo Code Nightly",
"views.contextMenu.label": "Roo Code Nightly",
"views.terminalMenu.label": "Roo Code Nightly",
"views.activitybar.title": "Roo Code Nightly",
"configuration.title": "Roo Code Nightly"
}

View file

@ -1,621 +0,0 @@
# Cache Strategy Documentation
This document provides an overview of the cache strategy implementation for Amazon Bedrock in the Roo-Code project, including class relationships and sequence diagrams.
## Class Relationship Diagram
```mermaid
classDiagram
class CacheStrategy {
<<abstract>>
#config: CacheStrategyConfig
#systemTokenCount: number
+determineOptimalCachePoints(): CacheResult
#initializeMessageGroups(): void
#calculateSystemTokens(): void
#createCachePoint(): ContentBlock
#messagesToContentBlocks(messages): Message[]
#meetsMinTokenThreshold(tokenCount): boolean
#estimateTokenCount(message): number
#applyCachePoints(messages, placements): Message[]
#formatResult(systemBlocks, messages): CacheResult
}
class MultiPointStrategy {
+determineOptimalCachePoints(): CacheResult
-determineMessageCachePoints(minTokensPerPoint, remainingCachePoints): CachePointPlacement[]
-formatWithoutCachePoints(): CacheResult
-findOptimalPlacementForRange(startIndex, endIndex, minTokensPerPoint): CachePointPlacement
}
class AwsBedrockHandler {
-client: BedrockRuntimeClient
-costModelConfig: object
-previousCachePointPlacements: Map<string, CachePointPlacement[]>
+createMessage(systemPrompt, messages): ApiStream
+completePrompt(prompt): Promise<string>
-supportsAwsPromptCache(modelConfig): boolean
-getModelByName(modelName): object
+getModel(): object
-removeCachePoints(content): any
-convertToBedrockConverseMessages(anthropicMessages, systemMessage, usePromptCache, modelInfo, conversationId): object
}
class CacheStrategyConfig {
+modelInfo: ModelInfo
+systemPrompt?: string
+messages: MessageParam[]
+usePromptCache: boolean
+previousCachePointPlacements?: CachePointPlacement[]
}
class ModelInfo {
+maxTokens: number
+contextWindow: number
+supportsPromptCache: boolean
+maxCachePoints: number
+minTokensPerCachePoint: number
+cachableFields: Array<string>
}
class CacheResult {
+system: SystemContentBlock[]
+messages: Message[]
+messageCachePointPlacements?: CachePointPlacement[]
}
class CachePointPlacement {
+index: number
+type: string
+tokensCovered: number
}
CacheStrategy <|-- MultiPointStrategy : extends
CacheStrategy o-- CacheStrategyConfig : uses
CacheStrategyConfig o-- ModelInfo : contains
CacheStrategy ..> CacheResult : produces
CacheStrategy ..> CachePointPlacement : creates
AwsBedrockHandler ..> MultiPointStrategy : creates
AwsBedrockHandler ..> CachePointPlacement : tracks
MultiPointStrategy ..> CachePointPlacement : preserves
```
## Sequence Diagram: Multi-Point Strategy
This diagram illustrates the process flow when using the MultiPointStrategy with multiple cache points in messages.
```mermaid
sequenceDiagram
participant Client as Client Code
participant Bedrock as AwsBedrockHandler
participant Strategy as MultiPointStrategy
participant AWS as Amazon Bedrock Service
Client->>Bedrock: createMessage(systemPrompt, messages)
Note over Bedrock: Generate conversationId to track cache points
Bedrock->>Bedrock: getModel() to get model info
Bedrock->>Bedrock: Check if model supports prompt cache
Bedrock->>Strategy: new MultiPointStrategy(config)
Note over Strategy: config contains modelInfo, systemPrompt, messages, usePromptCache, previousCachePointPlacements
Bedrock->>Strategy: determineOptimalCachePoints()
alt usePromptCache is false or no messages
Strategy->>Strategy: formatWithoutCachePoints()
else
Strategy->>Strategy: Check if system cache is supported
alt supportsSystemCache and systemPrompt exists
Strategy->>Strategy: meetsMinTokenThreshold(systemTokenCount)
alt systemTokenCount >= minTokensPerCachePoint
Strategy->>Strategy: Add cache point after system prompt
Note over Strategy: Decrement remainingCachePoints
end
end
Strategy->>Strategy: determineMessageCachePoints(minTokensPerPoint, remainingCachePoints)
alt previousCachePointPlacements exists
Note over Strategy: Analyze previous placements
Note over Strategy: Preserve N-1 cache points when possible
Note over Strategy: Determine which points to keep or combine
else
loop while currentIndex < messages.length and remainingCachePoints > 0
Strategy->>Strategy: findOptimalPlacementForRange(currentIndex, totalMessages-1, minTokensPerPoint)
alt placement found
Strategy->>Strategy: Add placement to placements array
Strategy->>Strategy: Update currentIndex and decrement remainingCachePoints
end
end
end
Strategy->>Strategy: applyCachePoints(messages, placements)
Strategy->>Strategy: Store cache point placements in result
end
Strategy-->>Bedrock: Return CacheResult with system blocks, messages, and messageCachePointPlacements
Bedrock->>Bedrock: Store cache point placements for conversationId
Bedrock->>AWS: Send request with multiple cache points
AWS-->>Bedrock: Stream response
Bedrock-->>Client: Yield response chunks
```
## Key Concepts
### Cache Strategy
The cache strategy system is designed to optimize the placement of cache points in Amazon Bedrock API requests. Cache points allow the service to reuse previously processed parts of the prompt, reducing token usage and improving response times.
- **MultiPointStrategy**: Upon first MR of Bedrock caching, this strategy is used for all cache point placement scenarios. It distributes cache points throughout the conversation to maximize caching efficiency, whether the model supports one or multiple cache points.
### MultiPointStrategy Placement Logic
- **System Prompt Caching**: If the system prompt is large enough (exceeds minTokensPerCachePoint), a cache point is placed after it.
- **Message Caching**: The strategy uses a simplified approach for placing cache points in messages:
1. For new conversations (no previous cache points):
- It iteratively finds the last user message in each range
- It ensures each placement covers at least the minimum token threshold
- It continues until all available cache points are used or no more valid placements can be found
2. For growing conversations (with previous cache points):
- It preserves previous cache points when possible
- It analyzes the token distribution between existing cache points
- It compares the token count of new messages with the smallest gap between existing cache points
- It only combines cache points if the new messages have more tokens than the smallest gap
A key challenge in cache point placement is maintaining consistency across consecutive messages in a growing conversation. When new messages are added to a conversation, we want to ensure that:
1. Cache points from previous messages are reused as much as possible to maximize cache hits
2. New cache points are placed optimally for the new messages
The simplified approach ensures that:
- Cache points are always placed after user messages, which are natural conversation boundaries
- Each cache point covers at least the minimum token threshold
- N-1 cache points remain in the same location when possible in growing conversations
- Cache points are combined only when it makes sense to do so (when the benefit outweighs the cost)
- New messages receive cache points only when they contain enough tokens to justify the reallocation
The examples in this document reflect this optimized implementation.
### Integration with Amazon Bedrock
The AwsBedrockHandler class integrates with the cache strategies by:
1. Determining if the model supports prompt caching
2. Creating the appropriate strategy based on model capabilities
3. Applying the strategy to format messages with optimal cache points
4. Sending the formatted request to Amazon Bedrock
5. Processing and returning the response
## Usage Considerations
- Cache points are only effective if the same content is reused across multiple requests
- The minimum token threshold ensures cache points are only placed where they provide meaningful benefits
- System prompt caching is prioritized when available, as it's typically static across requests
- Message caching is more complex and depends on conversation structure and token distribution
## Examples: Multi-Point Strategy Cache Point Placement
### Example 1: Initial Cache Point Placement
In this example, we'll demonstrate how the `determineMessageCachePoints` method places cache points in a new conversation.
**Input Configuration:**
```javascript
const config = {
modelInfo: {
maxTokens: 4096,
contextWindow: 200000,
supportsPromptCache: true,
maxCachePoints: 3,
minTokensPerCachePoint: 100,
cachableFields: ["system", "messages"],
},
systemPrompt: "You are a helpful assistant.", // ~10 tokens
messages: [
{ role: "user", content: "Tell me about machine learning." }, // ~50 tokens
{ role: "assistant", content: "Machine learning is a field of study..." }, // ~150 tokens
{ role: "user", content: "What about deep learning?" }, // ~40 tokens
{ role: "assistant", content: "Deep learning is a subset of machine learning..." }, // ~160 tokens
],
usePromptCache: true,
}
```
**Execution Process:**
1. First, the system prompt is evaluated for caching (10 tokens < minTokensPerCachePoint of 100), so no cache point is used there.
2. The `determineMessageCachePoints` method is called with `minTokensPerPoint = 100` and `remainingCachePoints = 3`.
3. Since there are no previous cache point placements, it enters the special case for new conversations.
4. It calls `findOptimalPlacementForRange` with the entire message range.
5. The method finds the last user message in the range (index 2: "What about deep learning?").
6. It calculates the total tokens covered (240) and verifies it exceeds the minimum threshold (100).
7. It adds this placement to the placements array and continues the process for the next range.
8. Since there are no more user messages after this point that would cover enough tokens, no more cache points are placed.
**Output Cache Point Placements:**
```javascript
;[
{
index: 2, // After the second user message (What about deep learning?)
type: "message",
tokensCovered: 240, // ~240 tokens covered (first 3 messages)
},
]
```
**Resulting Message Structure:**
```
[User]: Tell me about machine learning.
[Assistant]: Machine learning is a field of study...
[User]: What about deep learning?
[CACHE POINT]
[Assistant]: Deep learning is a subset of machine learning...
```
**Note**: The algorithm places a cache point after the second user message (the last user message in the range) because it's the optimal placement and the accumulated tokens (240) exceed the minimum threshold (100).
### Example 2: Adding One Exchange with Cache Point Preservation
Now, let's see what happens when we add one more exchange (user-assistant pair) to the conversation and use the cache point preservation logic:
**Updated Input Configuration with Previous Cache Points:**
```javascript
const config = {
// Same modelInfo and systemPrompt as before
messages: [
// Previous 4 messages...
{ role: "user", content: "How do neural networks work?" }, // ~50 tokens
{ role: "assistant", content: "Neural networks are composed of layers of nodes..." }, // ~180 tokens
],
usePromptCache: true,
// Pass the previous cache point placements from Example 1
previousCachePointPlacements: [
{
index: 2, // After the second user message (What about deep learning?)
type: "message",
tokensCovered: 240,
},
],
}
```
**Execution Process for Example 2 with Cache Point Preservation:**
1. The system prompt evaluation remains the same (no cache point used).
2. The algorithm detects that `previousCachePointPlacements` is provided in the config.
3. It analyzes the previous cache point placements and the current message structure.
4. Since we have 3 total cache points available and used 1 in the previous conversation, we can preserve the previous cache point and still have 2 remaining for the new messages.
5. The algorithm preserves the cache point from the previous conversation:
- The cache point at index 2 (after "What about deep learning?")
6. It then calculates the optimal placement for the remaining cache points based on the new messages.
7. Since there are 2 new messages with significant token count (230 tokens), it places a second cache point after the new user message.
**Output Cache Point Placements with Preservation:**
```javascript
;[
{
index: 2, // After the second user message (What about deep learning?) - PRESERVED
type: "message",
tokensCovered: 240, // ~240 tokens covered (first 3 messages)
},
{
index: 4, // After the third user message (How do neural networks work?) - NEW PLACEMENT
type: "message",
tokensCovered: 230, // ~230 tokens covered (messages between cache points)
},
]
```
**Resulting Message Structure with Preservation:**
```
[User]: Tell me about machine learning.
[Assistant]: Machine learning is a field of study...
[User]: What about deep learning?
[CACHE POINT 1] - PRESERVED FROM PREVIOUS
[Assistant]: Deep learning is a subset of machine learning...
[User]: How do neural networks work?
[CACHE POINT 2] - NEW PLACEMENT
[Assistant]: Neural networks are composed of layers of nodes...
```
**Note**: The algorithm preserved the cache point from the previous conversation and placed a new cache point for the new messages. This ensures maximum cache hit rates while still adapting to the growing conversation.
### Example 3: Adding Another Exchange with Cache Point Preservation
Let's add one more exchange to see how the cache strategy continues to adapt:
**Updated Input Configuration with Previous Cache Points:**
```javascript
const config = {
// Same modelInfo and systemPrompt as before
messages: [
// Previous 6 messages...
{ role: "user", content: "Can you explain backpropagation?" }, // ~40 tokens
{ role: "assistant", content: "Backpropagation is an algorithm used to train neural networks..." }, // ~170 tokens
],
usePromptCache: true,
// Pass the previous cache point placements from Example 2
previousCachePointPlacements: [
{
index: 2, // After the second user message (What about deep learning?)
type: "message",
tokensCovered: 240,
},
{
index: 4, // After the third user message (How do neural networks work?)
type: "message",
tokensCovered: 230,
},
],
}
```
**Execution Process for Example 3 with Cache Point Preservation:**
1. The system prompt evaluation remains the same (no cache point used).
2. The algorithm detects that `previousCachePointPlacements` is provided in the config.
3. It analyzes the previous cache point placements and the current message structure.
4. Following the N-1 preservation rule, it decides to keep both previous cache points (at indices 2 and 4) since there are 3 total cache points available.
5. It then calculates the optimal placement for the remaining cache point based on the new messages.
6. Since there are 2 new messages with significant token count (210 tokens), it places a new cache point after the new user message.
**Output Cache Point Placements with Preservation:**
```javascript
;[
{
index: 2, // After the second user message (What about deep learning?) - PRESERVED
type: "message",
tokensCovered: 240, // ~240 tokens covered (first 3 messages)
},
{
index: 4, // After the third user message (How do neural networks work?) - PRESERVED
type: "message",
tokensCovered: 230, // ~230 tokens covered (messages between cache points)
},
{
index: 6, // After the fourth user message (Can you explain backpropagation?) - NEW PLACEMENT
type: "message",
tokensCovered: 210, // ~210 tokens covered (messages between cache points)
},
]
```
**Resulting Message Structure with Preservation:**
```
[User]: Tell me about machine learning.
[Assistant]: Machine learning is a field of study...
[User]: What about deep learning?
[CACHE POINT 1] - PRESERVED FROM PREVIOUS
[Assistant]: Deep learning is a subset of machine learning...
[User]: How do neural networks work?
[CACHE POINT 2] - PRESERVED FROM PREVIOUS
[Assistant]: Neural networks are composed of layers of nodes...
[User]: Can you explain backpropagation?
[CACHE POINT 3] - NEW PLACEMENT
[Assistant]: Backpropagation is an algorithm used to train neural networks...
```
**Note**: The algorithm preserved both cache points from the previous conversation and placed a new cache point for the new messages. This ensures maximum cache hit rates while still adapting to the growing conversation.
### Example 4: Adding Messages (With Token Comparison)
In this example, we'll demonstrate how the algorithm handles the case when new messages have a token count small enough that cache points should not be changed:
**Updated Input Configuration with Previous Cache Points:**
```javascript
const config = {
// Same modelInfo and systemPrompt as before
messages: [
// Previous 10 messages...
{
role: "user",
content: "Can you explain the difference between supervised and unsupervised learning in detail?",
}, // ~80 tokens
{
role: "assistant",
content:
"Certainly! Supervised learning and unsupervised learning are two fundamental paradigms in machine learning with..",
}, // ~130 tokens
],
usePromptCache: true,
// Pass the previous cache point placements from Example 3
previousCachePointPlacements: [
{
index: 2, // After the second user message
type: "message",
tokensCovered: 240,
},
{
index: 6, // After the fourth user message
type: "message",
tokensCovered: 440,
},
{
index: 8, // After the fifth user message
type: "message",
tokensCovered: 260,
},
],
}
```
**Execution Process for Example 4 with Token Comparison:**
1. The algorithm detects that all cache points are used and new messages have been added.
2. It calculates the token count of the new messages (210 tokens).
3. It analyzes the token distribution between existing cache points and finds the smallest gap (260 tokens).
4. It compares the token count of new messages (210) with the smallest gap (260).
5. Since the new messages have less tokens than the smallest gap (210 < 260), it decides not to re-allocate cache points
6. All existing cache points are preserved, and no cache point is allocated for the new messages.
**Output Cache Point Placements (Unchanged):**
```javascript
;[
{
index: 2, // After the second user message - PRESERVED
type: "message",
tokensCovered: 240,
},
{
index: 6, // After the fourth user message - PRESERVED
type: "message",
tokensCovered: 440,
},
{
index: 8, // After the fifth user message - PRESERVED
type: "message",
tokensCovered: 260,
},
]
```
**Resulting Message Structure:**
```
[User]: Tell me about machine learning.
[Assistant]: Machine learning is a field of study...
[User]: What about deep learning?
[CACHE POINT 1] - PRESERVED
[Assistant]: Deep learning is a subset of machine learning...
[User]: How do neural networks work?
[Assistant]: Neural networks are composed of layers of nodes...
[User]: Can you explain backpropagation?
[CACHE POINT 2] - PRESERVED
[Assistant]: Backpropagation is an algorithm used to train neural networks...
[User]: What are some applications of deep learning?
[CACHE POINT 3] - PRESERVED
[Assistant]: Deep learning has many applications including...
[User]: Can you explain the difference between supervised and unsupervised learning in detail?
[Assistant]: Certainly! Supervised learning and unsupervised learning are two fundamental paradigms in machine learning with...
```
**Note**: In this case, the algorithm determined that the new messages are the smallest portion of the message history in comparison to existing cache points. Restructuring the cache points to make room to cache the new messages would be a net negative since it would not make use of 2 previously cached blocks, would have to re-write those 2 as a single cache point, and would write a new small cache point that would be chosen to be merged in the next round of messages.
### Example 5: Adding Messages that reallocate cache points
Now let's see what happens when we add messages with a larger token count:
**Updated Input Configuration with Previous Cache Points:**
```javascript
const config = {
// Same modelInfo and systemPrompt as before
messages: [
// Previous 10 messages...
{
role: "user",
content: "Can you provide a detailed example of implementing a neural network for image classification?",
}, // ~100 tokens
{
role: "assistant",
content:
"Certainly! Here's a detailed example of implementing a convolutional neural network (CNN) for image classification using TensorFlow and Keras...",
}, // ~300 tokens
],
usePromptCache: true,
// Pass the previous cache point placements from Example 3
previousCachePointPlacements: [
{
index: 2, // After the second user message
type: "message",
tokensCovered: 240,
},
{
index: 6, // After the fourth user message
type: "message",
tokensCovered: 440,
},
{
index: 8, // After the fifth user message
type: "message",
tokensCovered: 260,
},
],
}
```
**Execution Process for Example 5 with Token Comparison:**
1. The algorithm detects that all cache points are used and new messages have been added.
2. It calculates the token count of the new messages (400 tokens).
3. It analyzes the token distribution between existing cache points and finds the smallest gap (260 tokens).
4. It calculates the required token threshold by applying a 20% increase to the smallest gap (260 \* 1.2 = 312).
5. It compares the token count of new messages (400) with this threshold (312).
6. Since the new messages have significantly more tokens than the threshold (400 > 312), it decides to combine cache points.
7. It identifies that the cache point at index 8 has the smallest token coverage (260 tokens).
8. It removes this cache point and places a new one after the new user message.
**Output Cache Point Placements with Reallocation:**
```javascript
;[
{
index: 2, // After the second user message - PRESERVED
type: "message",
tokensCovered: 240,
},
{
index: 6, // After the fourth user message - PRESERVED
type: "message",
tokensCovered: 440,
},
{
index: 10, // After the sixth user message - NEW PLACEMENT
type: "message",
tokensCovered: 660, // Tokens from messages 7 through 10 (260 + 400)
},
]
```
**Resulting Message Structure:**
```
[User]: Tell me about machine learning.
[Assistant]: Machine learning is a field of study...
[User]: What about deep learning?
[CACHE POINT 1] - PRESERVED
[Assistant]: Deep learning is a subset of machine learning...
[User]: How do neural networks work?
[Assistant]: Neural networks are composed of layers of nodes...
[User]: Can you explain backpropagation?
[CACHE POINT 2] - PRESERVED
[Assistant]: Backpropagation is an algorithm used to train neural networks...
[User]: What are some applications of deep learning?
[Assistant]: Deep learning has many applications including...
[User]: Can you provide a detailed example of implementing a neural network for image classification?
[CACHE POINT 3] - NEW PLACEMENT
[Assistant]: Certainly! Here's a detailed example of implementing a convolutional neural network (CNN) for image classification using TensorFlow and Keras...
```
**Note**: In this case, the algorithm determined that it would be beneficial to reallocate a cache point for the new messages since they contain more tokens than the smallest gap between existing cache points. This optimization ensures that the most token-heavy parts of the conversation are cached.
**Important**: The `tokensCovered` value for each cache point represents the total number of tokens from the previous cache point (or the beginning of the conversation for the first cache point) up to the current cache point. For example, the cache point at index 10 covers 660 tokens, which includes all tokens from messages 7 through 10 (after the cache point at index 6 up to and including the cache point at index 10).
### Key Observations
1. **Simple Initial Placement Logic**: The last user message in the range that meets the minimum token threshold is set as a cache point.
2. **User Message Boundary Requirement**: Cache points are placed exclusively after user messages, not after assistant messages. This ensures cache points are placed at natural conversation boundaries where the user has provided input.
3. **Token Threshold Enforcement**: Each segment between cache points must meet the minimum token threshold (100 tokens in our examples) to be considered for caching. This is enforced by a guard clause that checks if the total tokens covered by a placement meets the minimum threshold.
4. **Adaptive Placement for Growing Conversations**: As the conversation grows, the strategy adapts by preserving previous cache points when possible and only reallocating them when beneficial.
5. **Token Comparison Optimization with Required Increase**: When all cache points are used and new messages are added, the algorithm compares the token count of new messages with the smallest combined token count of contiguous existing cache points, applying a required percentage increase (20%) to ensure reallocation is worth it. Cache points are only combined if the new messages have significantly more tokens than this threshold, ensuring that reallocation is only done when it results in a substantial net positive effect on caching efficiency.
This adaptive approach ensures that as conversations grow, the caching strategy continues to optimize token usage and response times by strategically placing cache points at the most effective positions, while avoiding inefficient reallocations that could result in a net negative effect on caching performance.

View file

@ -1,445 +0,0 @@
# Bedrock Model Identification
This document explains how model information is identified and managed in the Amazon Bedrock provider implementation (`bedrock.ts`). It focuses on the sequence of operations that determine the `costModelConfig` property, which is crucial for token counting, pricing, and other features.
## Model Identification Flow
The `costModelConfig` property is set through different paths depending on the input configuration and response data from Bedrock. Below is a sequence diagram of how model identification works:
```mermaid
sequenceDiagram
participant Constructor
participant parseArn
participant parseBaseModelId
participant getModelById
participant getModel
Constructor->>parseArn: Initialize (if awsCustomArn provided)
parseArn->>parseBaseModelId: Extract region prefix from modelId
parseBaseModelId-->>parseArn: Return modelId without prefix
parseArn->>parseArn: Determine if cross-region inference
parseArn-->>Constructor: Return arnInfo with modelId and crossRegionInference flag
Constructor->>getModel: Call getModel()
getModel->>getModelById: Lookup model
getModelById-->>getModel: Return model info
getModel-->>Constructor: Return model config
Constructor->>Constructor: Set this.costModelConfig
```
### During Stream Processing (with Prompt Router)
```mermaid
sequenceDiagram
participant createMessage
participant parseArn
participant getModelById
createMessage->>parseArn: Process stream event with invokedModelId
parseArn->>parseArn: Extract modelId
parseArn-->>createMessage: Return invokedModelArn
createMessage->>getModelById: Call getModelById with invokedModelArn.modelId
getModelById-->>createMessage: Return invokedModel
createMessage->>createMessage: Set invokedModel.id = modelConfig.id
createMessage->>createMessage: Set this.costModelConfig = invokedModel
```
## Input Examples and Resulting Values
### Example 1: Standard Model Selection
**Input:**
```javascript
const handler = new AwsBedrockHandler({
apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
awsAccessKey: "ACCESS_KEY",
awsSecretKey: "SECRET_KEY",
awsRegion: "us-east-1",
})
```
**Sequence:**
1. Constructor initializes with options
2. Constructor calls `getModel()`
3. `getModel()` calls `getModelById("anthropic.claude-3-5-sonnet-20241022-v2:0")`
4. `getModelById()` looks up the model in `bedrockModels`
5. `this.costModelConfig` is set to:
```javascript
{
id: "anthropic.claude-3-5-sonnet-20241022-v2:0",
info: {
maxTokens: 4096,
contextWindow: 128000,
inputPrice: 3,
outputPrice: 15,
// other model properties...
}
}
```
### Example 2: Custom ARN for Foundation Model
**Input:**
```javascript
const handler = new AwsBedrockHandler({
apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
awsAccessKey: "ACCESS_KEY",
awsSecretKey: "SECRET_KEY",
awsRegion: "us-east-1",
awsCustomArn: "arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-3-5-sonnet-20241022-v2:0",
})
```
**Sequence:**
1. Constructor initializes with options
2. Constructor calls `parseArn("arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-3-5-sonnet-20241022-v2:0")`
3. `parseArn()` extracts:
```javascript
{
isValid: true,
region: "us-east-1",
modelType: "foundation-model",
modelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
crossRegionInference: false
}
```
4. Constructor sets `this.arnInfo` to the result
5. Constructor calls `getModel()`
6. `getModel()` calls `getModelById("anthropic.claude-3-5-sonnet-20241022-v2:0")`
7. `getModelById()` looks up the model in `bedrockModels`
8. `this.costModelConfig` is set to:
```javascript
{
id: "anthropic.claude-3-5-sonnet-20241022-v2:0", // Note: ID is not the ARN since it's a foundation-model
info: {
maxTokens: 4096,
contextWindow: 128000,
inputPrice: 3,
outputPrice: 15,
// other model properties...
}
}
```
### Example 3: Custom ARN for Prompt Router
**Input:**
```javascript
const handler = new AwsBedrockHandler({
apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
awsAccessKey: "ACCESS_KEY",
awsSecretKey: "SECRET_KEY",
awsRegion: "us-west-2",
awsCustomArn: "arn:aws:bedrock:us-west-2:123456789012:prompt-router/my-router",
})
```
**Sequence:**
1. Constructor initializes with options
2. Constructor calls `parseArn("arn:aws:bedrock:us-west-2:123456789012:prompt-router/my-router")`
3. `parseArn()` extracts:
```javascript
{
isValid: true,
region: "us-west-2",
modelType: "prompt-router",
modelId: "my-router",
crossRegionInference: false
}
```
4. Constructor sets `this.arnInfo` to the result
5. Constructor calls `getModel()`
6. `getModel()` calls `getModelById("my-router")`
7. `getModelById()` doesn't find "my-router" in `bedrockModels`, returns default model info
8. Since `this.arnInfo.modelType` is "prompt-router" (not "foundation-model"), `getModel()` sets the ID to the full ARN
9. `this.costModelConfig` is set to:
```javascript
{
id: "arn:aws:bedrock:us-west-2:123456789012:prompt-router/my-router", // Full ARN as ID
info: {
// Default model info for prompt routers
maxTokens: 4096,
contextWindow: 128000,
inputPrice: 3,
outputPrice: 15,
// other model properties...
}
}
```
### Example 4: Cross-Region Inference
**Input:**
```javascript
const handler = new AwsBedrockHandler({
apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
awsAccessKey: "ACCESS_KEY",
awsSecretKey: "SECRET_KEY",
awsRegion: "eu-west-1",
awsUseCrossRegionInference: true,
})
```
**Sequence:**
1. Constructor initializes with options
2. Constructor calls `getModel()`
3. `getModel()` calls `getModelById("anthropic.claude-3-5-sonnet-20241022-v2:0")`
4. `getModelById()` looks up the model in `bedrockModels`
5. Since `awsUseCrossRegionInference` is true, `getModel()` gets the prefix for "eu-west-1" (which is "eu.")
6. `getModel()` prepends "eu." to the model ID
7. `this.costModelConfig` is set to:
```javascript
{
id: "eu.anthropic.claude-3-5-sonnet-20241022-v2:0", // Note the "eu." prefix
info: {
maxTokens: 4096,
contextWindow: 128000,
inputPrice: 3,
outputPrice: 15,
// other model properties...
}
}
```
### Example 5: Prompt Router with invokedModelId in Stream
**Initial Input:**
```javascript
const handler = new AwsBedrockHandler({
awsAccessKey: "ACCESS_KEY",
awsSecretKey: "SECRET_KEY",
awsRegion: "us-west-2",
awsCustomArn: "arn:aws:bedrock:us-west-2:123456789012:prompt-router/my-router",
})
```
**Initial Sequence (same as Example 3):**
1. `this.costModelConfig` is initially set to:
```javascript
{
id: "arn:aws:bedrock:us-west-2:123456789012:prompt-router/my-router",
info: {
// Default model info for prompt routers
maxTokens: 4096,
contextWindow: 128000,
inputPrice: 3,
outputPrice: 15,
// other properties...
}
}
```
**Stream Event with invokedModelId:**
```javascript
{
trace: {
promptRouter: {
invokedModelId: "arn:aws:bedrock:us-west-2:123456789012:inference-profile/anthropic.claude-3-5-sonnet-20241022-v2:0",
usage: {
inputTokens: 150,
outputTokens: 250
}
}
}
}
```
**Stream Processing Sequence:**
1. `createMessage()` encounters the stream event with `invokedModelId`
2. It calls `parseArn("arn:aws:bedrock:us-west-2:123456789012:inference-profile/anthropic.claude-3-5-sonnet-20241022-v2:0")`
3. `parseArn()` extracts:
```javascript
{
isValid: true,
region: "us-west-2",
modelType: "inference-profile",
modelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
crossRegionInference: false
}
```
4. `createMessage()` calls `getModelById("anthropic.claude-3-5-sonnet-20241022-v2:0")`
5. `getModelById()` looks up the model in `bedrockModels` and returns the model info
6. `createMessage()` sets `invokedModel.id` to the original router ID
7. `this.costModelConfig` is updated to:
```javascript
{
id: "arn:aws:bedrock:us-west-2:123456789012:prompt-router/my-router", // Keeps router ID
info: {
// Claude 3.5 Sonnet model info
maxTokens: 4096,
contextWindow: 128000,
inputPrice: 3,
outputPrice: 15,
// other Claude-specific properties...
}
}
```
This ensures that:
1. Subsequent requests continue to use the prompt router
2. Token counting and pricing use the actual model's rates
3. Context window and other model-specific properties are correctly set
### Example 6: Cross-Region ARN with Region Prefix
**Input:**
```javascript
const handler = new AwsBedrockHandler({
apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
awsAccessKey: "ACCESS_KEY",
awsSecretKey: "SECRET_KEY",
awsRegion: "us-east-1",
awsCustomArn:
"arn:aws:bedrock:us-west-2:123456789012:inference-profile/us.anthropic.claude-3-5-sonnet-20241022-v2:0",
})
```
**Sequence:**
1. Constructor initializes with options
2. Constructor calls `parseArn("arn:aws:bedrock:us-west-2:123456789012:inference-profile/us.anthropic.claude-3-5-sonnet-20241022-v2:0")`
3. `parseArn()` extracts region and calls `parseBaseModelId("us.anthropic.claude-3-5-sonnet-20241022-v2:0")`
4. `parseBaseModelId()` recognizes "us." as a region prefix and removes it
5. `parseArn()` returns:
```javascript
{
isValid: true,
region: "us-west-2",
modelType: "inference-profile",
modelId: "anthropic.claude-3-5-sonnet-20241022-v2:0", // Note: prefix removed
crossRegionInference: true // Detected cross-region
}
```
6. Constructor sets `this.arnInfo` to the result and updates `this.options.awsRegion` to "us-west-2"
7. Constructor calls `getModel()`
8. `getModel()` calls `getModelById("anthropic.claude-3-5-sonnet-20241022-v2:0")`
9. `getModelById()` looks up the model in `bedrockModels`
10. Since `this.arnInfo.modelType` is "inference-profile" (not "foundation-model"), `getModel()` sets the ID to the full ARN
11. `this.costModelConfig` is set to:
```javascript
{
id: "arn:aws:bedrock:us-west-2:123456789012:inference-profile/us.anthropic.claude-3-5-sonnet-20241022-v2:0", // Full ARN
info: {
maxTokens: 4096,
contextWindow: 128000,
inputPrice: 3,
outputPrice: 15,
// other model properties...
}
}
```
### Example 7: Single-Region ARN with Region Prefix (apne3)
**Input:**
```javascript
const handler = new AwsBedrockHandler({
apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
awsAccessKey: "ACCESS_KEY",
awsSecretKey: "SECRET_KEY",
awsRegion: "ap-northeast-3", // Osaka region
awsCustomArn:
"arn:aws:bedrock:ap-northeast-3:123456789012:inference-profile/apne3.anthropic.claude-3-5-sonnet-20241022-v2:0",
})
```
**Sequence:**
1. Constructor initializes with options
2. Constructor calls `parseArn("arn:aws:bedrock:ap-northeast-3:123456789012:inference-profile/apne3.anthropic.claude-3-5-sonnet-20241022-v2:0")`
3. `parseArn()` extracts region and calls `parseBaseModelId("apne3.anthropic.claude-3-5-sonnet-20241022-v2:0")`
4. `parseBaseModelId()` recognizes "apne3." as a region prefix and removes it
5. `parseArn()` returns:
```javascript
{
isValid: true,
region: "ap-northeast-3",
modelType: "inference-profile",
modelId: "anthropic.claude-3-5-sonnet-20241022-v2:0", // Note: prefix removed
crossRegionInference: false // Not a cross-region prefix since apne3 maps to a single region
}
```
6. Constructor sets `this.arnInfo` to the result
7. Constructor calls `getModel()`
8. `getModel()` calls `getModelById("anthropic.claude-3-5-sonnet-20241022-v2:0")`
9. `getModelById()` looks up the model in `bedrockModels`
10. Since `this.arnInfo.modelType` is "inference-profile" (not "foundation-model"), `getModel()` sets the ID to the full ARN
11. `this.costModelConfig` is set to:
```javascript
{
id: "arn:aws:bedrock:ap-northeast-3:123456789012:inference-profile/apne3.anthropic.claude-3-5-sonnet-20241022-v2:0", // Full ARN
info: {
maxTokens: 4096,
contextWindow: 128000,
inputPrice: 3,
outputPrice: 15,
// other model properties...
}
}
```
## Region Prefixes
The system recognizes these region prefixes for cross-region inference:
| Prefix | Region ID | Description | Multi-Region |
| -------- | ---------------- | ------------------------- | ------------ |
| "us." | "us-east-1" | US East (N. Virginia) | Yes |
| "use1." | "us-east-1" | US East (N. Virginia) | No |
| "use2." | "us-east-2" | US East (Ohio) | No |
| "usw2." | "us-west-2" | US West (Oregon) | No |
| "eu." | "eu-west-1" | Europe (Ireland) | Yes |
| "euw1." | "eu-west-1" | Europe (Ireland) | No |
| "ap." | "ap-southeast-1" | Asia Pacific (Singapore) | Yes |
| "apne1." | "ap-northeast-1" | Asia Pacific (Tokyo) | No |
| "apne3." | "ap-northeast-3" | Asia Pacific (Osaka) | No |
| "ca." | "ca-central-1" | Canada (Central) | Yes |
| "sa." | "sa-east-1" | South America (São Paulo) | Yes |
| "apac." | "ap-southeast-1" | Default APAC region | Yes |
| "emea." | "eu-west-1" | Default EMEA region | Yes |
| "amer." | "us-east-1" | Default Americas region | Yes |
These prefixes are used to:
1. Identify and strip region prefixes from model IDs in `parseBaseModelId()`
2. Add appropriate region prefixes when cross-region inference is enabled in `getModel()`
**Note on Multi-Region Prefixes:**
- Prefixes marked as "Multi-Region" (like "us.", "eu.", "ap.", etc.) set the `crossRegionInference` flag to `true` when detected in an ARN
- These prefixes typically represent a geographic area with multiple AWS regions
- Single-region prefixes (like "apne3.", "use1.", etc.) set the `crossRegionInference` flag to `false`
- The `crossRegionInference` flag affects how the system handles region-specific model configurations
## Summary
The Bedrock provider's model identification system follows these key principles:
1. **ARN Parsing**: Extracts model ID, region, and resource type from ARNs
2. **Model Lookup**: Uses the extracted model ID to find model information
3. **ID Preservation**:
- For foundation models: Uses the model ID directly
- For other resources (prompt routers, inference profiles): Uses the full ARN as the ID
4. **Cross-Region Handling**: Adds or removes region prefixes as needed
5. **Dynamic Updates**: Updates model information when a prompt router provides an invokedModelId
This system ensures that:
- The correct model ID is used for API requests
- Accurate model information is used for token counting and pricing
- Cross-region inference works correctly
- Prompt routers can dynamically select models while maintaining proper tracking

View file

@ -1,443 +0,0 @@
## For All Settings
1. Add the setting to schema definitions:
- Add the item to `globalSettingsSchema` in `src/schemas/index.ts`
- Add the item to `globalSettingsRecord` in `src/schemas/index.ts`
- Example: `terminalCommandDelay: z.number().optional(),`
2. Add the setting to type definitions:
- Add the item to `src/exports/types.ts`
- Add the item to `src/exports/roo-code.d.ts`
- Add the setting to `src/shared/ExtensionMessage.ts`
- Add the setting to the WebviewMessage type in `src/shared/WebviewMessage.ts`
- Example: `terminalCommandDelay?: number | undefined`
3. Add test coverage:
- Add the setting to mockState in src/core/webview/**tests**/ClineProvider.test.ts
- Add test cases for setting persistence and state updates
- Ensure all tests pass before submitting changes
## For Checkbox Settings
1. Add the message type to src/shared/WebviewMessage.ts:
- Add the setting name to the WebviewMessage type's type union
- Example: `| "multisearchDiffEnabled"`
2. Add the setting to webview-ui/src/context/ExtensionStateContext.tsx:
- Add the setting to the ExtensionStateContextType interface
- Add the setter function to the interface
- Add the setting to the initial state in useState
- Add the setting to the contextValue object
- Example:
```typescript
interface ExtensionStateContextType {
multisearchDiffEnabled: boolean
setMultisearchDiffEnabled: (value: boolean) => void
}
```
3. Add the setting to src/core/webview/ClineProvider.ts:
- Add the setting name to the GlobalStateKey type union
- Add the setting to the Promise.all array in getState
- Add the setting to the return value in getState with a default value
- Add the setting to the destructured variables in getStateToPostToWebview
- Add the setting to the return value in getStateToPostToWebview
- Add a case in setWebviewMessageListener to handle the setting's message type
- Example:
```typescript
case "multisearchDiffEnabled":
await this.updateGlobalState("multisearchDiffEnabled", message.bool)
await this.postStateToWebview()
break
```
4. Add the checkbox UI to webview-ui/src/components/settings/SettingsView.tsx:
- Import the setting and its setter from ExtensionStateContext
- Add the VSCodeCheckbox component with the setting's state and onChange handler
- Add appropriate labels and description text
- Example:
```typescript
<VSCodeCheckbox
checked={multisearchDiffEnabled}
onChange={(e: any) => setMultisearchDiffEnabled(e.target.checked)}
>
<span style={{ fontWeight: "500" }}>Enable multi-search diff matching</span>
</VSCodeCheckbox>
```
5. Add the setting to handleSubmit in webview-ui/src/components/settings/SettingsView.tsx:
- Add a vscode.postMessage call to send the setting's value when clicking Save
- This step is critical for persistence - without it, the setting will not be saved when the user clicks Save
- Example:
```typescript
vscode.postMessage({ type: "multisearchDiffEnabled", bool: multisearchDiffEnabled })
```
6. Style Considerations:
- Use the VSCodeCheckbox component from @vscode/webview-ui-toolkit/react instead of HTML input elements
- Wrap each checkbox in a div element for proper spacing
- Use a span with className="font-medium" for the checkbox label inside the VSCodeCheckbox component
- Place the description in a separate div with className="text-vscode-descriptionForeground text-sm mt-1"
- Maintain consistent spacing between configuration options
- Example:
```typescript
<div>
<VSCodeCheckbox
checked={terminalPowershellCounter ?? true}
onChange={(e: any) => setCachedStateField("terminalPowershellCounter", e.target.checked)}
data-testid="terminal-powershell-counter-checkbox">
<span className="font-medium">{t("settings:terminal.powershellCounter.label")}</span>
</VSCodeCheckbox>
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:terminal.powershellCounter.description")}
</div>
</div>
```
## For Select/Dropdown Settings
1. Add the message type to src/shared/WebviewMessage.ts:
- Add the setting name to the WebviewMessage type's type union
- Example: `| "preferredLanguage"`
2. Add the setting to webview-ui/src/context/ExtensionStateContext.tsx:
- Add the setting to the ExtensionStateContextType interface
- Add the setter function to the interface
- Add the setting to the initial state in useState with a default value
- Add the setting to the contextValue object
- Example:
```typescript
interface ExtensionStateContextType {
preferredLanguage: string
setPreferredLanguage: (value: string) => void
}
```
3. Add the setting to src/core/webview/ClineProvider.ts:
- Add the setting name to the GlobalStateKey type union
- Add the setting to the Promise.all array in getState
- Add the setting to the return value in getState with a default value
- Add the setting to the destructured variables in getStateToPostToWebview
- Add the setting to the return value in getStateToPostToWebview
- This step is critical for UI display - without it, the setting will not be displayed in the UI
- Add a case in setWebviewMessageListener to handle the setting's message type
- Example:
```typescript
case "preferredLanguage":
await this.updateGlobalState("preferredLanguage", message.text)
await this.postStateToWebview()
break
```
4. Add the select UI to webview-ui/src/components/settings/SettingsView.tsx:
- Import the setting and its setter from ExtensionStateContext
- Add the select element with appropriate styling to match VSCode's theme
- Add options for the dropdown
- Add appropriate labels and description text
- Example:
```typescript
<select
value={preferredLanguage}
onChange={(e) => setPreferredLanguage(e.target.value)}
style={{
width: "100%",
padding: "4px 8px",
backgroundColor: "var(--vscode-input-background)",
color: "var(--vscode-input-foreground)",
border: "1px solid var(--vscode-input-border)",
borderRadius: "2px"
}}>
<option value="English">English</option>
<option value="Spanish">Spanish</option>
...
</select>
```
5. Add the setting to handleSubmit in webview-ui/src/components/settings/SettingsView.tsx:
- Add a vscode.postMessage call to send the setting's value when clicking Done
- Example:
```typescript
vscode.postMessage({ type: "preferredLanguage", text: preferredLanguage })
```
These steps ensure that:
- The setting's state is properly typed throughout the application
- The setting persists between sessions
- The setting's value is properly synchronized between the webview and extension
- The setting has a proper UI representation in the settings view
- Test coverage is maintained for the new setting
## Adding a New Configuration Item: Summary of Required Changes
To add a new configuration item to the system, the following changes are necessary:
1. **Feature-Specific Class** (if applicable)
- For settings that affect specific features (e.g., Terminal, Browser, etc.)
- Add a static property to store the value
- Add getter/setter methods to access and modify the value
2. **Schema Definition**
- Add the item to globalSettingsSchema in src/schemas/index.ts
- Add the item to globalSettingsRecord in src/schemas/index.ts
3. **Type Definitions**
- Add the item to src/exports/types.ts
- Add the item to src/exports/roo-code.d.ts
- Add the item to src/shared/ExtensionMessage.ts
- Add the item to src/shared/WebviewMessage.ts
4. **UI Component**
- Create or update a component in webview-ui/src/components/settings/
- Add appropriate slider/input controls with min/max/step values
- Ensure the props are passed correctly to the component in webview-ui/src/components/settings/SettingsView.tsx
- Update the component's props interface to include the new settings
5. **Translations**
- Add label and description in webview-ui/src/i18n/locales/en/settings.json
- Update all other languages
- If any language content is changed, synchronize all other languages with that change
- Translations must be performed within "translation" mode so change modes for that purpose
6. **State Management**
- Add the item to the destructuring in SettingsView.tsx
- Add the item to the handleSubmit function in webview-ui/src/components/settings/SettingsView.tsx
- Add the item to getStateToPostToWebview in src/core/webview/ClineProvider.ts
- Add the item to getState in src/core/webview/ClineProvider.ts with appropriate default values
- Add the item to the initialization in resolveWebviewView in src/core/webview/ClineProvider.ts
7. **Message Handling**
- Add a case for the item in src/core/webview/webviewMessageHandler.ts
8. **Implementation-Specific Logic**
- Implement any feature-specific behavior triggered by the setting
- Examples:
- Environment variables for terminal settings
- API configuration changes for provider settings
- UI behavior modifications for display settings
9. **Testing**
- Add test cases for the new settings in appropriate test files
- Verify settings persistence and state updates
10. **Ensuring Settings Persistence Across Reload**
To ensure settings persist across application reload, several key components must be properly configured:
1. **Initial State in ExtensionStateContextProvider**:
- Add the setting to the initial state in the useState call
- Example:
```typescript
const [state, setState] = useState<ExtensionState>({
// existing settings...
newSetting: false, // Default value for the new setting
})
```
2. **State Loading in ClineProvider**:
- Add the setting to the getState method to load it from storage
- Example:
```typescript
return {
// existing settings...
newSetting: stateValues.newSetting ?? false,
}
```
3. **State Initialization in resolveWebviewView**:
- Add the setting to the initialization in resolveWebviewView
- Example:
```typescript
this.getState().then(
({
// existing settings...
newSetting,
}) => {
// Initialize the setting with its stored value or default
FeatureClass.setNewSetting(newSetting ?? false)
},
)
```
4. **State Transmission to Webview**:
- Add the setting to the getStateToPostToWebview method
- Example:
```typescript
return {
// existing settings...
newSetting: newSetting ?? false,
}
```
5. **Setter Method in ExtensionStateContext**:
- Add the setter method to the contextValue object
- Example:
```typescript
const contextValue: ExtensionStateContextType = {
// existing properties and methods...
setNewSetting: (value) => setState((prevState) => ({ ...prevState, newSetting: value })),
}
```
11. **Debugging Settings Persistence Issues**
If a setting is not persisting across reload, check the following:
1. **Complete Chain of Persistence**:
- Verify that the setting is added to all required locations:
- globalSettingsSchema and globalSettingsRecord in src/schemas/index.ts
- Initial state in ExtensionStateContextProvider
- getState method in src/core/webview/ClineProvider.ts
- getStateToPostToWebview method in src/core/webview/ClineProvider.ts
- resolveWebviewView method in src/core/webview/ClineProvider.ts (if feature-specific)
- A break in any part of this chain can prevent persistence
2. **Default Values Consistency**:
- Ensure default values are consistent across all locations
- Inconsistent defaults can cause unexpected behavior
3. **Message Handling**:
- Confirm the src/core/webview/webviewMessageHandler.ts has a case for the setting
- Verify the message type matches what's sent from the UI
4. **UI Integration**:
- Check that the setting is included in the handleSubmit function in webview-ui/src/components/settings/SettingsView.tsx
- Ensure the UI component correctly updates the state
5. **Type Definitions**:
- Verify the setting is properly typed in all relevant interfaces
- Check for typos in property names across different files
6. **Storage Mechanism**:
- For complex settings, ensure proper serialization/deserialization
- Check that the setting is being correctly stored in VSCode's globalState
These checks help identify and resolve common issues with settings persistence.
12. **Advanced Troubleshooting: The Complete Settings Persistence Chain**
Settings persistence requires a complete chain of state management across multiple components. Understanding this chain is critical for both humans and AI to effectively troubleshoot persistence issues:
1. **Schema Definition (Entry Point)**:
- Settings must be properly defined in `globalSettingsSchema` and `globalSettingsRecord`
- Enum values should use proper zod schemas: `z.enum(["value1", "value2"])`
- Example:
```typescript
// In src/schemas/index.ts
export const globalSettingsSchema = z.object({
// Existing settings...
commandRiskLevel: z.enum(["readOnly", "reversibleChanges", "complexChanges"]).optional(),
})
const globalSettingsRecord: GlobalSettingsRecord = {
// Existing settings...
commandRiskLevel: undefined,
}
```
2. **UI Component (User Interaction)**:
- Must use consistent components (Select vs. select) with other similar settings
- Must use `setCachedStateField` for state updates, not direct state setting
- Must generate the correct message type through `vscode.postMessage`
- Example:
```tsx
// In a settings component
<Select value={commandRiskLevel} onValueChange={(value) => setCachedStateField("commandRiskLevel", value)}>
<SelectTrigger className="w-full">
<SelectValue placeholder={t("settings:common.select")} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="readOnly">{t("label.readOnly")}</SelectItem>
{/* Other options... */}
</SelectGroup>
</SelectContent>
</Select>
```
3. **Message Handler (State Saving)**:
- Must use correct message type in `src/core/webview/webviewMessageHandler.ts`
- Must use `updateGlobalState` with properly typed values
- Must call `postStateToWebview` after updates
- Example:
```typescript
// In src/core/webview/webviewMessageHandler.ts
case "commandRiskLevel":
await updateGlobalState(
"commandRiskLevel",
(message.text ?? "readOnly") as "readOnly" | "reversibleChanges" | "complexChanges"
)
await provider.postStateToWebview()
break
```
4. **State Retrieval (Reading State)**:
- In `getState`, state must be properly retrieved from stateValues
- In `getStateToPostToWebview`, the setting must be in the destructured parameters
- The setting must be included in the return value
- Use `contextProxy.getGlobalState` for direct access when needed
- Example:
```typescript
// In src/core/webview/ClineProvider.ts getStateToPostToWebview
const {
// Other state properties...
commandRiskLevel,
} = await this.getState()
return {
// Other state properties...
commandRiskLevel: commandRiskLevel ?? "readOnly",
}
```
5. **Debugging Strategies**:
- **Follow the State Flow**: Watch the setting's value at each step in the chain
- **Type Safety**: Ensure the same type is used throughout the chain
- **Component Consistency**: Use the same pattern as other working settings
- **Check Return Values**: Ensure the setting is included in all return objects
- **State vs. Configuration**: Understand when to use state vs. VSCode configuration
6. **Common Pitfalls**:
- **Type Mismatch**: Using string where an enum is expected
- **Chain Breaks**: Missing the setting in return objects
- **UI Inconsistency**: Using different component patterns
- **DefaultValue Issues**: Inconsistent default values across components
- **Missing Schema**: Not adding to schema or record definitions
Remember: A break at ANY point in this chain can cause persistence failures. When troubleshooting, systematically check each link in the chain to identify where the issue occurs.

23
e2e/.eslintrc.json Normal file
View file

@ -0,0 +1,23 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module"
},
"plugins": ["@typescript-eslint"],
"rules": {
"@typescript-eslint/naming-convention": [
"warn",
{
"selector": "import",
"format": ["camelCase", "PascalCase"]
}
],
"@typescript-eslint/semi": "off",
"eqeqeq": "warn",
"no-throw-literal": "warn",
"semi": "off"
},
"ignorePatterns": ["out"]
}

View file

@ -1,162 +0,0 @@
# VSCode Integration Tests
This document describes the integration test setup for the Roo Code VSCode extension.
## Overview
The integration tests use the `@vscode/test-electron` package to run tests in a real VSCode environment. These tests verify that the extension works correctly within VSCode, including features like mode switching, webview interactions, and API communication.
## Test Setup
### Directory Structure
```
e2e/src/
├── runTest.ts # Main test runner
├── suite/
│ ├── index.ts # Test suite configuration
│ ├── modes.test.ts # Mode switching tests
│ ├── tasks.test.ts # Task execution tests
│ └── extension.test.ts # Extension activation tests
```
### Test Runner Configuration
The test runner (`runTest.ts`) is responsible for:
- Setting up the extension development path
- Configuring the test environment
- Running the integration tests using `@vscode/test-electron`
### Environment Setup
1. Create a `.env.local` file in the root directory with required environment variables:
```
OPENROUTER_API_KEY=sk-or-v1-...
```
2. The test suite (`suite/index.ts`) configures:
- Mocha test framework with TDD interface
- 10-minute timeout for LLM communication
- Global extension API access
- WebView panel setup
- OpenRouter API configuration
## Test Suite Structure
Tests are organized using Mocha's TDD interface (`suite` and `test` functions). The main test files are:
- `modes.test.ts`: Tests mode switching functionality
- `tasks.test.ts`: Tests task execution
- `extension.test.ts`: Tests extension activation
### Global Objects
The following global objects are available in tests:
```typescript
declare global {
var api: RooCodeAPI
var provider: ClineProvider
var extension: vscode.Extension<RooCodeAPI>
var panel: vscode.WebviewPanel
}
```
## Running Tests
1. Ensure you have the required environment variables set in `.env.local`
2. Run the integration tests:
```bash
npm run test:integration
```
3. If you want to run a specific test, you can use the `test.only` function in the test file. This will run only the test you specify and ignore the others. Be sure to remove the `test.only` function before committing your changes.
The tests will:
- Download and launch a clean VSCode instance
- Install the extension
- Execute the test suite
- Report results
## Writing New Tests
When writing new integration tests:
1. Create a new test file in `src/test/suite/` with the `.test.ts` extension
2. Structure your tests using the TDD interface:
```typescript
import * as assert from "assert"
import * as vscode from "vscode"
suite("Your Test Suite Name", () => {
test("Should do something specific", async function () {
// Your test code here
})
})
```
3. Use the global objects (`api`, `provider`, `extension`, `panel`) to interact with the extension
### Best Practices
1. **Timeouts**: Use appropriate timeouts for async operations:
```typescript
const timeout = 30000
const interval = 1000
```
2. **State Management**: Reset extension state before/after tests:
```typescript
await globalThis.api.setConfiguration({
mode: "Ask",
alwaysAllowModeSwitch: true,
})
```
3. **Assertions**: Use clear assertions with meaningful messages:
```typescript
assert.ok(condition, "Descriptive message about what failed")
```
4. **Error Handling**: Wrap test code in try/catch blocks and clean up resources:
```typescript
try {
// Test code
} finally {
// Cleanup code
}
```
5. **Wait for Operations**: Use polling when waiting for async operations:
```typescript
let startTime = Date.now()
while (Date.now() - startTime < timeout) {
if (condition) {
break
}
await new Promise((resolve) => setTimeout(resolve, interval))
}
```
6. **Grading**: When grading tests, use the `Grade:` format to ensure the test is graded correctly (See modes.test.ts for an example).
```typescript
await globalThis.api.startNewTask({
text: `Given this prompt: ${testPrompt} grade the response from 1 to 10 in the format of "Grade: (1-10)": ${output} \n Be sure to say 'I AM DONE GRADING' after the task is complete`,
})
```

2370
e2e/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,21 +1,24 @@
{
"name": "e2e",
"version": "0.1.0",
"name": "@roo-code/vscode-e2e",
"private": true,
"scripts": {
"lint": "eslint src/**/*.ts --max-warnings=0",
"check-types": "tsc --noEmit",
"test": "npm run build && npx dotenvx run -f .env.local -- node ./out/runTest.js",
"ci": "npm run vscode-test && npm run test",
"build": "rimraf out && tsc -p tsconfig.json",
"vscode-test": "cd .. && npm run vscode-test"
"format": "prettier --write src",
"test:ci": "pnpm --filter roo-cline build:development && pnpm test:run",
"test:run": "rimraf out && tsc -p tsconfig.json && npx dotenvx run -f .env.local -- node ./out/runTest.js",
"clean": "rimraf out .turbo"
},
"devDependencies": {
"@roo-code/types": "^1.12.0",
"@types/mocha": "^10.0.10",
"@types/node": "^22.14.1",
"@types/vscode": "^1.95.0",
"@vscode/test-cli": "^0.0.10",
"@vscode/test-electron": "^2.4.0",
"glob": "^11.0.1",
"mocha": "^11.1.0",
"rimraf": "^6.0.1",
"typescript": "5.8.3"
}
}

View file

@ -6,7 +6,7 @@ async function main() {
try {
// The folder containing the Extension Manifest package.json
// Passed to `--extensionDevelopmentPath`
const extensionDevelopmentPath = path.resolve(__dirname, "../../")
const extensionDevelopmentPath = path.resolve(__dirname, "../../src")
// The path to the extension test script
// Passed to --extensionTestsPath

View file

@ -1,228 +0,0 @@
const esbuild = require("esbuild")
const fs = require("fs")
const path = require("path")
const production = process.argv.includes("--production")
const watch = process.argv.includes("--watch")
/**
* @type {import('esbuild').Plugin}
*/
const esbuildProblemMatcherPlugin = {
name: "esbuild-problem-matcher",
setup(build) {
build.onStart(() => {
console.log("[watch] build started")
})
build.onEnd((result) => {
result.errors.forEach(({ text, location }) => {
console.error(`✘ [ERROR] ${text}`)
console.error(` ${location.file}:${location.line}:${location.column}:`)
})
console.log("[watch] build finished")
})
},
}
const copyWasmFiles = {
name: "copy-wasm-files",
setup(build) {
build.onEnd(() => {
const nodeModulesDir = path.join(__dirname, "node_modules")
const distDir = path.join(__dirname, "dist")
// tiktoken WASM file
fs.copyFileSync(
path.join(nodeModulesDir, "tiktoken", "lite", "tiktoken_bg.wasm"),
path.join(distDir, "tiktoken_bg.wasm"),
)
// Also copy to the workers directory
fs.mkdirSync(path.join(distDir, "workers"), { recursive: true })
fs.copyFileSync(
path.join(nodeModulesDir, "tiktoken", "lite", "tiktoken_bg.wasm"),
path.join(distDir, "workers", "tiktoken_bg.wasm"),
)
// Main tree-sitter WASM file
fs.copyFileSync(
path.join(nodeModulesDir, "web-tree-sitter", "tree-sitter.wasm"),
path.join(distDir, "tree-sitter.wasm"),
)
// Copy language-specific WASM files
const languageWasmDir = path.join(__dirname, "node_modules", "tree-sitter-wasms", "out")
// Dynamically read all WASM files from the directory instead of using a hardcoded list
if (fs.existsSync(languageWasmDir)) {
const wasmFiles = fs.readdirSync(languageWasmDir).filter((file) => file.endsWith(".wasm"))
console.log(`Copying ${wasmFiles.length} tree-sitter WASM files to dist directory`)
wasmFiles.forEach((filename) => {
fs.copyFileSync(path.join(languageWasmDir, filename), path.join(distDir, filename))
})
} else {
console.warn(`Tree-sitter WASM directory not found: ${languageWasmDir}`)
}
})
},
}
// Simple function to copy locale files
function copyLocaleFiles() {
const srcDir = path.join(__dirname, "src", "i18n", "locales")
const destDir = path.join(__dirname, "dist", "i18n", "locales")
const outDir = path.join(__dirname, "out", "i18n", "locales")
// Ensure source directory exists before proceeding
if (!fs.existsSync(srcDir)) {
console.warn(`Source locales directory does not exist: ${srcDir}`)
return // Exit early if source directory doesn't exist
}
// Create destination directories
fs.mkdirSync(destDir, { recursive: true })
try {
fs.mkdirSync(outDir, { recursive: true })
} catch (e) {}
// Function to copy directory recursively
function copyDir(src, dest) {
const entries = fs.readdirSync(src, { withFileTypes: true })
for (const entry of entries) {
const srcPath = path.join(src, entry.name)
const destPath = path.join(dest, entry.name)
if (entry.isDirectory()) {
// Create directory and copy contents
fs.mkdirSync(destPath, { recursive: true })
copyDir(srcPath, destPath)
} else {
// Copy the file
fs.copyFileSync(srcPath, destPath)
}
}
}
// Copy files to dist directory
copyDir(srcDir, destDir)
console.log("Copied locale files to dist/i18n/locales")
// Copy to out directory for debugging
try {
copyDir(srcDir, outDir)
console.log("Copied locale files to out/i18n/locales")
} catch (e) {
console.warn("Could not copy to out directory:", e.message)
}
}
// Set up file watcher if in watch mode
function setupLocaleWatcher() {
if (!watch) return
const localesDir = path.join(__dirname, "src", "i18n", "locales")
// Ensure the locales directory exists before setting up watcher
if (!fs.existsSync(localesDir)) {
console.warn(`Cannot set up watcher: Source locales directory does not exist: ${localesDir}`)
return
}
console.log(`Setting up watcher for locale files in ${localesDir}`)
// Use a debounce mechanism
let debounceTimer = null
const debouncedCopy = () => {
if (debounceTimer) clearTimeout(debounceTimer)
debounceTimer = setTimeout(() => {
console.log("Locale files changed, copying...")
copyLocaleFiles()
}, 300) // Wait 300ms after last change before copying
}
// Watch the locales directory
try {
fs.watch(localesDir, { recursive: true }, (eventType, filename) => {
if (filename && filename.endsWith(".json")) {
console.log(`Locale file ${filename} changed, triggering copy...`)
debouncedCopy()
}
})
console.log("Watcher for locale files is set up")
} catch (error) {
console.error(`Error setting up watcher for ${localesDir}:`, error.message)
}
}
const copyLocalesFiles = {
name: "copy-locales-files",
setup(build) {
build.onEnd(() => {
copyLocaleFiles()
})
},
}
const extensionConfig = {
bundle: true,
minify: production,
sourcemap: !production,
logLevel: "silent",
plugins: [
copyWasmFiles,
copyLocalesFiles,
/* add to the end of plugins array */
esbuildProblemMatcherPlugin,
{
name: "alias-plugin",
setup(build) {
build.onResolve({ filter: /^pkce-challenge$/ }, (_args) => {
return { path: require.resolve("pkce-challenge/dist/index.browser.js") }
})
},
},
],
entryPoints: ["src/extension.ts"],
format: "cjs",
sourcesContent: false,
platform: "node",
outfile: "dist/extension.js",
external: ["vscode"],
}
const workerConfig = {
bundle: true,
minify: production,
sourcemap: !production,
logLevel: "silent",
entryPoints: ["src/workers/countTokens.ts"],
format: "cjs",
sourcesContent: false,
platform: "node",
outdir: "dist/workers",
}
async function main() {
const [extensionCtx, workerCtx] = await Promise.all([
esbuild.context(extensionConfig),
esbuild.context(workerConfig),
])
if (watch) {
await Promise.all([extensionCtx.watch(), workerCtx.watch()])
copyLocaleFiles()
setupLocaleWatcher()
} else {
await Promise.all([extensionCtx.rebuild(), workerCtx.rebuild()])
await Promise.all([extensionCtx.dispose(), workerCtx.dispose()])
}
}
main().catch((e) => {
console.error(e)
process.exit(1)
})

View file

@ -1,7 +1,7 @@
{
"name": "@evals/monorepo",
"private": true,
"packageManager": "pnpm@10.7.1+sha512.2d92c86b7928dc8284f53494fb4201f983da65f0fb4f0d40baafa5cf628fa31dae3e5968f12466f17df7e97310e30f343a648baea1b9b350685dafafffdf5808",
"packageManager": "pnpm@10.8.1",
"scripts": {
"lint": "turbo lint --log-order grouped --output-logs new-only",
"check-types": "turbo check-types --log-order grouped --output-logs new-only",

3224
evals/pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -28,11 +28,8 @@ build_extension() {
echo "🔨 Building the Roo Code extension..."
cd ..
mkdir -p bin
npm run install-extension -- --silent --no-audit || exit 1
npm run install-webview -- --silent --no-audit || exit 1
npm run install-e2e -- --silent --no-audit || exit 1
npx vsce package --out bin/roo-code-latest.vsix || exit 1
code --install-extension bin/roo-code-latest.vsix || exit 1
pnpm build --out ../bin/roo-code-$(git rev-parse --short HEAD).vsix || exit 1
code --install-extension bin/roo-code-$(git rev-parse --short HEAD).vsix || exit 1
cd evals
}

View file

@ -9,9 +9,6 @@
"**/*.test.ts",
"**/*.test.tsx",
"**/stories/**",
"coverage/**",
"dist/**",
"out/**",
"bin/**",
"e2e/**",
"evals/**",
@ -20,7 +17,8 @@
"src/workers/**",
"src/schemas/ipc.ts",
"src/extension.ts",
"scripts/**"
"scripts/**",
"vitest.config.ts"
],
"workspaces": {
"webview-ui": {

20885
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,479 +1,55 @@
{
"name": "roo-cline",
"displayName": "%extension.displayName%",
"description": "%extension.description%",
"publisher": "RooVeterinaryInc",
"version": "3.18.0",
"icon": "assets/icons/icon.png",
"galleryBanner": {
"color": "#617A91",
"theme": "dark"
},
"name": "roo-code",
"packageManager": "pnpm@10.8.1",
"engines": {
"vscode": "^1.84.0",
"node": "20.18.1"
},
"author": {
"name": "Roo Code"
},
"repository": {
"type": "git",
"url": "https://github.com/RooCodeInc/Roo-Code"
},
"homepage": "https://github.com/RooCodeInc/Roo-Code",
"categories": [
"AI",
"Chat",
"Programming Languages",
"Education",
"Snippets",
"Testing"
],
"keywords": [
"cline",
"claude",
"dev",
"mcp",
"openrouter",
"coding",
"agent",
"autonomous",
"chatgpt",
"sonnet",
"ai",
"llama",
"roo code",
"roocode"
],
"activationEvents": [
"onLanguage",
"onStartupFinished"
],
"main": "./dist/extension.js",
"contributes": {
"viewsContainers": {
"activitybar": [
{
"id": "roo-cline-ActivityBar",
"title": "%views.activitybar.title%",
"icon": "assets/icons/icon.svg"
}
]
},
"views": {
"roo-cline-ActivityBar": [
{
"type": "webview",
"id": "roo-cline.SidebarProvider",
"name": ""
}
]
},
"commands": [
{
"command": "roo-cline.plusButtonClicked",
"title": "%command.newTask.title%",
"icon": "$(add)"
},
{
"command": "roo-cline.mcpButtonClicked",
"title": "%command.mcpServers.title%",
"icon": "$(server)"
},
{
"command": "roo-cline.promptsButtonClicked",
"title": "%command.prompts.title%",
"icon": "$(notebook)"
},
{
"command": "roo-cline.historyButtonClicked",
"title": "%command.history.title%",
"icon": "$(history)"
},
{
"command": "roo-cline.popoutButtonClicked",
"title": "%command.openInEditor.title%",
"icon": "$(link-external)"
},
{
"command": "roo-cline.settingsButtonClicked",
"title": "%command.settings.title%",
"icon": "$(settings-gear)"
},
{
"command": "roo-cline.openInNewTab",
"title": "%command.openInNewTab.title%",
"category": "%configuration.title%"
},
{
"command": "roo-cline.explainCode",
"title": "%command.explainCode.title%",
"category": "%configuration.title%"
},
{
"command": "roo-cline.fixCode",
"title": "%command.fixCode.title%",
"category": "%configuration.title%"
},
{
"command": "roo-cline.improveCode",
"title": "%command.improveCode.title%",
"category": "%configuration.title%"
},
{
"command": "roo-cline.addToContext",
"title": "%command.addToContext.title%",
"category": "%configuration.title%"
},
{
"command": "roo-cline.newTask",
"title": "%command.newTask.title%",
"category": "%configuration.title%"
},
{
"command": "roo-cline.terminalAddToContext",
"title": "%command.terminal.addToContext.title%",
"category": "Terminal"
},
{
"command": "roo-cline.terminalFixCommand",
"title": "%command.terminal.fixCommand.title%",
"category": "Terminal"
},
{
"command": "roo-cline.terminalExplainCommand",
"title": "%command.terminal.explainCommand.title%",
"category": "Terminal"
},
{
"command": "roo-cline.setCustomStoragePath",
"title": "%command.setCustomStoragePath.title%",
"category": "%configuration.title%"
},
{
"command": "roo-cline.focusInput",
"title": "%command.focusInput.title%",
"category": "%configuration.title%"
},
{
"command": "roo-cline.acceptInput",
"title": "%command.acceptInput.title%",
"category": "%configuration.title%"
}
],
"menus": {
"editor/context": [
{
"submenu": "roo-cline.contextMenu",
"group": "navigation"
}
],
"roo-cline.contextMenu": [
{
"command": "roo-cline.addToContext",
"group": "1_actions@1"
},
{
"command": "roo-cline.explainCode",
"group": "1_actions@2"
},
{
"command": "roo-cline.improveCode",
"group": "1_actions@3"
}
],
"terminal/context": [
{
"submenu": "roo-cline.terminalMenu",
"group": "navigation"
}
],
"roo-cline.terminalMenu": [
{
"command": "roo-cline.terminalAddToContext",
"group": "1_actions@1"
},
{
"command": "roo-cline.terminalFixCommand",
"group": "1_actions@2"
},
{
"command": "roo-cline.terminalExplainCommand",
"group": "1_actions@3"
}
],
"view/title": [
{
"command": "roo-cline.plusButtonClicked",
"group": "navigation@1",
"when": "view == roo-cline.SidebarProvider"
},
{
"command": "roo-cline.promptsButtonClicked",
"group": "navigation@2",
"when": "view == roo-cline.SidebarProvider"
},
{
"command": "roo-cline.mcpButtonClicked",
"group": "navigation@3",
"when": "view == roo-cline.SidebarProvider"
},
{
"command": "roo-cline.historyButtonClicked",
"group": "navigation@4",
"when": "view == roo-cline.SidebarProvider"
},
{
"command": "roo-cline.popoutButtonClicked",
"group": "navigation@5",
"when": "view == roo-cline.SidebarProvider"
},
{
"command": "roo-cline.settingsButtonClicked",
"group": "navigation@6",
"when": "view == roo-cline.SidebarProvider"
}
],
"editor/title": [
{
"command": "roo-cline.plusButtonClicked",
"group": "navigation@1",
"when": "activeWebviewPanelId == roo-cline.TabPanelProvider"
},
{
"command": "roo-cline.promptsButtonClicked",
"group": "navigation@2",
"when": "activeWebviewPanelId == roo-cline.TabPanelProvider"
},
{
"command": "roo-cline.mcpButtonClicked",
"group": "navigation@3",
"when": "activeWebviewPanelId == roo-cline.TabPanelProvider"
},
{
"command": "roo-cline.historyButtonClicked",
"group": "navigation@4",
"when": "activeWebviewPanelId == roo-cline.TabPanelProvider"
},
{
"command": "roo-cline.popoutButtonClicked",
"group": "navigation@5",
"when": "activeWebviewPanelId == roo-cline.TabPanelProvider"
},
{
"command": "roo-cline.settingsButtonClicked",
"group": "navigation@6",
"when": "activeWebviewPanelId == roo-cline.TabPanelProvider"
}
]
},
"submenus": [
{
"id": "roo-cline.contextMenu",
"label": "%views.contextMenu.label%"
},
{
"id": "roo-cline.terminalMenu",
"label": "%views.terminalMenu.label%"
}
],
"configuration": {
"title": "%configuration.title%",
"properties": {
"roo-cline.allowedCommands": {
"type": "array",
"items": {
"type": "string"
},
"default": [
"npm test",
"npm install",
"tsc",
"git log",
"git diff",
"git show"
],
"description": "%commands.allowedCommands.description%"
},
"roo-cline.vsCodeLmModelSelector": {
"type": "object",
"properties": {
"vendor": {
"type": "string",
"description": "%settings.vsCodeLmModelSelector.vendor.description%"
},
"family": {
"type": "string",
"description": "%settings.vsCodeLmModelSelector.family.description%"
}
},
"description": "%settings.vsCodeLmModelSelector.description%"
},
"roo-cline.customStoragePath": {
"type": "string",
"default": "",
"description": "%settings.customStoragePath.description%"
}
}
}
},
"scripts": {
"build": "npm run vsix",
"build:webview": "cd webview-ui && npm run build",
"build:esbuild": "node esbuild.js --production",
"compile": "tsc -p . --outDir out && node esbuild.js",
"install:all": "npm install -D npm-run-all2@8.0.1 && npm-run-all -l -p install-*",
"install-extension": "npm install",
"install-webview": "cd webview-ui && npm install",
"install-e2e": "cd e2e && npm install",
"lint": "npm-run-all -l -p lint:*",
"lint:extension": "eslint src --ext .ts",
"lint:webview": "cd webview-ui && npm run lint",
"lint:e2e": "cd e2e && npm run lint",
"check-types": "npm-run-all -l -p check-types:*",
"check-types:extension": "tsc --noEmit",
"check-types:webview": "cd webview-ui && npm run check-types",
"check-types:e2e": "cd e2e && npm run check-types",
"package": "npm-run-all -l -p build:webview build:esbuild check-types lint",
"pretest": "npm run compile",
"dev": "cd webview-ui && npm run dev",
"test": "npm-run-all test:*",
"test:extension": "jest -w=40%",
"test:extension-esm": "vitest run",
"test:webview": "cd webview-ui && npm run test",
"preinstall": "npx only-allow pnpm",
"prepare": "husky",
"publish:marketplace": "npx vsce publish && npx ovsx publish",
"publish": "npm run build && changeset publish && npm install --package-lock-only",
"version-packages": "changeset version && npm install --package-lock-only",
"vscode:prepublish": "npm run package",
"vsix": "rimraf bin && mkdirp bin && npx vsce package --out bin",
"watch": "npm-run-all -l -p watch:*",
"watch:esbuild": "node esbuild.js --watch",
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
"watch-tests": "tsc -p . -w --outDir out",
"lint": "turbo lint --log-order grouped --output-logs new-only",
"check-types": "turbo check-types --log-order grouped --output-logs new-only",
"test": "turbo test --log-order grouped --output-logs new-only",
"format": "turbo format --log-order grouped --output-logs new-only",
"clean": "turbo clean --log-order grouped --output-logs new-only && rimraf dist out bin .vite-port .turbo",
"build": "pnpm --filter roo-cline vsix",
"build:nightly": "pnpm --filter @roo-code/vscode-nightly vsix",
"changeset": "changeset",
"knip": "knip --include files",
"clean": "npm-run-all -l -p clean:*",
"clean:extension": "rimraf bin dist out",
"clean:webview": "cd webview-ui && npm run clean",
"clean:e2e": "cd e2e && npm run clean",
"vscode-test": "npm-run-all -l -p vscode-test:*",
"vscode-test:extension": "tsc -p . --outDir out && node esbuild.js",
"vscode-test:webview": "cd webview-ui && npm run build",
"update-contributors": "node scripts/update-contributors.js",
"generate-types": "tsx scripts/generate-types.mts"
},
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.10.2",
"@anthropic-ai/sdk": "^0.37.0",
"@anthropic-ai/vertex-sdk": "^0.7.0",
"@aws-sdk/client-bedrock-runtime": "^3.779.0",
"@google/genai": "^0.13.0",
"@mistralai/mistralai": "^1.3.6",
"@modelcontextprotocol/sdk": "^1.9.0",
"@types/clone-deep": "^4.0.4",
"@types/pdf-parse": "^1.1.4",
"@types/tmp": "^0.2.6",
"@types/turndown": "^5.0.5",
"@types/vscode": "^1.95.0",
"@vscode/codicons": "^0.0.36",
"axios": "^1.7.4",
"cheerio": "^1.0.0",
"chokidar": "^4.0.1",
"clone-deep": "^4.0.1",
"default-shell": "^2.2.0",
"delay": "^6.0.0",
"diff": "^5.2.0",
"diff-match-patch": "^1.0.5",
"fast-deep-equal": "^3.1.3",
"fast-xml-parser": "^4.5.1",
"fastest-levenshtein": "^1.0.16",
"fzf": "^0.5.2",
"get-folder-size": "^5.0.0",
"i18next": "^24.2.2",
"isbinaryfile": "^5.0.2",
"mammoth": "^1.8.0",
"monaco-vscode-textmate-theme-converter": "^0.1.7",
"node-cache": "^5.1.2",
"node-ipc": "^12.0.0",
"openai": "^4.78.1",
"os-name": "^6.0.0",
"p-wait-for": "^5.0.2",
"pdf-parse": "^1.1.1",
"pkce-challenge": "^4.1.0",
"posthog-node": "^4.7.0",
"pretty-bytes": "^6.1.1",
"ps-tree": "^1.2.0",
"puppeteer-chromium-resolver": "^23.0.0",
"puppeteer-core": "^23.4.0",
"reconnecting-eventsource": "^1.6.4",
"sanitize-filename": "^1.6.3",
"say": "^0.16.0",
"serialize-error": "^11.0.3",
"simple-git": "^3.27.0",
"string-similarity": "^4.0.4",
"strip-ansi": "^7.1.0",
"strip-bom": "^5.0.0",
"tiktoken": "^1.0.21",
"tmp": "^0.2.3",
"tree-sitter-wasms": "^0.1.11",
"turndown": "^7.2.0",
"vscode-material-icons": "^0.1.1",
"web-tree-sitter": "^0.22.6",
"workerpool": "^9.2.0",
"yaml": "^2.8.0",
"zod": "^3.24.2"
"knip": "pnpm --filter @roo-code/build build && knip --include files",
"update-contributors": "node scripts/update-contributors.js"
},
"devDependencies": {
"@changesets/cli": "^2.27.10",
"@changesets/types": "^6.0.0",
"@dotenvx/dotenvx": "^1.34.0",
"@types/debug": "^4.1.12",
"@types/diff": "^5.2.1",
"@types/diff-match-patch": "^1.0.36",
"@types/glob": "^8.1.0",
"@types/jest": "^29.5.14",
"@types/mocha": "^10.0.10",
"@types/node": "20.x",
"@types/node-cache": "^4.1.3",
"@types/node-ipc": "^9.2.3",
"@types/ps-tree": "^1.1.6",
"@types/string-similarity": "^4.0.2",
"@typescript-eslint/eslint-plugin": "^7.14.1",
"@typescript-eslint/parser": "^7.11.0",
"@vscode/test-electron": "^2.5.2",
"@vscode/vsce": "3.3.2",
"esbuild": "^0.25.0",
"eslint": "^8.57.0",
"execa": "^9.5.2",
"glob": "^11.0.1",
"husky": "^9.1.7",
"jest": "^29.7.0",
"jest-simple-dot-reporter": "^1.0.5",
"knip": "^5.44.4",
"lint-staged": "^15.2.11",
"mkdirp": "^3.0.1",
"nock": "^14.0.4",
"npm-run-all2": "^8.0.1",
"only-allow": "^1.2.1",
"ovsx": "0.10.2",
"prettier": "^3.4.2",
"rimraf": "^6.0.1",
"ts-jest": "^29.2.5",
"tsup": "^8.4.0",
"tsx": "^4.19.3",
"typescript": "5.8.3",
"vitest": "^3.1.3",
"zod-to-ts": "^1.2.0"
"turbo": "^2.5.3",
"typescript": "^5.4.5"
},
"lint-staged": {
"*.{js,jsx,ts,tsx,json,css,md}": [
"prettier --write"
],
"src/**/*.{ts,tsx}": [
"npx eslint -c .eslintrc.json --max-warnings=0 --fix"
"npx eslint -c src/.eslintrc.json --max-warnings=0 --fix"
],
"webview-ui/**/*.{ts,tsx}": [
"npx eslint -c webview-ui/.eslintrc.json --max-warnings=0 --fix"
],
"e2e/**/*.{ts,tsx}": [
"npx eslint -c src/.eslintrc.json --max-warnings=0 --fix"
],
"{apps,packages}/**/*.{ts,tsx}": [
"npx eslint --max-warnings=0 --fix"
]
}
}

View file

@ -0,0 +1,25 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module"
},
"plugins": ["@typescript-eslint"],
"rules": {
"@typescript-eslint/naming-convention": [
"warn",
{
"selector": "import",
"format": ["camelCase", "PascalCase"]
}
],
"@typescript-eslint/semi": "off",
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error", { "varsIgnorePattern": "^_", "argsIgnorePattern": "^_" }],
"eqeqeq": "warn",
"no-throw-literal": "warn",
"semi": "off"
},
"ignorePatterns": ["dist"]
}

View file

@ -0,0 +1,21 @@
{
"name": "@roo-code/build",
"description": "ESBuild utilities for Roo Code.",
"private": true,
"type": "module",
"exports": "./dist/index.js",
"scripts": {
"lint": "eslint src --ext .ts,.tsx --max-warnings=0",
"check-types": "tsc --noEmit",
"test": "vitest --globals --run",
"build": "tsc",
"clean": "rimraf dist .turbo"
},
"devDependencies": {
"@types/node": "^22.15.20",
"vitest": "^3.1.3"
},
"dependencies": {
"zod": "^3.24.2"
}
}

View file

@ -0,0 +1,212 @@
// npx vitest --globals run src/__tests__/index.test.ts
import { generatePackageJson } from "../index.js"
describe("generatePackageJson", () => {
it("should be a test", () => {
const generatedPackageJson = generatePackageJson({
packageJson: {
name: "roo-cline",
displayName: "%extension.displayName%",
description: "%extension.description%",
publisher: "RooVeterinaryInc",
version: "3.17.2",
icon: "assets/icons/icon.png",
contributes: {
viewsContainers: {
activitybar: [
{
id: "roo-cline-ActivityBar",
title: "%views.activitybar.title%",
icon: "assets/icons/icon.svg",
},
],
},
views: {
"roo-cline-ActivityBar": [
{
type: "webview",
id: "roo-cline.SidebarProvider",
name: "",
},
],
},
commands: [
{
command: "roo-cline.plusButtonClicked",
title: "%command.newTask.title%",
icon: "$(add)",
},
{
command: "roo-cline.openInNewTab",
title: "%command.openInNewTab.title%",
category: "%configuration.title%",
},
],
menus: {
"editor/context": [
{
submenu: "roo-cline.contextMenu",
group: "navigation",
},
],
"roo-cline.contextMenu": [
{
command: "roo-cline.addToContext",
group: "1_actions@1",
},
],
"editor/title": [
{
command: "roo-cline.plusButtonClicked",
group: "navigation@1",
when: "activeWebviewPanelId == roo-cline.TabPanelProvider",
},
{
command: "roo-cline.settingsButtonClicked",
group: "navigation@6",
when: "activeWebviewPanelId == roo-cline.TabPanelProvider",
},
],
},
submenus: [
{
id: "roo-cline.contextMenu",
label: "%views.contextMenu.label%",
},
{
id: "roo-cline.terminalMenu",
label: "%views.terminalMenu.label%",
},
],
configuration: {
title: "%configuration.title%",
properties: {
"roo-cline.allowedCommands": {
type: "array",
items: {
type: "string",
},
default: ["npm test", "npm install", "tsc", "git log", "git diff", "git show"],
description: "%commands.allowedCommands.description%",
},
"roo-cline.customStoragePath": {
type: "string",
default: "",
description: "%settings.customStoragePath.description%",
},
},
},
},
scripts: {
lint: "eslint **/*.ts",
},
},
overrideJson: {
name: "roo-code-nightly",
displayName: "Roo Code Nightly",
publisher: "RooVeterinaryInc",
version: "0.0.1",
icon: "assets/icons/icon-nightly.png",
scripts: {},
},
substitution: ["roo-cline", "roo-code-nightly"],
})
expect(generatedPackageJson).toStrictEqual({
name: "roo-code-nightly",
displayName: "Roo Code Nightly",
description: "%extension.description%",
publisher: "RooVeterinaryInc",
version: "0.0.1",
icon: "assets/icons/icon-nightly.png",
contributes: {
viewsContainers: {
activitybar: [
{
id: "roo-code-nightly-ActivityBar",
title: "%views.activitybar.title%",
icon: "assets/icons/icon.svg",
},
],
},
views: {
"roo-code-nightly-ActivityBar": [
{
type: "webview",
id: "roo-code-nightly.SidebarProvider",
name: "",
},
],
},
commands: [
{
command: "roo-code-nightly.plusButtonClicked",
title: "%command.newTask.title%",
icon: "$(add)",
},
{
command: "roo-code-nightly.openInNewTab",
title: "%command.openInNewTab.title%",
category: "%configuration.title%",
},
],
menus: {
"editor/context": [
{
submenu: "roo-code-nightly.contextMenu",
group: "navigation",
},
],
"roo-code-nightly.contextMenu": [
{
command: "roo-code-nightly.addToContext",
group: "1_actions@1",
},
],
"editor/title": [
{
command: "roo-code-nightly.plusButtonClicked",
group: "navigation@1",
when: "activeWebviewPanelId == roo-code-nightly.TabPanelProvider",
},
{
command: "roo-code-nightly.settingsButtonClicked",
group: "navigation@6",
when: "activeWebviewPanelId == roo-code-nightly.TabPanelProvider",
},
],
},
submenus: [
{
id: "roo-code-nightly.contextMenu",
label: "%views.contextMenu.label%",
},
{
id: "roo-code-nightly.terminalMenu",
label: "%views.terminalMenu.label%",
},
],
configuration: {
title: "%configuration.title%",
properties: {
"roo-code-nightly.allowedCommands": {
type: "array",
items: {
type: "string",
},
default: ["npm test", "npm install", "tsc", "git log", "git diff", "git show"],
description: "%commands.allowedCommands.description%",
},
"roo-code-nightly.customStoragePath": {
type: "string",
default: "",
description: "%settings.customStoragePath.description%",
},
},
},
},
scripts: {},
})
})
})

View file

@ -0,0 +1,205 @@
import * as fs from "fs"
import * as path from "path"
import { ViewsContainer, Views, Menus, Configuration, contributesSchema } from "./types.js"
export function copyPaths(copyPaths: [string, string][], srcDir: string, dstDir: string) {
copyPaths.forEach(([srcRelPath, dstRelPath]) => {
const stats = fs.lstatSync(path.join(srcDir, srcRelPath))
if (stats.isDirectory()) {
if (fs.existsSync(path.join(dstDir, dstRelPath))) {
fs.rmSync(path.join(dstDir, dstRelPath), { recursive: true })
}
fs.mkdirSync(path.join(dstDir, dstRelPath), { recursive: true })
const count = copyDir(path.join(srcDir, srcRelPath), path.join(dstDir, dstRelPath), 0)
console.log(`[copyPaths] Copied ${count} files from ${srcRelPath} to ${dstRelPath}`)
} else {
fs.copyFileSync(path.join(srcDir, srcRelPath), path.join(dstDir, dstRelPath))
console.log(`[copyPaths] Copied ${srcRelPath} to ${dstRelPath}`)
}
})
}
export function copyDir(srcDir: string, dstDir: string, count: number): number {
const entries = fs.readdirSync(srcDir, { withFileTypes: true })
for (const entry of entries) {
const srcPath = path.join(srcDir, entry.name)
const dstPath = path.join(dstDir, entry.name)
if (entry.isDirectory()) {
fs.mkdirSync(dstPath, { recursive: true })
count = copyDir(srcPath, dstPath, count)
} else {
count = count + 1
fs.copyFileSync(srcPath, dstPath)
}
}
return count
}
export function copyWasms(srcDir: string, distDir: string): void {
const nodeModulesDir = path.join(srcDir, "node_modules")
fs.mkdirSync(distDir, { recursive: true })
// Tiktoken WASM file.
fs.copyFileSync(
path.join(nodeModulesDir, "tiktoken", "lite", "tiktoken_bg.wasm"),
path.join(distDir, "tiktoken_bg.wasm"),
)
console.log(`[copyWasms] Copied tiktoken WASMs to ${distDir}`)
// Also copy Tiktoken WASMs to the workers directory.
const workersDir = path.join(distDir, "workers")
fs.mkdirSync(workersDir, { recursive: true })
fs.copyFileSync(
path.join(nodeModulesDir, "tiktoken", "lite", "tiktoken_bg.wasm"),
path.join(workersDir, "tiktoken_bg.wasm"),
)
console.log(`[copyWasms] Copied tiktoken WASMs to ${workersDir}`)
// Main tree-sitter WASM file.
fs.copyFileSync(
path.join(nodeModulesDir, "web-tree-sitter", "tree-sitter.wasm"),
path.join(distDir, "tree-sitter.wasm"),
)
console.log(`[copyWasms] Copied tree-sitter.wasm to ${distDir}`)
// Copy language-specific WASM files.
const languageWasmDir = path.join(nodeModulesDir, "tree-sitter-wasms", "out")
if (!fs.existsSync(languageWasmDir)) {
throw new Error(`Directory does not exist: ${languageWasmDir}`)
}
// Dynamically read all WASM files from the directory instead of using a hardcoded list.
const wasmFiles = fs.readdirSync(languageWasmDir).filter((file) => file.endsWith(".wasm"))
wasmFiles.forEach((filename) => {
fs.copyFileSync(path.join(languageWasmDir, filename), path.join(distDir, filename))
})
console.log(`[copyWasms] Copied ${wasmFiles.length} tree-sitter language wasms to ${distDir}`)
}
export function copyLocales(srcDir: string, distDir: string): void {
const destDir = path.join(distDir, "i18n", "locales")
fs.mkdirSync(destDir, { recursive: true })
const count = copyDir(path.join(srcDir, "i18n", "locales"), destDir, 0)
console.log(`[copyLocales] Copied ${count} locale files to ${destDir}`)
}
export function setupLocaleWatcher(srcDir: string, distDir: string) {
const localesDir = path.join(srcDir, "i18n", "locales")
if (!fs.existsSync(localesDir)) {
console.warn(`Cannot set up watcher: Source locales directory does not exist: ${localesDir}`)
return
}
console.log(`Setting up watcher for locale files in ${localesDir}`)
let debounceTimer: NodeJS.Timeout | null = null
const debouncedCopy = () => {
if (debounceTimer) {
clearTimeout(debounceTimer)
}
// Wait 300ms after last change before copying.
debounceTimer = setTimeout(() => {
console.log("Locale files changed, copying...")
copyLocales(srcDir, distDir)
}, 300)
}
try {
fs.watch(localesDir, { recursive: true }, (_eventType, filename) => {
if (filename && filename.endsWith(".json")) {
console.log(`Locale file ${filename} changed, triggering copy...`)
debouncedCopy()
}
})
console.log("Watcher for locale files is set up")
} catch (error) {
console.error(
`Error setting up watcher for ${localesDir}:`,
error instanceof Error ? error.message : "Unknown error",
)
}
}
export function generatePackageJson({
packageJson: { contributes, ...packageJson },
overrideJson,
substitution,
}: {
packageJson: Record<string, any>
overrideJson: Record<string, any>
substitution: [string, string]
}) {
const { viewsContainers, views, commands, menus, submenus, configuration } = contributesSchema.parse(contributes)
const [from, to] = substitution
return {
...packageJson,
...overrideJson,
contributes: {
viewsContainers: transformArrayRecord<ViewsContainer>(viewsContainers, from, to, ["id"]),
views: transformArrayRecord<Views>(views, from, to, ["id"]),
commands: transformArray(commands, from, to, "command"),
menus: transformArrayRecord<Menus>(menus, from, to, ["command", "submenu", "when"]),
submenus: transformArray(submenus, from, to, "id"),
configuration: {
title: configuration.title,
properties: transformRecord<Configuration["properties"]>(configuration.properties, from, to),
},
},
}
}
function transformArrayRecord<T>(obj: Record<string, any[]>, from: string, to: string, props: string[]): T {
return Object.entries(obj).reduce(
(acc, [key, ary]) => ({
...acc,
[key.replace(from, to)]: ary.map((item) => {
const transformedItem = { ...item }
for (const prop of props) {
if (prop in item && typeof item[prop] === "string") {
transformedItem[prop] = item[prop].replace(from, to)
}
}
return transformedItem
}),
}),
{} as T,
)
}
function transformArray<T>(arr: any[], from: string, to: string, idProp: string): T[] {
return arr.map(({ [idProp]: id, ...rest }) => ({
[idProp]: id.replace(from, to),
...rest,
}))
}
function transformRecord<T>(obj: Record<string, any>, from: string, to: string): T {
return Object.entries(obj).reduce(
(acc, [key, value]) => ({
...acc,
[key.replace(from, to)]: value,
}),
{} as T,
)
}

11
packages/build/src/git.ts Normal file
View file

@ -0,0 +1,11 @@
import { execSync } from "child_process"
export function getGitSha() {
let gitSha = undefined
try {
gitSha = execSync("git rev-parse HEAD").toString().trim()
} catch (e) {}
return gitSha
}

View file

@ -0,0 +1,2 @@
export { getGitSha } from "./git.js"
export { copyPaths, copyDir, copyWasms, copyLocales, setupLocaleWatcher, generatePackageJson } from "./esbuild.js"

View file

@ -0,0 +1,98 @@
import { z } from "zod"
const viewsContainerSchema = z.record(
z.string(),
z.array(
z.object({
id: z.string(),
title: z.string(),
icon: z.string(),
}),
),
)
export type ViewsContainer = z.infer<typeof viewsContainerSchema>
const viewsSchema = z.record(
z.string(),
z.array(
z.object({
type: z.string(),
id: z.string(),
name: z.string(),
}),
),
)
export type Views = z.infer<typeof viewsSchema>
const commandsSchema = z.array(
z.object({
command: z.string(),
title: z.string(),
category: z.string().optional(),
icon: z.string().optional(),
}),
)
export type Commands = z.infer<typeof commandsSchema>
const menuItemSchema = z.object({
group: z.string(),
command: z.string().optional(),
submenu: z.string().optional(),
when: z.string().optional(),
})
export type MenuItem = z.infer<typeof menuItemSchema>
const menusSchema = z.record(z.string(), z.array(menuItemSchema))
export type Menus = z.infer<typeof menusSchema>
const submenusSchema = z.array(
z.object({
id: z.string(),
label: z.string(),
}),
)
export type Submenus = z.infer<typeof submenusSchema>
const configurationPropertySchema = z.object({
type: z.union([
z.literal("string"),
z.literal("array"),
z.literal("object"),
z.literal("boolean"),
z.literal("number"),
]),
items: z
.object({
type: z.string(),
})
.optional(),
properties: z.record(z.string(), z.any()).optional(),
default: z.any().optional(),
description: z.string(),
})
export type ConfigurationProperty = z.infer<typeof configurationPropertySchema>
const configurationSchema = z.object({
title: z.string(),
properties: z.record(z.string(), configurationPropertySchema),
})
export type Configuration = z.infer<typeof configurationSchema>
export const contributesSchema = z.object({
viewsContainers: viewsContainerSchema,
views: viewsSchema,
commands: commandsSchema,
menus: menusSchema,
submenus: submenusSchema,
configuration: configurationSchema,
})
export type Contributes = z.infer<typeof contributesSchema>

View file

@ -0,0 +1,22 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"declaration": true,
"declarationMap": true,
"esModuleInterop": true,
"incremental": false,
"isolatedModules": true,
"lib": ["es2022", "DOM", "DOM.Iterable"],
"module": "NodeNext",
"moduleDetection": "force",
"moduleResolution": "NodeNext",
"noUncheckedIndexedAccess": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": true,
"target": "ES2022",
"types": ["vitest/globals"],
"outDir": "dist"
},
"include": ["src"]
}

20586
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load diff

6
pnpm-workspace.yaml Normal file
View file

@ -0,0 +1,6 @@
packages:
- "src"
- "webview-ui"
- "e2e"
- "apps/*"
- "packages/*"

View file

@ -1,34 +0,0 @@
import path from "path"
import fs from "fs"
import { zodToTs, createTypeAlias, printNode } from "zod-to-ts"
import { $ } from "execa"
import schemas from "../src/schemas"
const { typeDefinitions } = schemas
async function main() {
const types: string[] = [
"// This file is automatically generated by running `npm run generate-types`\n// Do not edit it directly.",
]
for (const { schema, identifier } of typeDefinitions) {
types.push(printNode(createTypeAlias(zodToTs(schema, identifier).node, identifier)))
types.push(`export type { ${identifier} }`)
}
fs.writeFileSync("src/exports/types.ts", types.join("\n\n"))
await $`npx tsup src/exports/interface.ts --dts -d out`
fs.copyFileSync("out/interface.d.ts", "src/exports/roo-code.d.ts")
await $`npx prettier --write src/exports/types.ts src/exports/roo-code.d.ts`
if (fs.existsSync(path.join("..", "Roo-Code-Types"))) {
fs.copyFileSync("out/interface.js", path.join("..", "Roo-Code-Types", "src", "index.js"))
fs.copyFileSync("out/interface.d.ts", path.join("..", "Roo-Code-Types", "src", "index.d.ts"))
}
}
main()

5
src/.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
README.md
CHANGELOG.md
LICENSE
webview-ui
assets/vscode-material-icons

2
src/.prettierignore Normal file
View file

@ -0,0 +1,2 @@
dist/
webview-ui/

33
src/.vscodeignore Normal file
View file

@ -0,0 +1,33 @@
# Exclude everything
**
# Include README.md, CHANGELOG.md and LICENSE
!README.md
!CHANGELOG.md
!LICENSE
# Include package.json
!package.json
!package.nls.*
# Include the built extension
!dist
# Include the built webview
**/*.map
!webview-ui/audio
!webview-ui/build/assets/*.js
!webview-ui/build/assets/*.ttf
!webview-ui/build/assets/*.css
# Include default themes JSON files used in getTheme
!integrations/theme/default-themes/**
# Include icons and images
!assets/codicons/**
!assets/vscode-material-icons/**
!assets/icons/**
!assets/images/**
# Include .env file for telemetry
!.env

File diff suppressed because it is too large Load diff

Binary file not shown.

View file

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

BIN
src/assets/icons/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

View file

Before

Width:  |  Height:  |  Size: 884 B

After

Width:  |  Height:  |  Size: 884 B

View file

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

View file

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View file

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -572,21 +572,8 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
"index.css",
])
const codiconsUri = getUri(webview, this.contextProxy.extensionUri, [
"node_modules",
"@vscode",
"codicons",
"dist",
"codicon.css",
])
const materialIconsUri = getUri(webview, this.contextProxy.extensionUri, [
"node_modules",
"vscode-material-icons",
"generated",
"icons",
])
const codiconsUri = getUri(webview, this.contextProxy.extensionUri, ["assets", "codicons", "codicon.css"])
const materialIconsUri = getUri(webview, this.contextProxy.extensionUri, ["assets", "vscode-material-icons"])
const imagesUri = getUri(webview, this.contextProxy.extensionUri, ["assets", "images"])
const audioUri = getUri(webview, this.contextProxy.extensionUri, ["webview-ui", "audio"])
@ -660,40 +647,13 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
"assets",
"index.css",
])
// The JS file from the React build output
const scriptUri = getUri(webview, this.contextProxy.extensionUri, ["webview-ui", "build", "assets", "index.js"])
// The codicon font from the React build output
// https://github.com/microsoft/vscode-extension-samples/blob/main/webview-codicons-sample/src/extension.ts
// we installed this package in the extension so that we can access it how its intended from the extension (the font file is likely bundled in vscode), and we just import the css fileinto our react app we don't have access to it
// don't forget to add font-src ${webview.cspSource};
const codiconsUri = getUri(webview, this.contextProxy.extensionUri, [
"node_modules",
"@vscode",
"codicons",
"dist",
"codicon.css",
])
// The material icons from the React build output
const materialIconsUri = getUri(webview, this.contextProxy.extensionUri, [
"node_modules",
"vscode-material-icons",
"generated",
"icons",
])
const codiconsUri = getUri(webview, this.contextProxy.extensionUri, ["assets", "codicons", "codicon.css"])
const materialIconsUri = getUri(webview, this.contextProxy.extensionUri, ["assets", "vscode-material-icons"])
const imagesUri = getUri(webview, this.contextProxy.extensionUri, ["assets", "images"])
const audioUri = getUri(webview, this.contextProxy.extensionUri, ["webview-ui", "audio"])
// const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, "assets", "main.js"))
// const styleResetUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, "assets", "reset.css"))
// const styleVSCodeUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, "assets", "vscode.css"))
// // Same for stylesheet
// const stylesheetUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, "assets", "main.css"))
// Use a nonce to only allow a specific script to be run.
/*
content security policy of your webview to only allow scripts that have a specific nonce

121
src/esbuild.mjs Normal file
View file

@ -0,0 +1,121 @@
import * as esbuild from "esbuild"
import * as path from "path"
import { fileURLToPath } from "url"
import { copyPaths, copyWasms, copyLocales, setupLocaleWatcher } from "@roo-code/build"
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
async function main() {
const production = process.argv.includes("--production")
const watch = process.argv.includes("--watch")
const minify = production
const sourcemap = !production
/**
* @type {import('esbuild').BuildOptions}
*/
const buildOptions = {
bundle: true,
minify,
sourcemap,
logLevel: "silent",
format: "cjs",
sourcesContent: false,
platform: "node",
}
const srcDir = __dirname
const buildDir = __dirname
const distDir = path.join(buildDir, "dist")
/**
* @type {import('esbuild').Plugin[]}
*/
const plugins = [
{
name: "copy-files",
setup(build) {
build.onEnd(() => {
copyPaths(
[
["../README.md", "README.md"],
["../CHANGELOG.md", "CHANGELOG.md"],
["../LICENSE", "LICENSE"],
["node_modules/vscode-material-icons/generated", "assets/vscode-material-icons"],
["../webview-ui/audio", "webview-ui/audio"],
],
srcDir,
buildDir,
)
})
},
},
{
name: "copy-wasms",
setup(build) {
build.onEnd(() => copyWasms(srcDir, distDir))
},
},
{
name: "copy-locales",
setup(build) {
build.onEnd(() => copyLocales(srcDir, distDir))
},
},
{
name: "esbuild-problem-matcher",
setup(build) {
build.onStart(() => console.log("[esbuild-problem-matcher#onStart]"))
build.onEnd((result) => {
result.errors.forEach(({ text, location }) => {
console.error(`✘ [ERROR] ${text}`)
console.error(` ${location.file}:${location.line}:${location.column}:`)
})
console.log("[esbuild-problem-matcher#onEnd]")
})
},
},
]
/**
* @type {import('esbuild').BuildOptions}
*/
const extensionConfig = {
...buildOptions,
plugins,
entryPoints: ["extension.ts"],
outfile: "dist/extension.js",
external: ["vscode"],
}
/**
* @type {import('esbuild').BuildOptions}
*/
const workerConfig = {
...buildOptions,
entryPoints: ["workers/countTokens.ts"],
outdir: "dist/workers",
}
const [extensionCtx, workerCtx] = await Promise.all([
esbuild.context(extensionConfig),
esbuild.context(workerConfig),
])
if (watch) {
await Promise.all([extensionCtx.watch(), workerCtx.watch()])
copyLocales(srcDir, distDir)
setupLocaleWatcher(srcDir, distDir)
} else {
await Promise.all([extensionCtx.rebuild(), workerCtx.rebuild()])
await Promise.all([extensionCtx.dispose(), workerCtx.dispose()])
}
}
main().catch((e) => {
console.error(e)
process.exit(1)
})

View file

@ -1548,6 +1548,7 @@ declare const Package: {
readonly name: string
readonly version: string
readonly outputChannel: string
readonly sha: string | undefined
}
/**
* ProviderName

View file

@ -1,4 +1,4 @@
// This file is automatically generated by running `npm run generate-types`
// This file is automatically generated by running `pnpm --filter roo-cline generate-types`
// Do not edit it directly.
type GlobalSettings = {

View file

@ -51,7 +51,7 @@ export async function activate(context: vscode.ExtensionContext) {
extensionContext = context
outputChannel = vscode.window.createOutputChannel(Package.outputChannel)
context.subscriptions.push(outputChannel)
outputChannel.appendLine(`${Package.name} extension activated`)
outputChannel.appendLine(`${Package.name} extension activated - ${JSON.stringify(Package)}`)
// Migrate old settings to new
await migrateSettings(context, outputChannel)

View file

@ -140,7 +140,7 @@ async function testTerminalCommand(
processId: Promise.resolve(123),
creationOptions: {},
exitStatus: undefined,
state: { isInteractedWith: true },
state: { isInteractedWith: true, shell: undefined },
dispose: jest.fn(),
hide: jest.fn(),
show: jest.fn(),

View file

@ -87,7 +87,7 @@ async function testCmdCommand(
processId: Promise.resolve(123),
creationOptions: {},
exitStatus: undefined,
state: { isInteractedWith: true },
state: { isInteractedWith: true, shell: undefined },
dispose: jest.fn(),
hide: jest.fn(),
show: jest.fn(),

View file

@ -88,7 +88,7 @@ async function testPowerShellCommand(
processId: Promise.resolve(123),
creationOptions: {},
exitStatus: undefined,
state: { isInteractedWith: true },
state: { isInteractedWith: true, shell: undefined },
dispose: jest.fn(),
hide: jest.fn(),
show: jest.fn(),

View file

@ -56,7 +56,7 @@ export async function getTheme() {
if (currentTheme === undefined && defaultThemes[colorTheme]) {
const filename = `${defaultThemes[colorTheme]}.json`
currentTheme = await fs.readFile(
path.join(getExtensionUri().fsPath, "src", "integrations", "theme", "default-themes", filename),
path.join(getExtensionUri().fsPath, "integrations", "theme", "default-themes", filename),
"utf-8",
)
}
@ -66,7 +66,7 @@ export async function getTheme() {
if (parsed.include) {
const includeThemeString = await fs.readFile(
path.join(getExtensionUri().fsPath, "src", "integrations", "theme", "default-themes", parsed.include),
path.join(getExtensionUri().fsPath, "integrations", "theme", "default-themes", parsed.include),
"utf-8",
)
const includeTheme = parseThemeString(includeThemeString)

View file

@ -25,25 +25,23 @@ module.exports = {
// PowerShell tests are conditionally skipped in the test files themselves using the setupFilesAfterEnv
],
moduleNameMapper: {
"^vscode$": "<rootDir>/src/__mocks__/vscode.js",
"@modelcontextprotocol/sdk$": "<rootDir>/src/__mocks__/@modelcontextprotocol/sdk/index.js",
"@modelcontextprotocol/sdk/(.*)": "<rootDir>/src/__mocks__/@modelcontextprotocol/sdk/$1",
"^delay$": "<rootDir>/src/__mocks__/delay.js",
"^p-wait-for$": "<rootDir>/src/__mocks__/p-wait-for.js",
"^serialize-error$": "<rootDir>/src/__mocks__/serialize-error.js",
"^strip-ansi$": "<rootDir>/src/__mocks__/strip-ansi.js",
"^default-shell$": "<rootDir>/src/__mocks__/default-shell.js",
"^os-name$": "<rootDir>/src/__mocks__/os-name.js",
"^strip-bom$": "<rootDir>/src/__mocks__/strip-bom.js",
"^@roo/(.*)$": "<rootDir>/src/$1",
"^@src/(.*)$": "<rootDir>/webview-ui/src/$1",
"^vscode$": "<rootDir>/__mocks__/vscode.js",
"@modelcontextprotocol/sdk$": "<rootDir>/__mocks__/@modelcontextprotocol/sdk/index.js",
"@modelcontextprotocol/sdk/(.*)": "<rootDir>/__mocks__/@modelcontextprotocol/sdk/$1",
"^delay$": "<rootDir>/__mocks__/delay.js",
"^p-wait-for$": "<rootDir>/__mocks__/p-wait-for.js",
"^serialize-error$": "<rootDir>/__mocks__/serialize-error.js",
"^strip-ansi$": "<rootDir>/__mocks__/strip-ansi.js",
"^default-shell$": "<rootDir>/__mocks__/default-shell.js",
"^os-name$": "<rootDir>/__mocks__/os-name.js",
"^strip-bom$": "<rootDir>/__mocks__/strip-bom.js",
},
transformIgnorePatterns: [
"node_modules/(?!(@modelcontextprotocol|delay|p-wait-for|serialize-error|strip-ansi|default-shell|os-name|strip-bom)/)",
],
roots: ["<rootDir>/src", "<rootDir>/webview-ui/src"],
modulePathIgnorePatterns: [".vscode-test"],
roots: ["<rootDir>"],
modulePathIgnorePatterns: ["dist", "out"],
reporters: [["jest-simple-dot-reporter", {}]],
setupFiles: ["<rootDir>/src/__mocks__/jest.setup.ts"],
setupFilesAfterEnv: ["<rootDir>/src/integrations/terminal/__tests__/setupTerminalTests.ts"],
setupFiles: ["<rootDir>/__mocks__/jest.setup.ts"],
setupFilesAfterEnv: ["<rootDir>/integrations/terminal/__tests__/setupTerminalTests.ts"],
}

442
src/package.json Normal file
View file

@ -0,0 +1,442 @@
{
"name": "roo-cline",
"displayName": "%extension.displayName%",
"description": "%extension.description%",
"publisher": "RooVeterinaryInc",
"version": "3.18.0",
"icon": "assets/icons/icon.png",
"galleryBanner": {
"color": "#617A91",
"theme": "dark"
},
"engines": {
"vscode": "^1.84.0",
"node": "20.18.1"
},
"author": {
"name": "Roo Code"
},
"repository": {
"type": "git",
"url": "https://github.com/RooCodeInc/Roo-Code"
},
"homepage": "https://github.com/RooCodeInc/Roo-Code",
"categories": [
"AI",
"Chat",
"Programming Languages",
"Education",
"Snippets",
"Testing"
],
"keywords": [
"cline",
"claude",
"dev",
"mcp",
"openrouter",
"coding",
"agent",
"autonomous",
"chatgpt",
"sonnet",
"ai",
"llama",
"roo code",
"roocode"
],
"activationEvents": [
"onLanguage",
"onStartupFinished"
],
"main": "./dist/extension.js",
"contributes": {
"viewsContainers": {
"activitybar": [
{
"id": "roo-cline-ActivityBar",
"title": "%views.activitybar.title%",
"icon": "assets/icons/icon.svg"
}
]
},
"views": {
"roo-cline-ActivityBar": [
{
"type": "webview",
"id": "roo-cline.SidebarProvider",
"name": ""
}
]
},
"commands": [
{
"command": "roo-cline.plusButtonClicked",
"title": "%command.newTask.title%",
"icon": "$(add)"
},
{
"command": "roo-cline.mcpButtonClicked",
"title": "%command.mcpServers.title%",
"icon": "$(server)"
},
{
"command": "roo-cline.promptsButtonClicked",
"title": "%command.prompts.title%",
"icon": "$(notebook)"
},
{
"command": "roo-cline.historyButtonClicked",
"title": "%command.history.title%",
"icon": "$(history)"
},
{
"command": "roo-cline.popoutButtonClicked",
"title": "%command.openInEditor.title%",
"icon": "$(link-external)"
},
{
"command": "roo-cline.settingsButtonClicked",
"title": "%command.settings.title%",
"icon": "$(settings-gear)"
},
{
"command": "roo-cline.openInNewTab",
"title": "%command.openInNewTab.title%",
"category": "%configuration.title%"
},
{
"command": "roo-cline.explainCode",
"title": "%command.explainCode.title%",
"category": "%configuration.title%"
},
{
"command": "roo-cline.fixCode",
"title": "%command.fixCode.title%",
"category": "%configuration.title%"
},
{
"command": "roo-cline.improveCode",
"title": "%command.improveCode.title%",
"category": "%configuration.title%"
},
{
"command": "roo-cline.addToContext",
"title": "%command.addToContext.title%",
"category": "%configuration.title%"
},
{
"command": "roo-cline.newTask",
"title": "%command.newTask.title%",
"category": "%configuration.title%"
},
{
"command": "roo-cline.terminalAddToContext",
"title": "%command.terminal.addToContext.title%",
"category": "Terminal"
},
{
"command": "roo-cline.terminalFixCommand",
"title": "%command.terminal.fixCommand.title%",
"category": "Terminal"
},
{
"command": "roo-cline.terminalExplainCommand",
"title": "%command.terminal.explainCommand.title%",
"category": "Terminal"
},
{
"command": "roo-cline.setCustomStoragePath",
"title": "%command.setCustomStoragePath.title%",
"category": "%configuration.title%"
},
{
"command": "roo-cline.focusInput",
"title": "%command.focusInput.title%",
"category": "%configuration.title%"
},
{
"command": "roo-cline.acceptInput",
"title": "%command.acceptInput.title%",
"category": "%configuration.title%"
}
],
"menus": {
"editor/context": [
{
"submenu": "roo-cline.contextMenu",
"group": "navigation"
}
],
"roo-cline.contextMenu": [
{
"command": "roo-cline.addToContext",
"group": "1_actions@1"
},
{
"command": "roo-cline.explainCode",
"group": "1_actions@2"
},
{
"command": "roo-cline.improveCode",
"group": "1_actions@3"
}
],
"terminal/context": [
{
"submenu": "roo-cline.terminalMenu",
"group": "navigation"
}
],
"roo-cline.terminalMenu": [
{
"command": "roo-cline.terminalAddToContext",
"group": "1_actions@1"
},
{
"command": "roo-cline.terminalFixCommand",
"group": "1_actions@2"
},
{
"command": "roo-cline.terminalExplainCommand",
"group": "1_actions@3"
}
],
"view/title": [
{
"command": "roo-cline.plusButtonClicked",
"group": "navigation@1",
"when": "view == roo-cline.SidebarProvider"
},
{
"command": "roo-cline.promptsButtonClicked",
"group": "navigation@2",
"when": "view == roo-cline.SidebarProvider"
},
{
"command": "roo-cline.mcpButtonClicked",
"group": "navigation@3",
"when": "view == roo-cline.SidebarProvider"
},
{
"command": "roo-cline.historyButtonClicked",
"group": "navigation@4",
"when": "view == roo-cline.SidebarProvider"
},
{
"command": "roo-cline.popoutButtonClicked",
"group": "navigation@5",
"when": "view == roo-cline.SidebarProvider"
},
{
"command": "roo-cline.settingsButtonClicked",
"group": "navigation@6",
"when": "view == roo-cline.SidebarProvider"
}
],
"editor/title": [
{
"command": "roo-cline.plusButtonClicked",
"group": "navigation@1",
"when": "activeWebviewPanelId == roo-cline.TabPanelProvider"
},
{
"command": "roo-cline.promptsButtonClicked",
"group": "navigation@2",
"when": "activeWebviewPanelId == roo-cline.TabPanelProvider"
},
{
"command": "roo-cline.mcpButtonClicked",
"group": "navigation@3",
"when": "activeWebviewPanelId == roo-cline.TabPanelProvider"
},
{
"command": "roo-cline.historyButtonClicked",
"group": "navigation@4",
"when": "activeWebviewPanelId == roo-cline.TabPanelProvider"
},
{
"command": "roo-cline.popoutButtonClicked",
"group": "navigation@5",
"when": "activeWebviewPanelId == roo-cline.TabPanelProvider"
},
{
"command": "roo-cline.settingsButtonClicked",
"group": "navigation@6",
"when": "activeWebviewPanelId == roo-cline.TabPanelProvider"
}
]
},
"submenus": [
{
"id": "roo-cline.contextMenu",
"label": "%views.contextMenu.label%"
},
{
"id": "roo-cline.terminalMenu",
"label": "%views.terminalMenu.label%"
}
],
"configuration": {
"title": "%configuration.title%",
"properties": {
"roo-cline.allowedCommands": {
"type": "array",
"items": {
"type": "string"
},
"default": [
"npm test",
"npm install",
"tsc",
"git log",
"git diff",
"git show"
],
"description": "%commands.allowedCommands.description%"
},
"roo-cline.vsCodeLmModelSelector": {
"type": "object",
"properties": {
"vendor": {
"type": "string",
"description": "%settings.vsCodeLmModelSelector.vendor.description%"
},
"family": {
"type": "string",
"description": "%settings.vsCodeLmModelSelector.family.description%"
}
},
"description": "%settings.vsCodeLmModelSelector.description%"
},
"roo-cline.customStoragePath": {
"type": "string",
"default": "",
"description": "%settings.customStoragePath.description%"
}
}
}
},
"scripts": {
"lint": "eslint **/*.ts",
"check-types": "tsc --noEmit",
"pretest": "pnpm bundle",
"test": "jest -w=40% && vitest run",
"format": "prettier --write .",
"bundle": "pnpm clean && pnpm --filter @roo-code/build build && node esbuild.mjs",
"build": "pnpm bundle --production && pnpm --filter @roo-code/vscode-webview build",
"build:development": "pnpm bundle && pnpm --filter @roo-code/vscode-webview build",
"publish:marketplace": "vsce publish --no-dependencies && ovsx publish --no-dependencies",
"publish": "pnpm vsix && changeset publish && pnpm install --package-lock-only",
"version-packages": "changeset version && pnpm install --package-lock-only",
"vscode:prepublish": "pnpm build",
"vsix": "mkdirp ../bin && npx vsce package --no-dependencies --out ../bin",
"watch:esbuild": "pnpm bundle --watch",
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
"generate-types": "tsx scripts/generate-types.mts",
"clean": "rimraf README.md CHANGELOG.md LICENSE dist webview-ui out .turbo"
},
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.10.2",
"@anthropic-ai/sdk": "^0.37.0",
"@anthropic-ai/vertex-sdk": "^0.7.0",
"@aws-sdk/client-bedrock-runtime": "^3.779.0",
"@aws-sdk/credential-providers": "^3.806.0",
"@google/genai": "^0.13.0",
"@mistralai/mistralai": "^1.3.6",
"@modelcontextprotocol/sdk": "^1.9.0",
"@vscode/codicons": "^0.0.36",
"axios": "^1.7.4",
"cheerio": "^1.0.0",
"chokidar": "^4.0.1",
"clone-deep": "^4.0.1",
"default-shell": "^2.2.0",
"delay": "^6.0.0",
"diff": "^5.2.0",
"diff-match-patch": "^1.0.5",
"fast-deep-equal": "^3.1.3",
"fast-xml-parser": "^4.5.1",
"fastest-levenshtein": "^1.0.16",
"fzf": "^0.5.2",
"get-folder-size": "^5.0.0",
"google-auth-library": "^9.15.1",
"i18next": "^24.2.2",
"ignore": "^7.0.3",
"isbinaryfile": "^5.0.2",
"mammoth": "^1.8.0",
"monaco-vscode-textmate-theme-converter": "^0.1.7",
"node-cache": "^5.1.2",
"node-ipc": "^12.0.0",
"openai": "^4.78.1",
"os-name": "^6.0.0",
"p-wait-for": "^5.0.2",
"pdf-parse": "^1.1.1",
"pkce-challenge": "^4.1.0",
"posthog-node": "^4.7.0",
"pretty-bytes": "^6.1.1",
"ps-tree": "^1.2.0",
"puppeteer-chromium-resolver": "^23.0.0",
"puppeteer-core": "^23.4.0",
"reconnecting-eventsource": "^1.6.4",
"sanitize-filename": "^1.6.3",
"say": "^0.16.0",
"serialize-error": "^11.0.3",
"simple-git": "^3.27.0",
"sound-play": "^1.1.0",
"string-similarity": "^4.0.4",
"strip-ansi": "^7.1.0",
"strip-bom": "^5.0.0",
"tiktoken": "^1.0.21",
"tmp": "^0.2.3",
"tree-sitter-wasms": "^0.1.11",
"turndown": "^7.2.0",
"vscode-material-icons": "^0.1.1",
"web-tree-sitter": "^0.22.6",
"workerpool": "^9.2.0",
"yaml": "^2.8.0",
"zod": "^3.24.2"
},
"devDependencies": {
"@changesets/cli": "^2.27.10",
"@changesets/types": "^6.0.0",
"@jest/globals": "^29.7.0",
"@roo-code/build": "workspace:^",
"@types/clone-deep": "^4.0.4",
"@types/debug": "^4.1.12",
"@types/diff": "^5.2.1",
"@types/diff-match-patch": "^1.0.36",
"@types/glob": "^8.1.0",
"@types/jest": "^29.5.14",
"@types/mocha": "^10.0.10",
"@types/node": "20.x",
"@types/node-cache": "^4.1.3",
"@types/node-ipc": "^9.2.3",
"@types/ps-tree": "^1.1.6",
"@types/string-similarity": "^4.0.2",
"@types/tmp": "^0.2.6",
"@types/turndown": "^5.0.5",
"@types/vscode": "^1.84.0",
"@typescript-eslint/eslint-plugin": "^7.14.1",
"@typescript-eslint/parser": "^7.11.0",
"@vscode/test-electron": "^2.5.2",
"@vscode/vsce": "3.3.2",
"esbuild": "^0.25.0",
"eslint": "^8.57.0",
"execa": "^9.5.2",
"glob": "^11.0.1",
"jest": "^29.7.0",
"jest-simple-dot-reporter": "^1.0.5",
"mkdirp": "^3.0.1",
"nock": "^14.0.4",
"npm-run-all2": "^8.0.1",
"ovsx": "0.10.2",
"prettier": "^3.4.2",
"rimraf": "^6.0.1",
"ts-jest": "^29.2.5",
"tsup": "^8.4.0",
"tsx": "^4.19.3",
"typescript": "5.8.3",
"vitest": "^3.1.3",
"zod-to-ts": "^1.2.0"
}
}

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