merge conflicts resolved

This commit is contained in:
pashpashpash
2025-02-05 02:51:17 -08:00
22 changed files with 884 additions and 177 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Adding changesets for automating version bumping and release notes
+1
View File
@@ -21,6 +21,7 @@
- [ ] Changes are limited to a single feature, bugfix or chore (split larger changes into separate PRs)
- [ ] Tests are passing (`npm test`) and code is formatted and linted (`npm run format && npm run lint`)
- [ ] I have created a changeset using `npm run changeset` (required for user-facing changes)
- [ ] I have reviewed [contributor guidelines](https://github.com/cline/cline/blob/main/CONTRIBUTING.md)
### Screenshots
@@ -0,0 +1,103 @@
"""
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
import sys
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"
bracketed_version_pattern = f"## [{VERSION}]\n"
prev_version_pattern = f"## [{PREV_VERSION}]\n"
print(f"latest version: {VERSION}")
print(f"prev_version: {PREV_VERSION}")
# Try both unbracketed and bracketed version patterns
version_index = changelog_text.find(version_pattern)
if version_index == -1:
version_index = changelog_text.find(bracketed_version_pattern)
if version_index == -1:
# If version not found, add it at the top (after the first line)
first_newline = changelog_text.find('\n')
if first_newline == -1:
# If no newline found, just prepend
return f"## [{VERSION}]\n\n{changelog_text}"
return f"{changelog_text[:first_newline + 1]}## [{VERSION}]\n\n{changelog_text[first_newline + 1:]}"
else:
# Using bracketed version
version_pattern = bracketed_version_pattern
notes_start_index = version_index + 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")
# Ensure we have at least 2 lines before removing them
if len(changeset_lines) < 2:
print("Warning: Changeset content has fewer than 2 lines")
parsed_lines = "\n".join(changeset_lines)
else:
# 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:]
# Ensure version number is bracketed
updated_changelog = updated_changelog.replace(f"## {VERSION}", f"## [{VERSION}]")
return updated_changelog
try:
print(f"Reading changelog from: {CHANGELOG_PATH}")
with open(CHANGELOG_PATH, 'r') as f:
changelog_content = f.read()
print(f"Changelog content length: {len(changelog_content)} characters")
print("First 200 characters of changelog:")
print(changelog_content[:200])
print("----------------------------------------------------------------------------------")
new_changelog = overwrite_changelog_section(changelog_content, NEW_CONTENT)
print("New changelog content:")
print("----------------------------------------------------------------------------------")
print(new_changelog)
print("----------------------------------------------------------------------------------")
print(f"Writing updated changelog back to: {CHANGELOG_PATH}")
with open(CHANGELOG_PATH, 'w') as f:
f.write(new_changelog)
print(f"{CHANGELOG_PATH} updated successfully!")
except FileNotFoundError:
print(f"Error: Changelog file not found at {CHANGELOG_PATH}")
sys.exit(1)
except Exception as e:
print(f"Error updating changelog: {str(e)}")
print(f"Current working directory: {os.getcwd()}")
sys.exit(1)
+162
View File
@@ -0,0 +1,162 @@
name: Changeset Release
run-name: Changeset Release ${{ github.actor != 'cline-bot' && '- Create PR' || '- Update Changelog' }}
permissions:
contents: write
pull-requests: write
on:
workflow_dispatch:
pull_request:
types: [closed, opened, labeled]
env:
REPO_PATH: ${{ github.repository }}
GIT_REF: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || 'main' }}
jobs:
# Job 1: Create version bump PR when changesets are merged to main
changeset-pr-version-bump:
if: >
( github.event_name == 'pull_request' &&
github.event.pull_request.merged == true &&
github.event.pull_request.base.ref == 'main' &&
github.actor != 'cline-bot' ) ||
github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Git Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
with:
fetch-depth: 0
ref: ${{ env.GIT_REF }}
- name: Setup Node.js
uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4
with:
node-version: 20
cache: "npm"
- name: Install Dependencies
run: npm run install:all
# Check if there are any new changesets to process
- name: Check for changesets
id: check-changesets
run: |
NEW_CHANGESETS=$(find .changeset -name "*.md" ! -name "README.md" | wc -l | tr -d ' ')
echo "Changesets diff with previous version: $NEW_CHANGESETS"
echo "new_changesets=$NEW_CHANGESETS" >> $GITHUB_OUTPUT
# Create version bump PR using changesets/action if there are new changesets
- name: Changeset Pull Request
if: steps.check-changesets.outputs.new_changesets != '0'
id: changesets
uses: changesets/action@e9cc34b540dd3ad1b030c57fd97269e8f6ad905a # v1
with:
commit: "changeset version bump"
title: "Changeset version bump"
version: npm run version-packages # This performs the changeset version bump
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Job 2: Process version bump PR created by cline-bot
changeset-pr-edit-approve:
name: Auto approve and merge Bump version PRs
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
if: >
github.event_name == 'pull_request' &&
github.event.pull_request.base.ref == 'main' &&
github.actor == 'cline-bot' &&
contains(github.event.pull_request.title, 'Changeset version bump')
steps:
- name: Determine checkout ref
id: checkout-ref
run: |
echo "Event action: ${{ github.event.action }}"
echo "Actor: ${{ github.actor }}"
echo "Head ref: ${{ github.head_ref }}"
echo "PR SHA: ${{ github.event.pull_request.head.sha }}"
if [[ "${{ github.event.action }}" == "opened" && "${{ github.actor }}" == "cline-bot" ]]; then
echo "Using branch ref: ${{ github.head_ref }}"
echo "git_ref=${{ github.head_ref }}" >> $GITHUB_OUTPUT
else
echo "Using SHA ref: ${{ github.event.pull_request.head.sha }}"
echo "git_ref=${{ github.event.pull_request.head.sha }}" >> $GITHUB_OUTPUT
fi
- name: Checkout Repo
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0
ref: ${{ steps.checkout-ref.outputs.git_ref }}
# Get current and previous versions to edit changelog entry
- name: Get version
id: get_version
run: |
VERSION=$(git show HEAD:package.json | jq -r '.version')
echo "version=$VERSION" >> $GITHUB_OUTPUT
PREV_VERSION=$(git show origin/main:package.json | jq -r '.version')
echo "prev_version=$PREV_VERSION" >> $GITHUB_OUTPUT
echo "version=$VERSION"
echo "prev_version=$PREV_VERSION"
# Update CHANGELOG.md with proper format
- name: Update Changelog Format
if: ${{ !contains(github.event.pull_request.labels.*.name, 'changelog-ready') }}
env:
VERSION: ${{ steps.get_version.outputs.version }}
PREV_VERSION: ${{ steps.get_version.outputs.prev_version }}
run: python .github/scripts/overwrite_changeset_changelog.py
# Commit and push changelog updates
- name: Push Changelog updates
if: ${{ !contains(github.event.pull_request.labels.*.name, 'changelog-ready') }}
run: |
git config user.name "cline-bot"
git config user.email github-actions@github.com
echo "Running git add and commit..."
git add CHANGELOG.md
git commit -m "Updating CHANGELOG.md format"
git status
echo "--------------------------------------------------------------------------------"
echo "Pushing to remote..."
echo "--------------------------------------------------------------------------------"
git push
# Add label to indicate changelog has been formatted
- name: Add changelog-ready label
if: ${{ !contains(github.event.pull_request.labels.*.name, 'changelog-ready') }}
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ['changelog-ready']
});
# Auto-approve PR only after it has been labeled
- name: Auto approve PR
if: contains(github.event.pull_request.labels.*.name, 'changelog-ready')
uses: hmarr/auto-approve-action@de8bf34d0402c38aa2c8346973342b2cb02c4435 # v4
with:
review-message: "I'm approving since it's a bump version PR"
# Auto-merge PR
- name: Automerge on PR
if: false # Needs enablePullRequestAutoMerge in repo settings to work contains(github.event.pull_request.labels.*.name, 'changelog-ready')
run: gh pr merge --auto --merge ${{ github.event.pull_request.number }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+119
View File
@@ -0,0 +1,119 @@
name: Check Changeset
run-name: Check for Changeset in PR
permissions:
contents: read
pull-requests: write
on:
pull_request:
branches:
- main
types: [opened, synchronize, reopened, ready_for_review]
jobs:
check-changeset:
# Skip draft PRs and dependabot PRs
if: github.event.pull_request.draft == false && github.actor != 'dependabot[bot]'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}
- name: Check for changeset
id: check-changeset
run: |
# Debug info
echo "Current directory: $(pwd)"
echo "PR Base Ref: ${{ github.event.pull_request.base.ref }}"
echo "PR Head Ref: ${{ github.event.pull_request.head.ref }}"
echo "PR Head SHA: ${{ github.event.pull_request.head.sha }}"
echo "Git status:"
git status
# Get list of changed files
git fetch origin ${{ github.event.pull_request.base.ref }}
CHANGED_FILES=$(git diff --name-only origin/${{ github.event.pull_request.base.ref }} HEAD)
echo "Changed files:"
echo "$CHANGED_FILES"
echo "Listing .changeset directory:"
ls -la .changeset/
# Check if any of the changed files are in docs/ or .github/
DOCS_ONLY=true
while IFS= read -r file; do
if [[ ! "$file" =~ ^(docs/|.github/) ]]; then
DOCS_ONLY=false
break
fi
done <<< "$CHANGED_FILES"
# If changes are docs-only, skip changeset check
if [ "$DOCS_ONLY" = true ]; then
echo "Only documentation files were changed, skipping changeset check"
exit 0
fi
# Check if any changeset files are in the changed files
CHANGESET_IN_PR=false
while IFS= read -r file; do
if [[ "$file" =~ ^\.changeset/.*\.md$ && "$file" != ".changeset/README.md" ]]; then
echo "Found changeset file in PR: $file"
CHANGESET_IN_PR=true
break
fi
done <<< "$CHANGED_FILES"
if [ "$CHANGESET_IN_PR" = false ]; then
# Double check local changeset files as backup
CHANGESETS=$(find .changeset -name "*.md" ! -name "README.md" | wc -l | tr -d ' ')
echo "Number of local changesets: $CHANGESETS"
if [ "$CHANGESETS" -eq 0 ]; then
echo "::error::No changeset file found in PR changes or local directory. Please run 'npm run changeset' to create one."
exit 1
fi
fi
- name: Comment on PR
if: failure()
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
with:
script: |
const message = `This PR requires a changeset since it includes user-facing changes. Please:
1. Run \`npm run changeset\` locally
2. Choose the appropriate version bump:
- \`major\` for breaking changes (1.0.0 → 2.0.0)
- \`minor\` for new features (1.0.0 → 1.1.0)
- \`patch\` for bug fixes (1.0.0 → 1.0.1)
3. Write a clear description of your changes
4. Commit the generated changeset file
Note: Documentation-only changes do not require a changeset.`;
// Get existing comments
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number
});
// Check if we already commented
const botComment = comments.data.find(comment =>
comment.user.login === 'github-actions[bot]' &&
comment.body.includes('This PR requires a changeset')
);
if (!botComment) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: message
});
}
+13 -3
View File
@@ -56,20 +56,30 @@ Anyone can contribute code to Cline, but we ask that you follow these guidelines
- Update existing tests if your changes affect them
- Include both unit tests and integration tests where appropriate
4. **Commit Guidelines**
4. **Version Management with Changesets**
- Create a changeset for any user-facing changes using `npm run changeset`
- Choose the appropriate version bump:
- `major` for breaking changes (1.0.0 → 2.0.0)
- `minor` for new features (1.0.0 → 1.1.0)
- `patch` for bug fixes (1.0.0 → 1.0.1)
- Write clear, descriptive changeset messages that explain the impact
- Documentation-only changes don't require changesets
5. **Commit Guidelines**
- Write clear, descriptive commit messages
- Use conventional commit format (e.g., "feat:", "fix:", "docs:")
- Reference relevant issues in commits using #issue-number
5. **Before Submitting**
6. **Before Submitting**
- Rebase your branch on the latest main
- Ensure your branch builds successfully
- Double-check all tests are passing
- Review your changes for any debugging code or console logs
6. **Pull Request Description**
7. **Pull Request Description**
- Clearly describe what your changes do
- Include steps to test the changes
- List any breaking changes
+2 -2
View File
@@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2024 Cline Bot Inc.
Copyright 2025 Cline Bot Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -198,4 +198,4 @@
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
limitations under the License.
+1 -1
View File
@@ -187,4 +187,4 @@ To contribute to the project, start with our [Contributing Guide](CONTRIBUTING.m
## License
[Apache 2.0 © 2024 Cline Bot Inc.](./LICENSE)
[Apache 2.0 © 2025 Cline Bot Inc.](./LICENSE)
@@ -102,4 +102,4 @@ The **Problems** section in VS Code shows any errors or warnings in your code. Y
## Next Steps
After installing these tools, you'll be ready to start coding! Return to the [Getting Started with Cline for New Coders](getting-started-new-coders.md) guide to continue your journey.
After installing these tools, you'll be ready to start coding! Return to the [Getting Started with Cline for New Coders](../getting-started-new-coders/README.md) guide to continue your journey.
+2 -1
View File
@@ -197,7 +197,8 @@
"publish:marketplace": "vsce publish && ovsx publish",
"publish:marketplace:prerelease": "vsce publish --pre-release && ovsx publish --pre-release",
"prepare": "husky",
"changeset": "changeset"
"changeset": "changeset",
"version-packages": "changeset version"
},
"devDependencies": {
"@changesets/cli": "^2.27.12",
+3
View File
@@ -14,6 +14,7 @@ import { DeepSeekHandler } from "./providers/deepseek"
import { MistralHandler } from "./providers/mistral"
import { VsCodeLmHandler } from "./providers/vscode-lm"
import { ClineHandler } from "./providers/cline"
import { LiteLlmHandler } from "./providers/litellm"
export interface ApiHandler {
createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream
@@ -53,6 +54,8 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
return new VsCodeLmHandler(options)
case "cline":
return new ClineHandler(options)
case "litellm":
return new LiteLlmHandler(options)
default:
return new AnthropicHandler(options)
}
+60
View File
@@ -0,0 +1,60 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { ApiHandlerOptions, liteLlmDefaultModelId, liteLlmModelInfoSaneDefaults } from "../../shared/api"
import { ApiHandler } from ".."
import { ApiStream } from "../transform/stream"
import { convertToOpenAiMessages } from "../transform/openai-format"
export class LiteLlmHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: this.options.liteLlmBaseUrl || "http://localhost:4000",
apiKey: "not-needed",
})
}
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const formattedMessages = convertToOpenAiMessages(messages)
const systemMessage: OpenAI.Chat.ChatCompletionSystemMessageParam = {
role: "system",
content: systemPrompt,
}
const stream = await this.client.chat.completions.create({
model: this.options.liteLlmModelId || liteLlmDefaultModelId,
messages: [systemMessage, ...formattedMessages],
temperature: 0,
stream: true,
stream_options: { include_usage: true },
})
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
if (delta?.content) {
yield {
type: "text",
text: delta.content,
}
}
if (chunk.usage) {
yield {
type: "usage",
inputTokens: chunk.usage.prompt_tokens || 0,
outputTokens: chunk.usage.completion_tokens || 0,
}
}
}
}
getModel() {
return {
id: this.options.liteLlmModelId || liteLlmDefaultModelId,
info: liteLlmModelInfoSaneDefaults,
}
}
}
+1 -1
View File
@@ -21,7 +21,7 @@ export class MistralHandler implements ApiHandler {
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new Mistral({
serverURL: "https://codestral.mistral.ai",
serverURL: "https://api.mistral.ai",
apiKey: this.options.mistralApiKey,
})
}
+22 -5
View File
@@ -46,6 +46,7 @@ import { HistoryItem } from "../shared/HistoryItem"
import { ClineAskResponse, ClineCheckpointRestore } from "../shared/WebviewMessage"
import { calculateApiCost } from "../utils/cost"
import { fileExistsAtPath } from "../utils/fs"
import { LLMFileAccessController } from "../services/llm-access-control/LLMFileAccessController"
import { arePathsEqual, getReadablePath } from "../utils/path"
import { fixModelHtmlEscaping, removeInvalidChars } from "../utils/string"
import { AssistantMessageContent, parseAssistantMessage, ToolParamName, ToolUseName } from "./assistant-message"
@@ -81,6 +82,7 @@ export class Cline {
private chatSettings: ChatSettings
apiConversationHistory: Anthropic.MessageParam[] = []
clineMessages: ClineMessage[] = []
private llmAccessController: LLMFileAccessController
private askResponse?: ClineAskResponse
private askResponseText?: string
private askResponseImages?: string[]
@@ -124,6 +126,10 @@ export class Cline {
images?: string[],
historyItem?: HistoryItem,
) {
this.llmAccessController = new LLMFileAccessController(cwd)
this.llmAccessController.initialize().catch((error) => {
console.error("Failed to initialize LLMFileAccessController:", error)
})
this.providerRef = new WeakRef(provider)
this.api = buildApiHandler(apiConfiguration)
this.terminalManager = new TerminalManager()
@@ -749,6 +755,7 @@ export class Cline {
// if the extension process were killed, then on restart the clineMessages might not be empty, so we need to set it to [] when we create a new Cline client (otherwise webview would show stale messages from previous session)
this.clineMessages = []
this.apiConversationHistory = []
await this.providerRef.deref()?.postStateToWebview()
await this.say("text", task, images)
@@ -1051,6 +1058,7 @@ export class Cline {
this.terminalManager.disposeAll()
this.urlContentFetcher.closeBrowser()
this.browserSession.closeBrowser()
this.llmAccessController.dispose()
await this.diffViewProvider.revertChanges() // need to await for when we want to make sure directories/files are reverted before re-starting the task from a checkpoint
}
@@ -1195,6 +1203,14 @@ export class Cline {
return false
}
private formatErrorWithStatusCode(error: any): string {
const statusCode = error.status || error.statusCode || (error.response && error.response.status)
const message = error.message ?? JSON.stringify(serializeError(error), null, 2)
// Only prepend the statusCode if it's not already part of the message
return statusCode && !message.includes(statusCode.toString()) ? `${statusCode} - ${message}` : message
}
async *attemptApiRequest(previousApiReqIndex: number): ApiStream {
// Wait for MCP servers to be connected before generating system prompt
await pWaitFor(() => this.providerRef.deref()?.mcpHub?.isConnecting !== true, { timeout: 10_000 }).catch(() => {
@@ -1302,10 +1318,9 @@ export class Cline {
} else {
// request failed after retrying automatically once, ask user if they want to retry again
// note that this api_req_failed ask is unique in that we only present this option if the api hasn't streamed any content yet (ie it fails on the first chunk due), as it would allow them to hit a retry button. However if the api failed mid-stream, it could be in any arbitrary state where some tools may have executed, so that error is handled differently and requires cancelling the task entirely.
const { response } = await this.ask(
"api_req_failed",
error.message ?? JSON.stringify(serializeError(error), null, 2),
)
const errorMessage = this.formatErrorWithStatusCode(error)
const { response } = await this.ask("api_req_failed", errorMessage)
if (response !== "yesButtonClicked") {
// this will never happen since if noButtonClicked, we will clear current task, aborting this instance
throw new Error("API request failed")
@@ -3053,7 +3068,9 @@ export class Cline {
// abandoned happens when extension is no longer waiting for the cline instance to finish aborting (error is thrown here when any function in the for loop throws due to this.abort)
if (!this.abandoned) {
this.abortTask() // if the stream failed, there's various states the task could be in (i.e. could have streamed some tools the user may have executed), so we just resort to replicating a cancel task
await abortStream("streaming_failed", error.message ?? JSON.stringify(serializeError(error), null, 2))
const errorMessage = this.formatErrorWithStatusCode(error)
await abortStream("streaming_failed", errorMessage)
const history = await this.providerRef.deref()?.getTaskWithId(this.taskId)
if (history) {
await this.providerRef.deref()?.initClineWithHistoryItem(history.historyItem)
+18
View File
@@ -77,6 +77,8 @@ type GlobalStateKey =
| "previousModeApiProvider"
| "previousModeModelId"
| "previousModeModelInfo"
| "liteLlmBaseUrl"
| "liteLlmModelId"
export const GlobalFileNames = {
apiConversationHistory: "api_conversation_history.json",
@@ -448,6 +450,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
openRouterModelId,
openRouterModelInfo,
vsCodeLmModelSelector,
liteLlmBaseUrl,
liteLlmModelId,
} = message.apiConfiguration
await this.updateGlobalState("apiProvider", apiProvider)
await this.updateGlobalState("apiModelId", apiModelId)
@@ -476,6 +480,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.updateGlobalState("openRouterModelId", openRouterModelId)
await this.updateGlobalState("openRouterModelInfo", openRouterModelInfo)
await this.updateGlobalState("vsCodeLmModelSelector", vsCodeLmModelSelector)
await this.updateGlobalState("liteLlmBaseUrl", liteLlmBaseUrl)
await this.updateGlobalState("liteLlmModelId", liteLlmModelId)
if (this.cline) {
this.cline.api = buildApiHandler(message.apiConfiguration)
}
@@ -541,6 +547,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
case "lmstudio":
await this.updateGlobalState("previousModeModelId", apiConfiguration.lmStudioModelId)
break
case "litellm":
await this.updateGlobalState("previousModeModelId", apiConfiguration.liteLlmModelId)
break
}
// Restore the model used in previous mode
@@ -570,6 +579,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
case "lmstudio":
await this.updateGlobalState("lmStudioModelId", newModelId)
break
case "litellm":
await this.updateGlobalState("liteLlmModelId", newModelId)
break
}
if (this.cline) {
@@ -1392,6 +1404,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
browserSettings,
chatSettings,
vsCodeLmModelSelector,
liteLlmBaseUrl,
liteLlmModelId,
userInfo,
authToken,
previousModeApiProvider,
@@ -1432,6 +1446,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.getGlobalState("browserSettings") as Promise<BrowserSettings | undefined>,
this.getGlobalState("chatSettings") as Promise<ChatSettings | undefined>,
this.getGlobalState("vsCodeLmModelSelector") as Promise<vscode.LanguageModelChatSelector | undefined>,
this.getGlobalState("liteLlmBaseUrl") as Promise<string | undefined>,
this.getGlobalState("liteLlmModelId") as Promise<string | undefined>,
this.getGlobalState("userInfo") as Promise<UserInfo | undefined>,
this.getSecret("authToken") as Promise<string | undefined>,
this.getGlobalState("previousModeApiProvider") as Promise<ApiProvider | undefined>,
@@ -1484,6 +1500,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
openRouterModelInfo,
vsCodeLmModelSelector,
authToken,
liteLlmBaseUrl,
liteLlmModelId,
},
lastShownAnnouncementId,
customInstructions,
@@ -33,44 +33,44 @@ describe("LLMFileAccessController", () => {
describe("Default Patterns", () => {
// it("should block access to common ignored files", async () => {
// const results = await Promise.all([
// const results = [
// controller.validateAccess(".env"),
// controller.validateAccess(".git/config"),
// controller.validateAccess("node_modules/package.json"),
// ])
// ]
// results.forEach((result) => result.should.be.false())
// })
it("should allow access to regular files", async () => {
const results = await Promise.all([
const results = [
controller.validateAccess("src/index.ts"),
controller.validateAccess("README.md"),
controller.validateAccess("package.json"),
])
]
results.forEach((result) => result.should.be.true())
})
})
describe("Custom Patterns", () => {
it("should block access to custom ignored patterns", async () => {
const results = await Promise.all([
const results = [
controller.validateAccess("config.secret"),
controller.validateAccess("private/data.txt"),
controller.validateAccess("temp.json"),
controller.validateAccess("nested/deep/file.secret"),
controller.validateAccess("private/nested/deep/file.txt"),
])
]
results.forEach((result) => result.should.be.false())
})
it("should allow access to non-ignored files", async () => {
const results = await Promise.all([
const results = [
controller.validateAccess("public/data.txt"),
controller.validateAccess("config.json"),
controller.validateAccess("src/temp/file.ts"),
controller.validateAccess("nested/deep/file.txt"),
controller.validateAccess("not-private/data.txt"),
])
]
results.forEach((result) => result.should.be.true())
})
@@ -83,11 +83,11 @@ describe("LLMFileAccessController", () => {
controller = new LLMFileAccessController(tempDir)
await controller.initialize()
const results = await Promise.all([
const results = [
controller.validateAccess("data-123.json"), // Should be false (wildcard)
controller.validateAccess("data.json"), // Should be true (doesn't match pattern)
controller.validateAccess("script.tmp"), // Should be false (extension match)
])
]
results[0].should.be.false() // data-123.json
results[1].should.be.true() // data.json
@@ -112,9 +112,8 @@ describe("LLMFileAccessController", () => {
// )
// controller = new LLMFileAccessController(tempDir)
// await controller.initialize()
// const results = await Promise.all([
// const results = [
// // Basic negation
// controller.validateAccess("temp/file.txt"), // Should be false (in temp/)
// controller.validateAccess("temp/allowed/file.txt"), // Should be true (negated)
@@ -130,7 +129,7 @@ describe("LLMFileAccessController", () => {
// controller.validateAccess("assets/logo.png"), // Should be false (in assets/)
// controller.validateAccess("assets/public/logo.png"), // Should be true (negated and matches *.png)
// controller.validateAccess("assets/public/data.json"), // Should be true (in negated public/)
// ])
// ]
// results[0].should.be.false() // temp/file.txt
// results[1].should.be.true() // temp/allowed/file.txt
@@ -154,7 +153,7 @@ describe("LLMFileAccessController", () => {
controller = new LLMFileAccessController(tempDir)
await controller.initialize()
const result = await controller.validateAccess("test.secret")
const result = controller.validateAccess("test.secret")
result.should.be.false()
})
})
@@ -163,55 +162,55 @@ describe("LLMFileAccessController", () => {
it("should handle absolute paths and match ignore patterns", async () => {
// Test absolute path that should be allowed
const allowedPath = path.join(tempDir, "src/file.ts")
const allowedResult = await controller.validateAccess(allowedPath)
const allowedResult = controller.validateAccess(allowedPath)
allowedResult.should.be.true()
// Test absolute path that matches an ignore pattern (*.secret)
const ignoredPath = path.join(tempDir, "config.secret")
const ignoredResult = await controller.validateAccess(ignoredPath)
const ignoredResult = controller.validateAccess(ignoredPath)
ignoredResult.should.be.false()
// Test absolute path in ignored directory (private/)
const ignoredDirPath = path.join(tempDir, "private/data.txt")
const ignoredDirResult = await controller.validateAccess(ignoredDirPath)
const ignoredDirResult = controller.validateAccess(ignoredDirPath)
ignoredDirResult.should.be.false()
})
it("should handle relative paths and match ignore patterns", async () => {
// Test relative path that should be allowed
const allowedResult = await controller.validateAccess("./src/file.ts")
const allowedResult = controller.validateAccess("./src/file.ts")
allowedResult.should.be.true()
// Test relative path that matches an ignore pattern (*.secret)
const ignoredResult = await controller.validateAccess("./config.secret")
const ignoredResult = controller.validateAccess("./config.secret")
ignoredResult.should.be.false()
// Test relative path in ignored directory (private/)
const ignoredDirResult = await controller.validateAccess("./private/data.txt")
const ignoredDirResult = controller.validateAccess("./private/data.txt")
ignoredDirResult.should.be.false()
})
it("should normalize paths with backslashes", async () => {
const result = await controller.validateAccess("src\\file.ts")
const result = controller.validateAccess("src\\file.ts")
result.should.be.true()
})
it("should handle paths outside cwd", async () => {
// Create a path that points to parent directory of cwd
const outsidePath = path.join(path.dirname(tempDir), "outside.txt")
const result = await controller.validateAccess(outsidePath)
const result = controller.validateAccess(outsidePath)
// Should return false for security since path is outside cwd
result.should.be.false()
// Test with a deeply nested path outside cwd
const deepOutsidePath = path.join(path.dirname(tempDir), "deep", "nested", "outside.secret")
const deepResult = await controller.validateAccess(deepOutsidePath)
const deepResult = controller.validateAccess(deepOutsidePath)
deepResult.should.be.false()
// Test with a path that tries to escape using ../
const escapeAttemptPath = path.join(tempDir, "..", "escape-attempt.txt")
const escapeResult = await controller.validateAccess(escapeAttemptPath)
const escapeResult = controller.validateAccess(escapeAttemptPath)
escapeResult.should.be.false()
})
})
@@ -228,7 +227,7 @@ describe("LLMFileAccessController", () => {
describe("Error Handling", () => {
it("should handle invalid paths", async () => {
// Test with an invalid path containing null byte
const result = await controller.validateAccess("\0invalid")
const result = controller.validateAccess("\0invalid")
result.should.be.true()
})
@@ -240,7 +239,7 @@ describe("LLMFileAccessController", () => {
try {
const controller = new LLMFileAccessController(emptyDir)
await controller.initialize()
const result = await controller.validateAccess("file.txt")
const result = controller.validateAccess("file.txt")
result.should.be.true()
} finally {
await fs.rm(emptyDir, { recursive: true, force: true })
@@ -253,7 +252,7 @@ describe("LLMFileAccessController", () => {
controller = new LLMFileAccessController(tempDir)
await controller.initialize()
const result = await controller.validateAccess("regular-file.txt")
const result = controller.validateAccess("regular-file.txt")
result.should.be.true()
})
})
@@ -2,6 +2,7 @@ import path from "path"
import { fileExistsAtPath } from "../../utils/fs"
import fs from "fs/promises"
import ignore, { Ignore } from "ignore"
import * as vscode from "vscode"
/**
* Controls LLM access to files by enforcing ignore patterns.
@@ -11,6 +12,8 @@ import ignore, { Ignore } from "ignore"
export class LLMFileAccessController {
private cwd: string
private ignoreInstance: Ignore
private fileWatcher: vscode.FileSystemWatcher | null
private disposables: vscode.Disposable[] = []
/**
* Default patterns that are always ignored for security
@@ -20,19 +23,49 @@ export class LLMFileAccessController {
constructor(cwd: string) {
this.cwd = cwd
this.ignoreInstance = ignore()
// Add default patterns immediately
this.ignoreInstance.add(LLMFileAccessController.DEFAULT_PATTERNS)
this.fileWatcher = null
// Set up file watcher for .clineignore
this.setupFileWatcher()
}
/**
* Initialize the controller by loading custom patterns
* This must be called and awaited before using the controller
* Must be called after construction and before using the controller
*/
async initialize(): Promise<void> {
await this.loadCustomPatterns()
}
/**
* Set up the file watcher for .clineignore changes
*/
private setupFileWatcher(): void {
const clineignorePattern = new vscode.RelativePattern(this.cwd, ".clineignore")
this.fileWatcher = vscode.workspace.createFileSystemWatcher(clineignorePattern)
// Watch for changes and updates
this.disposables.push(
this.fileWatcher.onDidChange(() => {
this.loadCustomPatterns().catch((error) => {
console.error("Failed to load updated .clineignore patterns:", error)
})
}),
this.fileWatcher.onDidCreate(() => {
this.loadCustomPatterns().catch((error) => {
console.error("Failed to load new .clineignore patterns:", error)
})
}),
this.fileWatcher.onDidDelete(() => {
this.resetToDefaultPatterns()
}),
)
// Add fileWatcher itself to disposables
this.disposables.push(this.fileWatcher)
}
/**
* Load custom patterns from .clineignore if it exists
*/
@@ -40,6 +73,8 @@ export class LLMFileAccessController {
try {
const ignorePath = path.join(this.cwd, ".clineignore")
if (await fileExistsAtPath(ignorePath)) {
// Reset ignore instance to prevent duplicate patterns
this.resetToDefaultPatterns()
const content = await fs.readFile(ignorePath, "utf8")
const customPatterns = content
.split("\n")
@@ -49,11 +84,18 @@ export class LLMFileAccessController {
this.ignoreInstance.add(customPatterns)
}
} catch (error) {
console.error("Failed to load .clineignore:", error)
// Continue with default patterns
}
}
/**
* Reset ignore patterns to defaults
*/
private resetToDefaultPatterns(): void {
this.ignoreInstance = ignore()
this.ignoreInstance.add(LLMFileAccessController.DEFAULT_PATTERNS)
}
/**
* Check if a file should be accessible to the LLM
* @param filePath - Path to check (relative to cwd)
@@ -97,4 +139,13 @@ export class LLMFileAccessController {
return [] // Fail closed for security
}
}
/**
* Clean up resources when the controller is no longer needed
*/
dispose(): void {
this.disposables.forEach((d) => d.dispose())
this.disposables = []
this.fileWatcher = null
}
}
+87 -7
View File
@@ -12,12 +12,15 @@ export type ApiProvider =
| "mistral"
| "vscode-lm"
| "cline"
| "litellm"
export interface ApiHandlerOptions {
apiModelId?: string
apiKey?: string // anthropic
clineApiKey?: string
authToken?: string // firebase auth token for cline provider
liteLlmBaseUrl?: string
liteLlmModelId?: string
anthropicBaseUrl?: string
openRouterApiKey?: string
openRouterModelId?: string
@@ -355,16 +358,16 @@ export const openAiNativeModels = {
contextWindow: 128_000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 3,
outputPrice: 12,
inputPrice: 1.1,
outputPrice: 4.4,
},
"gpt-4o": {
maxTokens: 4_096,
contextWindow: 128_000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 5,
outputPrice: 15,
inputPrice: 2.5,
outputPrice: 10,
},
"gpt-4o-mini": {
maxTokens: 16_384,
@@ -411,10 +414,74 @@ export const deepSeekModels = {
// Mistral
// https://docs.mistral.ai/getting-started/models/models_overview/
export type MistralModelId = keyof typeof mistralModels
export const mistralDefaultModelId: MistralModelId = "codestral-latest"
export const mistralDefaultModelId: MistralModelId = "codestral-2501"
export const mistralModels = {
"codestral-latest": {
maxTokens: 32_768,
"mistral-large-2411": {
maxTokens: 131_000,
contextWindow: 131_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 2.0,
outputPrice: 6.0,
},
"pixtral-large-2411": {
maxTokens: 131_000,
contextWindow: 131_000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 2.0,
outputPrice: 6.0,
},
"ministral-3b-2410": {
maxTokens: 131_000,
contextWindow: 131_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.04,
outputPrice: 0.04,
},
"ministral-8b-2410": {
maxTokens: 131_000,
contextWindow: 131_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.1,
outputPrice: 0.1,
},
"mistral-small-2501": {
maxTokens: 32_000,
contextWindow: 32_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.1,
outputPrice: 0.3,
},
"pixtral-12b-2409": {
maxTokens: 131_000,
contextWindow: 131_000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0.15,
outputPrice: 0.15,
},
"open-mistral-nemo-2407": {
maxTokens: 131_000,
contextWindow: 131_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.15,
outputPrice: 0.15,
},
"open-codestral-mamba": {
maxTokens: 256_000,
contextWindow: 256_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.15,
outputPrice: 0.15,
},
"codestral-2501": {
maxTokens: 256_000,
contextWindow: 256_000,
supportsImages: false,
supportsPromptCache: false,
@@ -422,3 +489,16 @@ export const mistralModels = {
outputPrice: 0.9,
},
} as const satisfies Record<string, ModelInfo>
// LiteLLM
// https://docs.litellm.ai/docs/
export type LiteLLMModelId = string
export const liteLlmDefaultModelId = "gpt-3.5-turbo"
export const liteLlmModelInfoSaneDefaults: ModelInfo = {
maxTokens: 4096,
contextWindow: 8192,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
}
+123 -123
View File
@@ -36,7 +36,7 @@
"@types/react-dom": "^18.3.0",
"@types/vscode-webview": "^1.57.5",
"jsdom": "^25.0.1",
"vitest": "^2.1.8"
"vitest": "^2.1.9"
}
},
"node_modules/@adobe/css-tools": {
@@ -3860,9 +3860,9 @@
"license": "MIT"
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.32.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.32.1.tgz",
"integrity": "sha512-/pqA4DmqyCm8u5YIDzIdlLcEmuvxb0v8fZdFhVMszSpDTgbQKdw3/mB3eMUHIbubtJ6F9j+LtmyCnHTEqIHyzA==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.2.tgz",
"integrity": "sha512-6Fyg9yQbwJR+ykVdT9sid1oc2ewejS6h4wzQltmJfSW53N60G/ah9pngXGANdy9/aaE/TcUFpWosdm7JXS1WTQ==",
"cpu": [
"arm"
],
@@ -3874,9 +3874,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.32.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.32.1.tgz",
"integrity": "sha512-If3PDskT77q7zgqVqYuj7WG3WC08G1kwXGVFi9Jr8nY6eHucREHkfpX79c0ACAjLj3QIWKPJR7w4i+f5EdLH5Q==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.2.tgz",
"integrity": "sha512-K5GfWe+vtQ3kyEbihrimM38UgX57UqHp+oME7X/EX9Im6suwZfa7Hsr8AtzbJvukTpwMGs+4s29YMSO3rwWtsw==",
"cpu": [
"arm64"
],
@@ -3888,9 +3888,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.32.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.32.1.tgz",
"integrity": "sha512-zCpKHioQ9KgZToFp5Wvz6zaWbMzYQ2LJHQ+QixDKq52KKrF65ueu6Af4hLlLWHjX1Wf/0G5kSJM9PySW9IrvHA==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.2.tgz",
"integrity": "sha512-PSN58XG/V/tzqDb9kDGutUruycgylMlUE59f40ny6QIRNsTEIZsrNQTJKUN2keMMSmlzgunMFqyaGLmly39sug==",
"cpu": [
"arm64"
],
@@ -3902,9 +3902,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.32.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.32.1.tgz",
"integrity": "sha512-sFvF+t2+TyUo/ZQqUcifrJIgznx58oFZbdHS9TvHq3xhPVL9nOp+yZ6LKrO9GWTP+6DbFtoyLDbjTpR62Mbr3Q==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.2.tgz",
"integrity": "sha512-gQhK788rQJm9pzmXyfBB84VHViDERhAhzGafw+E5mUpnGKuxZGkMVDa3wgDFKT6ukLC5V7QTifzsUKdNVxp5qQ==",
"cpu": [
"x64"
],
@@ -3916,9 +3916,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.32.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.32.1.tgz",
"integrity": "sha512-NbOa+7InvMWRcY9RG+B6kKIMD/FsnQPH0MWUvDlQB1iXnF/UcKSudCXZtv4lW+C276g3w5AxPbfry5rSYvyeYA==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.2.tgz",
"integrity": "sha512-eiaHgQwGPpxLC3+zTAcdKl4VsBl3r0AiJOd1Um/ArEzAjN/dbPK1nROHrVkdnoE6p7Svvn04w3f/jEZSTVHunA==",
"cpu": [
"arm64"
],
@@ -3930,9 +3930,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.32.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.32.1.tgz",
"integrity": "sha512-JRBRmwvHPXR881j2xjry8HZ86wIPK2CcDw0EXchE1UgU0ubWp9nvlT7cZYKc6bkypBt745b4bglf3+xJ7hXWWw==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.2.tgz",
"integrity": "sha512-lhdiwQ+jf8pewYOTG4bag0Qd68Jn1v2gO1i0mTuiD+Qkt5vNfHVK/jrT7uVvycV8ZchlzXp5HDVmhpzjC6mh0g==",
"cpu": [
"x64"
],
@@ -3944,9 +3944,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.32.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.32.1.tgz",
"integrity": "sha512-PKvszb+9o/vVdUzCCjL0sKHukEQV39tD3fepXxYrHE3sTKrRdCydI7uldRLbjLmDA3TFDmh418XH19NOsDRH8g==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.2.tgz",
"integrity": "sha512-lfqTpWjSvbgQP1vqGTXdv+/kxIznKXZlI109WkIFPbud41bjigjNmOAAKoazmRGx+k9e3rtIdbq2pQZPV1pMig==",
"cpu": [
"arm"
],
@@ -3958,9 +3958,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.32.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.32.1.tgz",
"integrity": "sha512-9WHEMV6Y89eL606ReYowXuGF1Yb2vwfKWKdD1A5h+OYnPZSJvxbEjxTRKPgi7tkP2DSnW0YLab1ooy+i/FQp/Q==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.2.tgz",
"integrity": "sha512-RGjqULqIurqqv+NJTyuPgdZhka8ImMLB32YwUle2BPTDqDoXNgwFjdjQC59FbSk08z0IqlRJjrJ0AvDQ5W5lpw==",
"cpu": [
"arm"
],
@@ -3972,9 +3972,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.32.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.32.1.tgz",
"integrity": "sha512-tZWc9iEt5fGJ1CL2LRPw8OttkCBDs+D8D3oEM8mH8S1ICZCtFJhD7DZ3XMGM8kpqHvhGUTvNUYVDnmkj4BDXnw==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.2.tgz",
"integrity": "sha512-ZvkPiheyXtXlFqHpsdgscx+tZ7hoR59vOettvArinEspq5fxSDSgfF+L5wqqJ9R4t+n53nyn0sKxeXlik7AY9Q==",
"cpu": [
"arm64"
],
@@ -3986,9 +3986,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.32.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.32.1.tgz",
"integrity": "sha512-FTYc2YoTWUsBz5GTTgGkRYYJ5NGJIi/rCY4oK/I8aKowx1ToXeoVVbIE4LGAjsauvlhjfl0MYacxClLld1VrOw==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.2.tgz",
"integrity": "sha512-UlFk+E46TZEoxD9ufLKDBzfSG7Ki03fo6hsNRRRHF+KuvNZ5vd1RRVQm8YZlGsjcJG8R252XFK0xNPay+4WV7w==",
"cpu": [
"arm64"
],
@@ -4000,9 +4000,9 @@
]
},
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
"version": "4.32.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.32.1.tgz",
"integrity": "sha512-F51qLdOtpS6P1zJVRzYM0v6MrBNypyPEN1GfMiz0gPu9jN8ScGaEFIZQwteSsGKg799oR5EaP7+B2jHgL+d+Kw==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.2.tgz",
"integrity": "sha512-hJhfsD9ykx59jZuuoQgYT1GEcNNi3RCoEmbo5OGfG8RlHOiVS7iVNev9rhLKh7UBYq409f4uEw0cclTXx8nh8Q==",
"cpu": [
"loong64"
],
@@ -4014,9 +4014,9 @@
]
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.32.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.32.1.tgz",
"integrity": "sha512-wO0WkfSppfX4YFm5KhdCCpnpGbtgQNj/tgvYzrVYFKDpven8w2N6Gg5nB6w+wAMO3AIfSTWeTjfVe+uZ23zAlg==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.2.tgz",
"integrity": "sha512-g/O5IpgtrQqPegvqopvmdCF9vneLE7eqYfdPWW8yjPS8f63DNam3U4ARL1PNNB64XHZDHKpvO2Giftf43puB8Q==",
"cpu": [
"ppc64"
],
@@ -4028,9 +4028,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.32.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.32.1.tgz",
"integrity": "sha512-iWswS9cIXfJO1MFYtI/4jjlrGb/V58oMu4dYJIKnR5UIwbkzR0PJ09O0PDZT0oJ3LYWXBSWahNf/Mjo6i1E5/g==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.2.tgz",
"integrity": "sha512-bSQijDC96M6PuooOuXHpvXUYiIwsnDmqGU8+br2U7iPoykNi9JtMUpN7K6xml29e0evK0/g0D1qbAUzWZFHY5Q==",
"cpu": [
"riscv64"
],
@@ -4042,9 +4042,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.32.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.32.1.tgz",
"integrity": "sha512-RKt8NI9tebzmEthMnfVgG3i/XeECkMPS+ibVZjZ6mNekpbbUmkNWuIN2yHsb/mBPyZke4nlI4YqIdFPgKuoyQQ==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.2.tgz",
"integrity": "sha512-49TtdeVAsdRuiUHXPrFVucaP4SivazetGUVH8CIxVsNsaPHV4PFkpLmH9LeqU/R4Nbgky9lzX5Xe1NrzLyraVA==",
"cpu": [
"s390x"
],
@@ -4056,9 +4056,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.32.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.32.1.tgz",
"integrity": "sha512-WQFLZ9c42ECqEjwg/GHHsouij3pzLXkFdz0UxHa/0OM12LzvX7DzedlY0SIEly2v18YZLRhCRoHZDxbBSWoGYg==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.2.tgz",
"integrity": "sha512-j+jFdfOycLIQ7FWKka9Zd3qvsIyugg5LeZuHF6kFlXo6MSOc6R1w37YUVy8VpAKd81LMWGi5g9J25P09M0SSIw==",
"cpu": [
"x64"
],
@@ -4070,9 +4070,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.32.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.32.1.tgz",
"integrity": "sha512-BLoiyHDOWoS3uccNSADMza6V6vCNiphi94tQlVIL5de+r6r/CCQuNnerf+1g2mnk2b6edp5dk0nhdZ7aEjOBsA==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.2.tgz",
"integrity": "sha512-aDPHyM/D2SpXfSNCVWCxyHmOqN9qb7SWkY1+vaXqMNMXslZYnwh9V/UCudl6psyG0v6Ukj7pXanIpfZwCOEMUg==",
"cpu": [
"x64"
],
@@ -4084,9 +4084,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.32.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.32.1.tgz",
"integrity": "sha512-w2l3UnlgYTNNU+Z6wOR8YdaioqfEnwPjIsJ66KxKAf0p+AuL2FHeTX6qvM+p/Ue3XPBVNyVSfCrfZiQh7vZHLQ==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.2.tgz",
"integrity": "sha512-LQRkCyUBnAo7r8dbEdtNU08EKLCJMgAk2oP5H3R7BnUlKLqgR3dUjrLBVirmc1RK6U6qhtDw29Dimeer8d5hzQ==",
"cpu": [
"arm64"
],
@@ -4098,9 +4098,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.32.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.32.1.tgz",
"integrity": "sha512-Am9H+TGLomPGkBnaPWie4F3x+yQ2rr4Bk2jpwy+iV+Gel9jLAu/KqT8k3X4jxFPW6Zf8OMnehyutsd+eHoq1WQ==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.2.tgz",
"integrity": "sha512-wt8OhpQUi6JuPFkm1wbVi1BByeag87LDFzeKSXzIdGcX4bMLqORTtKxLoCbV57BHYNSUSOKlSL4BYYUghainYA==",
"cpu": [
"ia32"
],
@@ -4112,9 +4112,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.32.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.32.1.tgz",
"integrity": "sha512-ar80GhdZb4DgmW3myIS9nRFYcpJRSME8iqWgzH2i44u+IdrzmiXVxeFnExQ5v4JYUSpg94bWjevMG8JHf1Da5Q==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.2.tgz",
"integrity": "sha512-rUrqINax0TvrPBXrFKg0YbQx18NpPN3NNrgmaao9xRNbTwek7lOXObhx8tQy8gelmQ/gLaGy1WptpU2eKJZImg==",
"cpu": [
"x64"
],
@@ -5209,14 +5209,14 @@
"license": "ISC"
},
"node_modules/@vitest/expect": {
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.8.tgz",
"integrity": "sha512-8ytZ/fFHq2g4PJVAtDX57mayemKgDR6X3Oa2Foro+EygiOJHUXhCqBAAKQYYajZpFoIfvBCF1j6R6IYRSIUFuw==",
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz",
"integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/spy": "2.1.8",
"@vitest/utils": "2.1.8",
"@vitest/spy": "2.1.9",
"@vitest/utils": "2.1.9",
"chai": "^5.1.2",
"tinyrainbow": "^1.2.0"
},
@@ -5225,13 +5225,13 @@
}
},
"node_modules/@vitest/mocker": {
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.8.tgz",
"integrity": "sha512-7guJ/47I6uqfttp33mgo6ga5Gr1VnL58rcqYKyShoRK9ebu8T5Rs6HN3s1NABiBeVTdWNrwUMcHH54uXZBN4zA==",
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz",
"integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/spy": "2.1.8",
"@vitest/spy": "2.1.9",
"estree-walker": "^3.0.3",
"magic-string": "^0.30.12"
},
@@ -5272,9 +5272,9 @@
}
},
"node_modules/@vitest/pretty-format": {
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.8.tgz",
"integrity": "sha512-9HiSZ9zpqNLKlbIDRWOnAWqgcA7xu+8YxXSekhr0Ykab7PAYFkhkwoqVArPOtJhPmYeE2YHgKZlj3CP36z2AJQ==",
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz",
"integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5285,13 +5285,13 @@
}
},
"node_modules/@vitest/runner": {
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.8.tgz",
"integrity": "sha512-17ub8vQstRnRlIU5k50bG+QOMLHRhYPAna5tw8tYbj+jzjcspnwnwtPtiOlkuKC4+ixDPTuLZiqiWWQ2PSXHVg==",
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz",
"integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/utils": "2.1.8",
"@vitest/utils": "2.1.9",
"pathe": "^1.1.2"
},
"funding": {
@@ -5299,13 +5299,13 @@
}
},
"node_modules/@vitest/snapshot": {
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.8.tgz",
"integrity": "sha512-20T7xRFbmnkfcmgVEz+z3AU/3b0cEzZOt/zmnvZEctg64/QZbSDJEVm9fLnnlSi74KibmRsO9/Qabi+t0vCRPg==",
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz",
"integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "2.1.8",
"@vitest/pretty-format": "2.1.9",
"magic-string": "^0.30.12",
"pathe": "^1.1.2"
},
@@ -5324,9 +5324,9 @@
}
},
"node_modules/@vitest/spy": {
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.8.tgz",
"integrity": "sha512-5swjf2q95gXeYPevtW0BLk6H8+bPlMb4Vw/9Em4hFxDcaOxS+e0LOX4yqNxoHzMR2akEB2xfpnWUzkZokmgWDg==",
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz",
"integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5337,13 +5337,13 @@
}
},
"node_modules/@vitest/utils": {
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.8.tgz",
"integrity": "sha512-dwSoui6djdwbfFmIgbIjX2ZhIoG7Ex/+xpxyiEgIGzjliY8xGkcpITKTlp6B4MgtGkF2ilvm97cPM96XZaAgcA==",
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz",
"integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "2.1.8",
"@vitest/pretty-format": "2.1.9",
"loupe": "^3.1.2",
"tinyrainbow": "^1.2.0"
},
@@ -19405,9 +19405,9 @@
}
},
"node_modules/vite-node": {
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.8.tgz",
"integrity": "sha512-uPAwSr57kYjAUux+8E2j0q0Fxpn8M9VoyfGiRI8Kfktz9NcYMCenwY5RnZxnF1WTu3TGiYipirIzacLL3VVGFg==",
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz",
"integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -19457,9 +19457,9 @@
}
},
"node_modules/vite/node_modules/rollup": {
"version": "4.32.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.32.1.tgz",
"integrity": "sha512-z+aeEsOeEa3mEbS1Tjl6sAZ8NE3+AalQz1RJGj81M+fizusbdDMoEJwdJNHfaB40Scr4qNu+welOfes7maKonA==",
"version": "4.34.2",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.2.tgz",
"integrity": "sha512-sBDUoxZEaqLu9QeNalL8v3jw6WjPku4wfZGyTU7l7m1oC+rpRihXc/n/H+4148ZkGz5Xli8CHMns//fFGKvpIQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -19473,42 +19473,42 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.32.1",
"@rollup/rollup-android-arm64": "4.32.1",
"@rollup/rollup-darwin-arm64": "4.32.1",
"@rollup/rollup-darwin-x64": "4.32.1",
"@rollup/rollup-freebsd-arm64": "4.32.1",
"@rollup/rollup-freebsd-x64": "4.32.1",
"@rollup/rollup-linux-arm-gnueabihf": "4.32.1",
"@rollup/rollup-linux-arm-musleabihf": "4.32.1",
"@rollup/rollup-linux-arm64-gnu": "4.32.1",
"@rollup/rollup-linux-arm64-musl": "4.32.1",
"@rollup/rollup-linux-loongarch64-gnu": "4.32.1",
"@rollup/rollup-linux-powerpc64le-gnu": "4.32.1",
"@rollup/rollup-linux-riscv64-gnu": "4.32.1",
"@rollup/rollup-linux-s390x-gnu": "4.32.1",
"@rollup/rollup-linux-x64-gnu": "4.32.1",
"@rollup/rollup-linux-x64-musl": "4.32.1",
"@rollup/rollup-win32-arm64-msvc": "4.32.1",
"@rollup/rollup-win32-ia32-msvc": "4.32.1",
"@rollup/rollup-win32-x64-msvc": "4.32.1",
"@rollup/rollup-android-arm-eabi": "4.34.2",
"@rollup/rollup-android-arm64": "4.34.2",
"@rollup/rollup-darwin-arm64": "4.34.2",
"@rollup/rollup-darwin-x64": "4.34.2",
"@rollup/rollup-freebsd-arm64": "4.34.2",
"@rollup/rollup-freebsd-x64": "4.34.2",
"@rollup/rollup-linux-arm-gnueabihf": "4.34.2",
"@rollup/rollup-linux-arm-musleabihf": "4.34.2",
"@rollup/rollup-linux-arm64-gnu": "4.34.2",
"@rollup/rollup-linux-arm64-musl": "4.34.2",
"@rollup/rollup-linux-loongarch64-gnu": "4.34.2",
"@rollup/rollup-linux-powerpc64le-gnu": "4.34.2",
"@rollup/rollup-linux-riscv64-gnu": "4.34.2",
"@rollup/rollup-linux-s390x-gnu": "4.34.2",
"@rollup/rollup-linux-x64-gnu": "4.34.2",
"@rollup/rollup-linux-x64-musl": "4.34.2",
"@rollup/rollup-win32-arm64-msvc": "4.34.2",
"@rollup/rollup-win32-ia32-msvc": "4.34.2",
"@rollup/rollup-win32-x64-msvc": "4.34.2",
"fsevents": "~2.3.2"
}
},
"node_modules/vitest": {
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.8.tgz",
"integrity": "sha512-1vBKTZskHw/aosXqQUlVWWlGUxSJR8YtiyZDJAFeW2kPAeX6S3Sool0mjspO+kXLuxVWlEDDowBAeqeAQefqLQ==",
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz",
"integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/expect": "2.1.8",
"@vitest/mocker": "2.1.8",
"@vitest/pretty-format": "^2.1.8",
"@vitest/runner": "2.1.8",
"@vitest/snapshot": "2.1.8",
"@vitest/spy": "2.1.8",
"@vitest/utils": "2.1.8",
"@vitest/expect": "2.1.9",
"@vitest/mocker": "2.1.9",
"@vitest/pretty-format": "^2.1.9",
"@vitest/runner": "2.1.9",
"@vitest/snapshot": "2.1.9",
"@vitest/spy": "2.1.9",
"@vitest/utils": "2.1.9",
"chai": "^5.1.2",
"debug": "^4.3.7",
"expect-type": "^1.1.0",
@@ -19520,7 +19520,7 @@
"tinypool": "^1.0.1",
"tinyrainbow": "^1.2.0",
"vite": "^5.0.0",
"vite-node": "2.1.8",
"vite-node": "2.1.9",
"why-is-node-running": "^2.3.0"
},
"bin": {
@@ -19535,8 +19535,8 @@
"peerDependencies": {
"@edge-runtime/vm": "*",
"@types/node": "^18.0.0 || >=20.0.0",
"@vitest/browser": "2.1.8",
"@vitest/ui": "2.1.8",
"@vitest/browser": "2.1.9",
"@vitest/ui": "2.1.9",
"happy-dom": "*",
"jsdom": "*"
},
+1 -1
View File
@@ -59,6 +59,6 @@
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"jsdom": "^25.0.1",
"vitest": "^2.1.8"
"vitest": "^2.1.9"
}
}
@@ -50,6 +50,44 @@ const remarkUrlToLink = () => {
}
}
/**
* Custom remark plugin that prevents filenames with extensions from being parsed as bold text
* For example: __init__.py should not be rendered as bold "init" followed by ".py"
* Solves https://github.com/cline/cline/issues/1028
*/
const remarkPreventBoldFilenames = () => {
return (tree: any) => {
visit(tree, "strong", (node: any, index: number | undefined, parent: any) => {
// Only process if there's a next node (potential file extension)
if (!parent || typeof index === "undefined" || index === parent.children.length - 1) return
const nextNode = parent.children[index + 1]
// Check if next node is text and starts with . followed by extension
if (nextNode.type !== "text" || !nextNode.value.match(/^\.[a-zA-Z0-9]+/)) return
// If the strong node has multiple children, something weird is happening
if (node.children?.length !== 1) return
// Get the text content from inside the strong node
const strongContent = node.children?.[0]?.value
if (!strongContent || typeof strongContent !== "string") return
// Validate that the strong content is a valid filename
if (!strongContent.match(/^[a-zA-Z0-9_-]+$/)) return
// Combine into a single text node
const newNode = {
type: "text",
value: `__${strongContent}__${nextNode.value}`,
}
// Replace both nodes with the combined text node
parent.children.splice(index, 2, newNode)
})
}
}
const StyledMarkdown = styled.div`
pre {
background-color: ${CODE_BLOCK_BG_COLOR};
@@ -160,6 +198,7 @@ const MarkdownBlock = memo(({ markdown }: MarkdownBlockProps) => {
const { theme } = useExtensionState()
const [reactContent, setMarkdown] = useRemark({
remarkPlugins: [
remarkPreventBoldFilenames,
remarkUrlToLink,
() => {
return (tree) => {
@@ -138,7 +138,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
VSCodeDropdown has an open bug where dynamically rendered options don't auto select the provided value prop. You can see this for yourself by comparing it with normal select/option elements, which work as expected.
https://github.com/microsoft/vscode-webview-ui-toolkit/issues/433
In our case, when the user switches between providers, we recalculate the selectedModelId depending on the provider, the default model for that provider, and a modelId that the user may have selected. Unfortunately, the VSCodeDropdown component wouldn't select this calculated value, and would default to the first "Select a model..." option instead, which makes it seem like the model was cleared out when it wasn't.
In our case, when the user switches between providers, we recalculate the selectedModelId depending on the provider, the default model for that provider, and a modelId that the user may have selected. Unfortunately, the VSCodeDropdown component wouldn't select this calculated value, and would default to the first "Select a model..." option instead, which makes it seem like the model was cleared out when it wasn't.
As a workaround, we create separate instances of the dropdown for each provider, and then conditionally render the one that matches the current provider.
*/
@@ -193,6 +193,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
<VSCodeOption value="vscode-lm">VS Code LM API</VSCodeOption>
<VSCodeOption value="lmstudio">LM Studio</VSCodeOption>
<VSCodeOption value="ollama">Ollama</VSCodeOption>
<VSCodeOption value="litellm">LiteLLM</VSCodeOption>
</VSCodeDropdown>
</DropdownContainer>
@@ -779,6 +780,38 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
</div>
)}
{selectedProvider === "litellm" && (
<div>
<VSCodeTextField
value={apiConfiguration?.liteLlmBaseUrl || ""}
style={{ width: "100%" }}
type="url"
onInput={handleInputChange("liteLlmBaseUrl")}
placeholder={"Default: http://localhost:4000"}>
<span style={{ fontWeight: 500 }}>Base URL (optional)</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.liteLlmModelId || ""}
style={{ width: "100%" }}
onInput={handleInputChange("liteLlmModelId")}
placeholder={"e.g. gpt-4"}>
<span style={{ fontWeight: 500 }}>Model ID</span>
</VSCodeTextField>
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
LiteLLM provides a unified interface to access various LLM providers' models. See their{" "}
<VSCodeLink href="https://docs.litellm.ai/docs/" style={{ display: "inline", fontSize: "inherit" }}>
quickstart guide
</VSCodeLink>{" "}
for more information.
</p>
</div>
)}
{selectedProvider === "ollama" && (
<div>
<VSCodeTextField
@@ -1121,6 +1154,12 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration):
supportsImages: false, // VSCode LM API currently doesn't support images
},
}
case "litellm":
return {
selectedProvider: provider,
selectedModelId: apiConfiguration?.liteLlmModelId || "",
selectedModelInfo: openAiModelInfoSaneDefaults,
}
default:
return getProviderData(anthropicModels, anthropicDefaultModelId)
}