Compare commits

..
Author SHA1 Message Date
Cline Evaluation bf1f665de8 Refactor reasoning effort option and checkpoint handling
• Replace "o3MiniReasoningEffort" with "reasoningEffort" in API providers
• Remove deprecated configuration properties from package.json
• Guard checkpoint tracker initialization and saving using the enableCheckpoints flag
2025-05-13 20:53:41 +04:00
140 changed files with 1670 additions and 10392 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
fix eternal loading states when the last message is a checkpoint
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Recent task list is now collapsible, allowing users to hide their recent tasks (e.g. when sharing their screen).
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Add detection for new users to display special components
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
refreshRequestyModels protobus migration
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
selectImages protos migration
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Remove vite files and make enable all the first item
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
askResponse protobus migration
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Adds switch to enabler/disable telemtry categories
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Add detailed configuration options for LiteLLM provider
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Releasing memory after every diff edit to help fix grey screen webview crashes
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
adds global endpoint for vertex ai users
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Add Tailwind CSS IntelliSense to the the recommended extensions list
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
new workflow feature
+1 -1
View File
@@ -209,7 +209,7 @@ class Task {
switch (chunk.type) {
case "text":
// Parse into content blocks
this.assistantMessageContent = parseAssistantMessageV2(chunk.text)
this.assistantMessageContent = parseAssistantMessage(chunk.text)
// Present blocks to user
await this.presentAssistantMessage()
break
-549
View File
@@ -1,549 +0,0 @@
The goal of this workflow is to take a changeset for a release of Cline, an autonomous coding agent extension that plugs right into your IDE, and write the updated announcement component, and the updated changelog.
For reference, here are some examples of how we converted previous changesets to announcement components / changelogs.
- 3.14
<changeset>
This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and publish to npm yourself or setup this action to publish automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.
Releases
claude-dev@3.14.0
Minor Changes
77c9863: create clinerules folder if its currently a file and creating new rule
0ffb7dd: disabling shift hint for now & improving tooltip behavior
79b76fd: Add support for custom model ID in AWS Bedrock provider, enabling use of Application Inference Profile.
eb6e481: Full support for LaTeX rendering
df37f29: Add support for custom API request timeout. Previously, timeouts were hardcoded to 30 seconds for providers like Ollama or 15 seconds for OpenRouter and Cline. Now users can set a custom timeout value in milliseconds through the settings interface.
e4d26be: allow cursorrules and windsurfrules
c5de50f: Fix Handle @withRetry() SyntaxError when running extension locally issue
61d2f42: enabled pricing calculation for gemini and vertex + more robust caching & cache tracking for gemini & vertex
aed152b: add truncation notice when truncating manually
2fe2405: Migrate Cline Tools Section to new docs
19cc8bc: Add a timeout setting for the terminal connection, allowing users to adjust this if they are having timeout issues
03d4410: Added copy button to code blocks.
c78fe23: addressed race condition in terminal command usage
91e222f: add checkpoints after more messages
14230e7: add newrule slash command
1c7d33a: Add remote config with posthog allowing for disabling new features until they're reading, making for a better developer experience.
4196c14: add cache ui for open router and cline provider
d97424f: showing expanded task by default
5294e78: Refactor to not pass a message for showing the MCP View from the servers modal
70cc437: Fix Windows path issue: Correct handling of import.meta.url to avoid leading slash in pathname
4b697d8: Migrate the addRemoteServer to protobus
Patch Changes
c63d9a1: updated drag and drop text to say "drop" instead of "drag"
459adf0: Add markdown copy to chat
74ec823: Minor UX improvement to drag and drop ux
b0961f4: Remove linear pull request action
e9ce384: searchCommits protobus migration
5802b68: createRuleFile protobus migration
df7f9fc: Add dependsOn to more blocks in the tasks.json
41ae732: Fix for git commit mentions in repos with no git commits
7e78445: Adding args to allow Cursor to open workspaces (for checkpoint testing/development)
bdfda6f: feat(bedrock): Introduce Amazon Nova Premier
65243ad: Introduce UI library for future UI development
4565e06: checkIsImageURL migrated to protobus
5a8e9d8: protobus migration for openImage
deeda6e: Lowering Gemini cache TTL time
db0b022: Adding UI to show openrouter balance next to provider
4650ffa: deleteRuleFile protobus migration
d4bd755: fix cost calculation
</changeset>
<changelog>
## [3.14.0]
- Add UI to show openrouter balance next to provider
- Add support for custom model ID in AWS Bedrock provider, enabling use of Application Inference Profile (Thanks @clicube!)
- Add more robust caching & cache tracking for gemini & vertex providers
- Add support for LaTeX rendering
- Add support for custom API request timeout. Timeouts were 15-30s, but can now be configured via settings for OpenRouter/Cline & Ollama (Thanks @WingsDrafterwork!)
- Add truncation notice when truncating manually
- Add a timeout setting for the terminal connection, allowing users to set a time to wait for terminal startup
- Add copy button to code blocks
- Add copy button to markdown blocks (Thanks @weshoke!)
- Add checkpoints to more messages
- Add slash command to create a new rules file (/newrule)
- Add cache ui for open router and cline provider
- Add Amazon Nova Premier model to Bedrock (Thanks @watany!)
- Add support for cursorrules and windsurfrules
- Add support for batch history deletion (Thanks @danix800!)
- Improve Drag & Drop experience
- Create clinerules folder creating new rule if it's needed
- Enable pricing calculation for gemini and vertex providers
- Refactor message handling to not show the MCP View of the server modal
- Migrate the addRemoteServer to protobus (Thanks @DaveFres!)
- Update task header to be expanded by default
- Update Gemini cache TTL time to 15 minutes
- Fix race condition in terminal command usage
- Fix to correctly handle `import.meta.url`, avoiding leading slash in pathname for Windows (Thanks @DaveFres!)
- Fix @withRetry() decoration syntax error when running extension locally (Thanks @DaveFres!)
- Fix for git commit mentions in repos with no git commits
- Fix cost calculation (Thanks @BarreiroT!)
</changelog>
<announcement-component>
const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
const minorVersion = version.split(".").slice(0, 2).join(".") // 2.0.0 -> 2.0
return (
<div style={containerStyle}>
<VSCodeButton appearance="icon" onClick={hideAnnouncement} style={closeIconStyle}>
<span className="codicon codicon-close"></span>
</VSCodeButton>
<h3 style={h3TitleStyle}>
🎉{" "}New in v{minorVersion}
</h3>
<ul style={ulStyle}>
<li>
<b>Gemini prompt caching:</b> Gemini and Vertex providers now support prompt caching and price tracking for
Gemini models.
</li>
<li>
<b>Copy Buttons:</b> Buttons were added to Markdown and Code blocks that allow you to copy their contents
easily.
</li>
<li>
<b>/newrule command:</b> New slash command to have cline write your .clinerules for you based on your
workflow.
</li>
<li>
<b>Drag and drop improvements:</b> Don't forget to hold shift while dragging files!
</li>
<li>Added more checkpoints across the task, allowing you to restore from more than just file changes.</li>
<li>Added support for rendering LaTeX in message responses. (Try asking Cline to show the quadratic formula)</li>
</ul>
<Accordion isCompact className="pl-0">
<AccordionItem
key="1"
aria-label="Previous Updates"
title="Previous Updates:"
classNames={{
trigger: "bg-transparent border-0 pl-0 pb-0 w-fit",
title: "font-bold text-[var(--vscode-foreground)]",
indicator:
"text-[var(--vscode-foreground)] mb-0.5 -rotate-180 data-[open=true]:-rotate-90 rtl:rotate-0 rtl:data-[open=true]:-rotate-90",
}}>
<ul style={ulStyle}>
<li>
<b>Global Cline Rules:</b> store multiple rules files in Documents/Cline/Rules to share between
projects.
</li>
<li>
<b>Cline Rules Popup:</b> New button in the chat area to view workspace and global cline rules files
to plug and play specific rules for the task
</li>
<li>
<b>Slash Commands:</b> Type <code>/</code> in chat to see the list of quick actions, like starting a
new task (more coming soon!)
</li>
<li>
<b>Edit Messages:</b> You can now edit a message you sent previously by clicking on it. Optionally
restore your project when the message was sent!
</li>
</ul>
</AccordionItem>
</Accordion>
{/*
// Leave this here for an example of how to structure the announcement
<ul style={{ margin: "0 0 8px", paddingLeft: "12px" }}>
<li>
OpenRouter now supports prompt caching! They also have much higher rate limits than other providers,
so I recommend trying them out.
<br />
{!apiConfiguration?.openRouterApiKey && (
<VSCodeButtonLink
href={getOpenRouterAuthUrl(vscodeUriScheme)}
style={{
transform: "scale(0.85)",
transformOrigin: "left center",
margin: "4px -30px 2px 0",
}}>
Get OpenRouter API Key
</VSCodeButtonLink>
)}
{apiConfiguration?.openRouterApiKey && apiConfiguration?.apiProvider !== "openrouter" && (
<VSCodeButton
onClick={() => {
vscode.postMessage({
type: "apiConfiguration",
apiConfiguration: { ...apiConfiguration, apiProvider: "openrouter" },
})
}}
style={{
transform: "scale(0.85)",
transformOrigin: "left center",
margin: "4px -30px 2px 0",
}}>
Switch to OpenRouter
</VSCodeButton>
)}
</li>
<li>
<b>Edit Cline's changes before accepting!</b> When he creates or edits a file, you can modify his
changes directly in the right side of the diff view (+ hover over the 'Revert Block' arrow button in
the center to undo "<code>{"// rest of code here"}</code>" shenanigans)
</li>
<li>
New <code>search_files</code> tool that lets Cline perform regex searches in your project, letting
him refactor code, address TODOs and FIXMEs, remove dead code, and more!
</li>
<li>
When Cline runs commands, you can now type directly in the terminal (+ support for Python
environments)
</li>
</ul>*/}
<div style={hrStyle} />
<p style={linkContainerStyle}>
Join us on{" "}
<VSCodeLink style={linkStyle} href="https://x.com/cline">
X,
</VSCodeLink>{" "}
<VSCodeLink style={linkStyle} href="https://discord.gg/cline">
discord,
</VSCodeLink>{" "}
or{" "}
<VSCodeLink style={linkStyle} href="https://www.reddit.com/r/cline/">
r/cline
</VSCodeLink>
for more updates!
</p>
</div>
)
}
</announcement-component>
- 3.13
<changeset>
Minor Changes
2964388: Added copy button to MermaidBlock component
75143a7: Add the ability to fetch from global cline rules files
Patch Changes
a0252e7: convert inline style to tailwind css of file SettingsView.tsx
ab59bd9: Add stream options back to xai provider
7276f50: Icons to indicate an action is occuring outside of the users workspace
0b19ba6: update to NEW model
</changeset>
<changelog>
## [3.13.0]
- Add Cline rules popover under the chat field, allowing you to easily add, enable & disable workspace level or global rule files
- Add new slash command menu letting you type “/“ to do quick actions like creating new tasks
- Add ability to edit past messages, with options to restore your workspace back to that point
- Allow sending a message when selecting an option provided by the question or plan tool
- Add command to jump to Cline's chat input
- Add support for OpenAI o3 & 4o-mini (Thanks @PeterDaveHello and @arafatkatze!)
- Add baseURL option for Google Gemini provider (Thanks @owengo and @olivierhub!)
- Add support for Azure's DeepSeek model. (Thanks @yt3trees!)
- Add ability for models that support it to receive image responses from MCP servers (Thanks @rikaaa0928!)
- Improve search and replace diff editing by making it more flexible with models that fail to follow structured output instructions. (Thanks @chi-cat!)
- Add detection of Ctrl+C termination in terminal, improving output reading issues
- Fix issue where some commands with large output would cause UI to freeze
- Fix token usage tracking issues with vertex provider (Thanks @mzsima!)
- Fix issue with xAI reasoning content not being parsed (Thanks @mrubens!)
</changelog>
<announcement-component>
const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
const minorVersion = version.split(".").slice(0, 2).join(".") // 2.0.0 -> 2.0
return (
<div style={containerStyle}>
<VSCodeButton appearance="icon" onClick={hideAnnouncement} style={closeIconStyle}>
<span className="codicon codicon-close"></span>
</VSCodeButton>
<h3 style={h3TitleStyle}>
🎉{" "}New in v{minorVersion}
</h3>
<ul style={ulStyle}>
<li>
<b>Global Cline Rules:</b> store multiple rules files in Documents/Cline/Rules to share between projects.
</li>
<li>
<b>Cline Rules Popup:</b> New button in the chat area to view workspace and global cline rules files to plug
and play specific rules for the task
</li>
<li>
<b>Slash Commands:</b> Type <code>/</code> in chat to see the list of quick actions, like starting a new task
(more coming soon!)
</li>
<li>
<b>Edit Messages:</b> You can now edit a message you sent previously by clicking on it. Optionally restore
your project when the message was sent!
</li>
</ul>
<h4 style={{ margin: "5px 0 5px" }}>Previous Updates:</h4>
<ul style={ulStyle}>
<li>
<b>Model Favorites:</b> You can now mark your favorite models when using Cline & OpenRouter providers for
quick access!
</li>
<li>
<b>Faster Diff Editing:</b> Improved animation performance for large files, plus a new indicator in chat
showing the number of edits Cline makes.
</li>
<li>
<b>New Auto-Approve Options:</b> Turn off Cline's ability to read and edit files outside your workspace.
</li>
</ul>
{/*
// Leave this here for an example of how to structure the announcement
<ul style={{ margin: "0 0 8px", paddingLeft: "12px" }}>
<li>
OpenRouter now supports prompt caching! They also have much higher rate limits than other providers,
so I recommend trying them out.
<br />
{!apiConfiguration?.openRouterApiKey && (
<VSCodeButtonLink
href={getOpenRouterAuthUrl(vscodeUriScheme)}
style={{
transform: "scale(0.85)",
transformOrigin: "left center",
margin: "4px -30px 2px 0",
}}>
Get OpenRouter API Key
</VSCodeButtonLink>
)}
{apiConfiguration?.openRouterApiKey && apiConfiguration?.apiProvider !== "openrouter" && (
<VSCodeButton
onClick={() => {
vscode.postMessage({
type: "apiConfiguration",
apiConfiguration: { ...apiConfiguration, apiProvider: "openrouter" },
})
}}
style={{
transform: "scale(0.85)",
transformOrigin: "left center",
margin: "4px -30px 2px 0",
}}>
Switch to OpenRouter
</VSCodeButton>
)}
</li>
<li>
<b>Edit Cline's changes before accepting!</b> When he creates or edits a file, you can modify his
changes directly in the right side of the diff view (+ hover over the 'Revert Block' arrow button in
the center to undo "<code>{"// rest of code here"}</code>" shenanigans)
</li>
<li>
New <code>search_files</code> tool that lets Cline perform regex searches in your project, letting
him refactor code, address TODOs and FIXMEs, remove dead code, and more!
</li>
<li>
When Cline runs commands, you can now type directly in the terminal (+ support for Python
environments)
</li>
</ul>*/}
<div style={hrStyle} />
<p style={linkContainerStyle}>
Join us on{" "}
<VSCodeLink style={linkStyle} href="https://x.com/cline">
X,
</VSCodeLink>{" "}
<VSCodeLink style={linkStyle} href="https://discord.gg/cline">
discord,
</VSCodeLink>{" "}
or{" "}
<VSCodeLink style={linkStyle} href="https://www.reddit.com/r/cline/">
r/cline
</VSCodeLink>
for more updates!
</p>
</div>
)
}
</announcement-component>
We have a changeset PR that automatically generated as new unreleased PRs are merged into main, the PR is always called "Changeset version bump" and the author is github-actions.
The Changeset PR description looks something like this:
<changeset-pr-description>
This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and publish to npm yourself or [setup this action to publish automatically](https://github.com/changesets/action#with-publishing). If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.
# Releases
## claude-dev@3.16.0
### Minor Changes
- c6e8b04: Recent task list is now collapsible, allowing users to hide their recent tasks (e.g. when sharing their screen).
- aabe4ae: Add detection for new users to display special components
- 6c18d51: adds global endpoint for vertex ai users
- 080ed7c: Add Tailwind CSS IntelliSense to the the recommended extensions list
- 5147e28: new workflow feature
### Patch Changes
- c0b3c69: fix eternal loading states when the last message is a checkpoint
- 570ece3: selectImages protos migration
- 8d8452e: askResponse protobus migration
- cd1ff2a: Finishing the migration of Vscode Advanced settings to Settings Webview
</changeset-pr-description>
The changeset pr is ALWAYS on the following branch: `changeset-release/main`.
I have the `gh` command line tool set up and authenticated, so you have everything you need.
The first step is to get the full diff from the changeset PR to look at the changes that were automatically made to the `CHANGELOG.md` file. By default it will automatically add a new section to the changelog.md file with the new version. The problem with the automatically generated section is that it just takes the text that the developers threw into their changeset files for each corresponding PR, and they can be pretty vague and bad. Additionally there's some stuff that is totally irrelevant for the end user, like minor refactoring changes. So I manually typically go in and update this section to be a proper changelog that will show up in our patchnotes. You can look at how the rest of the file is done because those are all good examples of us updating this to use good language for the end user. We usually put new features up top (and the most exciting flagship features at the very top), and then bug fixes/improvements at the bottom. Having some basic organization to the ordering of the bullet points by content is nice. But use common sense.
To handle this process effectively, do the following:
For each of the automatically generated bullet points in the Changelog.md, you should
1. Take the commit hash at the start of the bullet point, and use the `gh` command line tool find the PR that it was associated with.
2. Use the `gh` command to get the PR title/description/discussion to understand the context surrounding the PR.
3. Use the `gh` command line tool to get the full PR diff to fully understand the changes made in the code.
4. Synthesize that knowledge to determine (a) whether or not this change is relevant to end users and (b) what the text & ordering of the line should be.
5. Update the `CHANGELOG.md` accordingly
Do this for every single item in the list from the autogenerated bullet points. We want to be diligent and have a full understanding of every feature so we can make the best changelog ever!
Here are some principles for good changelogs from keepchangelog.com, a handy guide:
<keepachangelog-pinciples-for-good-changelogs>
### Guiding Principles
- Changelogs are for humans, not machines.
- There should be an entry for every single version.
- The same types of changes should be grouped.
- The latest version comes first.
### Bullet points in the changelog should follow these principles:
- Types of changes
- Added for new features.
- Changed for changes in existing functionality.
- Deprecated for soon-to-be removed features.
- Removed for now removed features.
- Fixed for any bug fixes.
- Security in case of vulnerabilities.
</keepachangelog-pinciples-for-good-changelogs>
Lastly, when developers make a PR, they typically make a changeset. And they have 3 options when making the changeset:
1. Patch
2. Minor
3. Major
Sometimes they label something as minor when really it should just be a patch. Or vice versa. Because of this, the automatic version bump may be incorrect. So when starting out this workflow, you should use the <ask_followup_question> tool to confirm with me whether or not this should be a patch bump (show the old version number and what the proposed new version number would be) or a minor bump. Part of the release process is making sure the version in package.json that is automatically changed actually corresponds with what we decided the bump should actually be based on the features. ALL these modifications happen in the `changeset-release/main` branch btw.
<important_note>
Before doing any of this, make sure you check out the `changeset-release/main` and pull the most recent up to date changes. Then perform all this work in that branch.
New announcement banners should ONLY be made for minor version bumps or higher. That's another reason why double checking if the changelog warrants the bump is important.
Also, SUPER important: For any external contributors that aren't part of the cline github organization, we always want to add a (Thanks @username!) at the end of the changelog to attribute them properly. We're an open source project and it's ethical to do this.
</important_note>
Once the changelog looks good, and the version number looks good, we gotta double check that the version number in the changelog has the brackets around it. And as a final step, double check the package.json version number matches the latest number in the changelog. And as the ultimate final step we run `npm run install:all` to make sure the package version number permiates through the lock file.
<detailed_sequence_of_steps>
# Cline Release Process - Detailed Sequence of Steps
## Before Starting
1. First, examine the changeset PR without checking it out:
```bash
gh pr view changeset-release/main
```
2. View the PR diff to see the auto-generated CHANGELOG.md changes:
```bash
gh pr diff changeset-release/main > changeset-diff.txt
cat changeset-diff.txt | grep -A 50 "CHANGELOG.md"
```
## Initial Setup
3. Once you're ready to start, checkout and update the changeset release branch:
```bash
git checkout changeset-release/main
git pull origin changeset-release/main
```
## Analyzing Each Change
4. For each commit hash in the auto-generated changelog entries:
a. Find the PR number associated with a commit hash:
```bash
gh pr list --search "<commit-hash>" --state merged
```
b. Get PR details for better context:
```bash
gh pr view <PR-number>
```
c. Check if the contributor is external to determine if attribution is needed:
```bash
# Extract username from PR
USERNAME=$(gh pr view <PR-number> --json author --jq .author.login)
# Check if user is a member of the Cline organization
# this command is a bit finnicky, but it 100% works.
# if you see a `Error executing command: The command ran successfully, but we couldn't capture its output. Please proceed accordingly.` error, just retry it until you actually get the output
# don't make any assumptions, just retry the command to actually get the output and determine if they're external or not.
# no output means they are an external contributor, otherwise if there is output they are an internal contributor (part of our github org)
gh api "orgs/cline/members" --jq "map(.login)" | grep -i "pashpashpash"
```
d. View the full PR diff to understand code changes:
```bash
gh pr diff <PR-number> > pr-diff-<PR-number>.txt
cat pr-diff-<PR-number>.txt
```
## Updating the Changelog
5. Based on PR analysis, update the CHANGELOG.md with user-friendly descriptions:
- Use the `<replace_in_file>` tool to edit the CHANGELOG.md file
- Group by feature type (Added, Changed, Fixed)
- Put most exciting features at the top
- Move bug fixes and small improvements to the bottom
- Use clear, end-user focused language
- For external contributors, add attribution at the end of the relevant entry: `(Thanks @username!)`
## Version Number Verification
6. Confirm the version bump is appropriate:
- Check package.json to verify the auto-generated version number:
```bash
cat package.json | grep "\"version\""
```
- If the feature set doesn't warrant a minor bump, use the `<replace_in_file>` tool to modify package.json
7. Ensure the version in CHANGELOG.md has brackets around it:
```
## [3.16.0]
```
## Creating the Announcement (for minor/major versions only)
8. If this is a minor version bump, create/update the announcement component:
- Use the `<replace_in_file>` tool to edit the src/views/components/announcement.tsx file
- Update the highlights based on key features
- Move previous version highlights to the "Previous Updates" section
- Use the previous announcement components as reference for structure
## Finalizing the Release
9. Update dependencies with the new version number:
```bash
npm run install:all
```
10. Commit your changes:
```bash
git add CHANGELOG.md package.json package-lock.json src/views/components/announcement.tsx
git commit -m "Update CHANGELOG.md and announcement for version 3.16.0"
```
11. Push your changes to the changeset branch:
```bash
git push origin changeset-release/main
```
12. Check that your changes pushed successfully:
```bash
git status
```
</detailed_sequence_of_steps>
+1 -1
View File
@@ -1,4 +1,4 @@
blank_issues_enabled: true
blank_issues_enabled: false
contact_links:
- name: ✨ Feature Request
url: https://github.com/cline/cline/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop
+1 -2
View File
@@ -1,6 +1,5 @@
out
dist
dist-standalone
node_modules
tmp
.vscode-test/
@@ -19,4 +18,4 @@ coverage
# But don't ignore the coverage scripts in .github/scripts/
!.github/scripts/coverage/
*evals.env
*evals.env
+1 -6
View File
@@ -1,10 +1,5 @@
{
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": [
"dbaeumer.vscode-eslint",
"connor4312.esbuild-problem-matchers",
"ms-vscode.extension-test-runner",
"bradlc.vscode-tailwindcss"
]
"recommendations": ["dbaeumer.vscode-eslint", "connor4312.esbuild-problem-matchers", "ms-vscode.extension-test-runner"]
}
-39
View File
@@ -16,45 +16,6 @@
"IS_DEV": "true",
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}"
}
},
{
"name": "Run Extension (Fresh Install Mode)",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--profile-temp",
"--sync",
"off",
"--disable-extensions",
"--extensionDevelopmentPath=${workspaceFolder}",
"${workspaceFolder}"
],
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
"preLaunchTask": "clean-sandbox",
"internalConsoleOptions": "openOnSessionStart",
"postDebugTask": "stop",
"env": {
"IS_DEV": "true",
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}"
}
},
{
"type": "node",
"request": "launch",
"name": "Run Standalone Extension",
"skipFiles": ["<node_internals>/**"],
"sourceMaps": true,
"resolveSourceMapLocations": ["${workspaceFolder}/**", "!**/node_modules/**"],
"cwd": "${workspaceFolder}/dist-standalone",
"outFiles": ["${workspaceFolder}/dist-standalone/**/*.js"],
"preLaunchTask": "compile-standalone",
"env": {
"GRPC_TRACE": "all",
"GRPC_VERBOSITY": "DEBUG",
"NODE_PATH": "${workspaceFolder}/dist-standalone/node_modules"
},
"program": "standalone.js"
}
]
}
-16
View File
@@ -3,16 +3,6 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "compile-standalone",
"type": "npm",
"script": "compile-standalone",
"group": "build",
"problemMatcher": [],
"presentation": {
"reveal": "always"
}
},
{
"label": "npm: protos",
"type": "npm",
@@ -195,12 +185,6 @@
"label": "stop",
"command": "echo ${input:terminate}",
"type": "shell"
},
{
"label": "clean-sandbox",
"type": "shell",
"dependsOn": ["watch"],
"command": "rm -rf .vscode-dev"
}
],
"inputs": [
-29
View File
@@ -1,34 +1,5 @@
# Changelog
## [3.15.5]
- Fix inefficient memory management in the task timeline
- Fix Gemini rate limitation response not being handled properly (Thanks @BarreiroT!)
## [3.15.4]
- Add gemini model back to vertex provider
- Add gemini telemetry
- Add filtering for tasks tied to the current workspace
## [3.15.3]
- Add Fireworks API Provider
- Fix minor visual issues with auto-approve menu
- Fix one instance of terminal not getting output
- Fix 'Chrome was launched but debug port is not responding' error
## [3.15.2]
- Added details to auto approve menu and more sensible default controls
- Add detailed configuration options for LiteLLM provider
- Add webview telemetry for users who have opted in to telemetry
- Update Gemini in OpenRouter/Cline providers to use implicit caching
- Fix freezing issues during rendering of large streaming text
- Fix grey screen webview crashes by releasing memory after every diff edit
- Fix breaking out of diff auto-scroll
- Fix IME composition Enter autosending edited message
## [3.15.1]
- Fix bug where PowerShell commands weren't given enough time before giving up and showing an error
+5 -5
View File
@@ -44,16 +44,16 @@ This guide is tailored for organizations with established GCP environments (leve
#### 2.1 Choose and Confirm a Region
Vertex AI supports multiple regions. Select a region that meets your latency, compliance, and capacity needs. Examples include:
Vertex AI supports eight regions. Select a region that meets your latency, compliance, and capacity needs. Examples include:
- **us-east5 (Columbus, Ohio)**
- **us-east1 (South Carolina)**
- **us-east4 (Northern Virginia)**
- **us-central1 (Iowa)**
- **us-west1 (The Dalles, Oregon)**
- **us-west4 (Las Vegas, Nevada)**
- **europe-west1 (Belgium)**
- **europe-west4 (Netherlands)**
- **asia-southeast1 (Singapore)**
- **global (Global)**
The Global endpoint may offer higher availability and reduce resource exhausted errors. Only Gemini models are supported.
#### 2.2 Enable the Claude 3.5 Sonnet v2 Model
+2 -2
View File
@@ -26,9 +26,9 @@ For complete transparency, you can inspect our [telemetry implementation](https:
### How to Opt Out
Telemetry in Cline is entirely optional:
Telemetry in Cline is entirely optional and requires your explicit consent:
- When you update or install our VS Code extension, you'll see a message about our anonymous telemetry
- When you update or install our VS Code extension, you'll see a simple prompt: "Help Improve Cline" with Allow or Deny options
- You can change your preference anytime in settings
Cline also respects VS Code's global telemetry settings. If you've disabled telemetry at the VS Code level, Cline's telemetry will automatically be disabled as well.
+5 -24
View File
@@ -4,8 +4,6 @@ const path = require("path")
const production = process.argv.includes("--production")
const watch = process.argv.includes("--watch")
const standalone = process.argv.includes("--standalone")
const destDir = standalone ? "dist-standalone" : "dist"
/**
* @type {import('esbuild').Plugin}
@@ -87,7 +85,7 @@ const copyWasmFiles = {
build.onEnd(() => {
// tree sitter
const sourceDir = path.join(__dirname, "node_modules", "web-tree-sitter")
const targetDir = path.join(__dirname, destDir)
const targetDir = path.join(__dirname, "dist")
// Copy tree-sitter.wasm
fs.copyFileSync(path.join(sourceDir, "tree-sitter.wasm"), path.join(targetDir, "tree-sitter.wasm"))
@@ -119,8 +117,7 @@ const copyWasmFiles = {
},
}
// Base configuration shared between extension and standalone builds
const baseConfig = {
const extensionConfig = {
bundle: true,
minify: production,
sourcemap: !production,
@@ -143,32 +140,16 @@ const baseConfig = {
},
},
],
entryPoints: ["src/extension.ts"],
format: "cjs",
sourcesContent: false,
platform: "node",
}
// Extension-specific configuration
const extensionConfig = {
...baseConfig,
entryPoints: ["src/extension.ts"],
outfile: `${destDir}/extension.js`,
outfile: "dist/extension.js",
external: ["vscode"],
}
// Standalone-specific configuration
const standaloneConfig = {
...baseConfig,
entryPoints: ["src/standalone/standalone.ts"],
outfile: `${destDir}/standalone.js`,
// These gRPC protos need to load files from the module directory at runtime,
// so they cannot be bundled.
external: ["vscode", "@grpc/reflection", "grpc-health-check"],
}
async function main() {
const config = standalone ? standaloneConfig : extensionConfig
const extensionCtx = await esbuild.context(config)
const extensionCtx = await esbuild.context(extensionConfig)
if (watch) {
await extensionCtx.watch()
} else {
+101 -1039
View File
File diff suppressed because it is too large Load Diff
+3 -13
View File
@@ -2,7 +2,7 @@
"name": "claude-dev",
"displayName": "Cline",
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
"version": "3.15.5",
"version": "3.15.1",
"icon": "assets/icons/icon.png",
"engines": {
"vscode": "^1.84.0"
@@ -40,8 +40,6 @@
"llama"
],
"activationEvents": [
"onLanguage",
"onStartupFinished",
"workspaceContains:evals.env"
],
"main": "./dist/extension.js",
@@ -242,14 +240,11 @@
"scripts": {
"vscode:prepublish": "npm run package",
"compile": "npm run check-types && npm run lint && node esbuild.js",
"compile-standalone": "npm run protos && npm run check-types && npm run lint && node esbuild.js --standalone",
"postcompile-standalone": "node scripts/package-standalone.mjs",
"watch": "npm-run-all -p watch:*",
"watch:esbuild": "node esbuild.js --watch",
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
"package": "npm run build:webview && npm run check-types && npm run lint && node esbuild.js --production",
"protos": "node proto/build-proto.js && node scripts/generate-server-setup.mjs",
"postprotos": "prettier src/shared/proto src/core/controller webview-ui/src/services src/standalone/server-setup.ts --write --log-level silent",
"protos": "node proto/build-proto.js && prettier src/shared/proto src/core/controller webview-ui/src/services --write",
"compile-tests": "node ./scripts/build-tests.js",
"watch-tests": "tsc -p . -w --outDir out",
"pretest": "npm run compile-tests && npm run compile && npm run lint",
@@ -319,9 +314,8 @@
"@aws-sdk/client-bedrock-runtime": "^3.758.0",
"@bufbuild/protobuf": "^2.2.5",
"@google-cloud/vertexai": "^1.9.3",
"@google/genai": "^0.13.0",
"@google/genai": "^0.9.0",
"@grpc/grpc-js": "^1.9.15",
"@grpc/reflection": "^1.0.4",
"@mistralai/mistralai": "^1.5.0",
"@modelcontextprotocol/sdk": "^1.7.0",
"@opentelemetry/api": "^1.4.1",
@@ -332,7 +326,6 @@
"@opentelemetry/semantic-conventions": "^1.30.0",
"@sentry/browser": "^9.12.0",
"@vscode/codicons": "^0.0.36",
"archiver": "^7.0.1",
"axios": "^1.8.2",
"cheerio": "^1.0.0",
"chokidar": "^4.0.1",
@@ -346,7 +339,6 @@
"fzf": "^0.5.2",
"get-folder-size": "^5.0.0",
"globby": "^14.0.2",
"grpc-health-check": "^2.0.2",
"iconv-lite": "^0.6.3",
"ignore": "^7.0.3",
"image-size": "^2.0.2",
@@ -355,7 +347,6 @@
"mammoth": "^1.8.0",
"monaco-vscode-textmate-theme-converter": "^0.1.7",
"ollama": "^0.5.13",
"open": "^10.1.2",
"open-graph-scraper": "^6.9.0",
"openai": "^4.83.0",
"os-name": "^6.0.0",
@@ -369,7 +360,6 @@
"simple-git": "^3.27.0",
"strip-ansi": "^7.1.0",
"tree-sitter-wasms": "^0.1.11",
"ts-morph": "^25.0.1",
"turndown": "^7.2.0",
"web-tree-sitter": "^0.22.6",
"zod": "^3.24.2"
+21 -34
View File
@@ -29,7 +29,6 @@ const serviceNameMap = {
task: "cline.TaskService",
web: "cline.WebService",
models: "cline.ModelsService",
slash: "cline.SlashService",
// Add new services here - no other code changes needed!
}
const serviceDirs = Object.keys(serviceNameMap).map((serviceKey) => path.join(ROOT_DIR, "src", "core", "controller", serviceKey))
@@ -55,42 +54,30 @@ async function main() {
// Process all proto files
console.log(chalk.cyan("Processing proto files from"), SCRIPT_DIR)
const protoFiles = await globby("*.proto", { cwd: SCRIPT_DIR, absolute: true })
const protoFiles = await globby("*.proto", { cwd: SCRIPT_DIR })
// Build the protoc command with proper path handling for cross-platform
const tsProtocCommand = [
protoc,
`--proto_path="${SCRIPT_DIR}"`,
`--plugin=protoc-gen-ts_proto="${tsProtoPlugin}"`,
`--ts_proto_out="${TS_OUT_DIR}"`,
"--ts_proto_opt=outputServices=generic-definitions,env=node,esModuleInterop=true,useDate=false,useOptionals=messages",
...protoFiles,
].join(" ")
try {
console.log(chalk.cyan(`Generating TypeScript code for:\n${protoFiles.join("\n")}...`))
execSync(tsProtocCommand, { stdio: "inherit" })
} catch (error) {
console.error(chalk.red("Error generating TypeScript for proto files:"), error)
process.exit(1)
}
for (const protoFile of protoFiles) {
console.log(chalk.cyan(`Generating TypeScript code for ${protoFile}...`))
const descriptorOutDir = path.join(ROOT_DIR, "dist-standalone", "proto")
await fs.mkdir(descriptorOutDir, { recursive: true })
// Build the protoc command with proper path handling for cross-platform
const protocCommand = [
protoc,
`--plugin=protoc-gen-ts_proto="${tsProtoPlugin}"`,
`--ts_proto_out="${TS_OUT_DIR}"`,
"--ts_proto_opt=outputServices=generic-definitions,env=node,esModuleInterop=true,useDate=false,useOptionals=messages",
`--proto_path="${SCRIPT_DIR}"`,
`"${path.join(SCRIPT_DIR, protoFile)}"`,
].join(" ")
const descriptorFile = path.join(descriptorOutDir, "descriptor_set.pb")
const descriptorProtocCommand = [
protoc,
`--proto_path="${SCRIPT_DIR}"`,
`--descriptor_set_out="${descriptorFile}"`,
"--include_imports",
...protoFiles,
].join(" ")
try {
console.log(chalk.cyan("Generating descriptor set..."))
execSync(descriptorProtocCommand, { stdio: "inherit" })
} catch (error) {
console.error(chalk.red("Error generating descriptor set for proto file:"), error)
process.exit(1)
try {
const execOptions = {
stdio: "inherit",
}
execSync(protocCommand, execOptions)
} catch (error) {
console.error(chalk.red(`Error generating TypeScript for ${protoFile}:`), error)
process.exit(1)
}
}
console.log(chalk.green("Protocol Buffer code generation completed successfully."))
-4
View File
@@ -22,9 +22,6 @@ service FileService {
// Search git commits in the workspace
rpc searchCommits(StringRequest) returns (GitCommits);
// Select images from the file system and return as data URLs
rpc selectImages(EmptyRequest) returns (StringArray);
// Convert URIs to workspace-relative paths
rpc getRelativePaths(RelativePathsRequest) returns (RelativePaths);
@@ -85,7 +82,6 @@ message RuleFileRequest {
bool is_global = 2; // Common field for all operations
optional string rule_path = 3; // Path field for deleteRuleFile (optional)
optional string filename = 4; // Filename field for createRuleFile (optional)
optional string type = 5; // Type of the file to create (optional)
}
// Result for rule file operations with meaningful data only
-1
View File
@@ -10,7 +10,6 @@ service McpService {
rpc toggleMcpServer(ToggleMcpServerRequest) returns (McpServers);
rpc updateMcpTimeout(UpdateMcpTimeoutRequest) returns (McpServers);
rpc addRemoteMcpServer(AddRemoteMcpServerRequest) returns (McpServers);
rpc downloadMcp(StringRequest) returns (Empty);
}
message ToggleMcpServerRequest {
-14
View File
@@ -1,14 +0,0 @@
syntax = "proto3";
package cline;
option java_package = "bot.cline.proto";
option java_multiple_files = true;
import "common.proto";
// SlashService provides methods for managing slash
service SlashService {
// Sends button click message
rpc reportBug(StringRequest) returns (Empty);
rpc condense(StringRequest) returns (Empty);
}
-1
View File
@@ -6,7 +6,6 @@ import "common.proto";
service StateService {
rpc getLatestState(EmptyRequest) returns (State);
rpc subscribeToState(EmptyRequest) returns (stream State);
rpc toggleFavoriteModel(StringRequest) returns (Empty);
}
message State {
-11
View File
@@ -25,8 +25,6 @@ service TaskService {
rpc deleteNonFavoritedTasks(EmptyRequest) returns (DeleteNonFavoritedTasksResults);
// Gets filtered task history
rpc getTaskHistory(GetTaskHistoryRequest) returns (TaskHistoryArray);
// Sends a response to a previous ask operation
rpc askResponse(AskResponseRequest) returns (Empty);
}
// Request message for creating a new task
@@ -69,7 +67,6 @@ message GetTaskHistoryRequest {
bool favorites_only = 2;
string search_query = 3;
string sort_by = 4;
bool current_workspace_only = 5;
}
// Response for task history
@@ -91,11 +88,3 @@ message TaskItem {
int32 cache_writes = 9;
int32 cache_reads = 10;
}
// Request for ask response operation
message AskResponseRequest {
Metadata metadata = 1;
string response_type = 2;
string text = 3;
repeated string images = 4;
}
-79
View File
@@ -1,79 +0,0 @@
import * as fs from "fs"
import * as grpc from "@grpc/grpc-js"
import * as protoLoader from "@grpc/proto-loader"
import * as health from "grpc-health-check"
import { fileURLToPath } from "url"
import path from "path"
const OUT_FILE = path.resolve("src/standalone/server-setup.ts")
const DESCRIPTOR_SET = path.resolve("dist-standalone/proto/descriptor_set.pb")
// Load service definitions.
const clineDef = protoLoader.loadFileDescriptorSetFromBuffer(fs.readFileSync(DESCRIPTOR_SET))
const healthDef = protoLoader.loadSync(health.protoPath)
const packageDefinition = { ...clineDef, ...healthDef }
const proto = grpc.loadPackageDefinition(packageDefinition)
/**
* Generate imports and function to add all the handlers to the server for all services defined in the proto files.
*/
function generateHandlersAndExports() {
let imports = []
let handlerSetup = []
for (const [name, def] of Object.entries(proto.cline)) {
if (!def || !("service" in def)) {
continue
}
const domain = name.replace(/Service$/, "")
const dir = domain.charAt(0).toLowerCase() + domain.slice(1)
imports.push(`// ${domain} Service`)
handlerSetup.push(` // ${domain} Service`)
handlerSetup.push(` server.addService(proto.cline.${name}.service, {`)
for (const [rpcName, rpc] of Object.entries(def.service)) {
imports.push(`import { ${rpcName} } from "../core/controller/${dir}/${rpcName}"`)
if (rpc.requestStream) {
throw new Error("Request streaming is not supported")
}
if (rpc.responseStream) {
handlerSetup.push(` ${rpcName}: wrapStreamingResponse(${rpcName}, controller),`)
} else {
handlerSetup.push(` ${rpcName}: wrapper(${rpcName}, controller),`)
}
}
handlerSetup.push(` });`)
imports.push("")
handlerSetup.push("")
}
return {
imports: imports.join("\n"),
handlerSetup: handlerSetup.join("\n"),
}
}
const { imports, handlerSetup } = generateHandlersAndExports()
const scriptName = path.basename(fileURLToPath(import.meta.url))
// Create output file
let output = `// GENERATED CODE -- DO NOT EDIT!
// Generated by ${scriptName}
import * as grpc from "@grpc/grpc-js"
import { Controller } from "../core/controller"
import { GrpcHandlerWrapper, GrpcStreamingResponseHandlerWrapper } from "./grpc-types"
${imports}
export function addServices(
server: grpc.Server,
proto: any,
controller: Controller,
wrapper: GrpcHandlerWrapper,
wrapStreamingResponse: GrpcStreamingResponseHandlerWrapper,
): void {
${handlerSetup}
}
`
// Write output file
fs.writeFileSync(OUT_FILE, output)
console.log(`Generated service handlers in ${OUT_FILE}.`)
-96
View File
@@ -1,96 +0,0 @@
const fs = require("fs")
const path = require("path")
const { Project, SyntaxKind } = require("ts-morph")
function traverse(container, output, prefix = "") {
for (const node of container.getStatements()) {
const kind = node.getKind()
if (kind === SyntaxKind.ModuleDeclaration) {
const name = node.getName().replace(/^['"]|['"]$/g, "")
var fullPrefix
if (prefix) {
fullPrefix = `${prefix}.${name}`
} else {
fullPrefix = name
}
output.push(`${fullPrefix} = {};`)
const body = node.getBody()
if (body && body.getKind() === SyntaxKind.ModuleBlock) {
traverse(body, output, fullPrefix)
}
} else if (kind === SyntaxKind.FunctionDeclaration) {
const name = node.getName()
const params = node.getParameters().map((p, i) => sanitizeParam(p.getName(), i))
const typeNode = node.getReturnTypeNode()
const returnType = typeNode ? typeNode.getText() : ""
const ret = mapReturn(returnType)
output.push(
`${prefix}.${name} = function(${params.join(", ")}) { console.log('Called stubbed function: ${prefix}.${name}'); ${ret} };`,
)
} else if (kind === SyntaxKind.EnumDeclaration) {
const name = node.getName()
const members = node.getMembers().map((m) => m.getName())
output.push(`${prefix}.${name} = { ${members.map((m) => `${m}: 0`).join(", ")} };`)
} else if (kind === SyntaxKind.VariableStatement) {
for (const decl of node.getDeclarations()) {
const name = decl.getName()
output.push(`${prefix}.${name} = createStub("${prefix}.${name}");`)
}
} else if (kind == SyntaxKind.ClassDeclaration) {
const name = node.getName()
output.push(
`${prefix}.${name} = class { constructor(...args) {
console.log('Constructed stubbed class: new ${prefix}.${name}(', args, ')');
return createStub(${prefix}.${name});
}};`,
)
} else if (kind === SyntaxKind.TypeAliasDeclaration || kind === SyntaxKind.InterfaceDeclaration) {
//console.log("Skipping", SyntaxKind[kind], node.getName())
// Skip interfaces and type aliases because they are only used at compile time by typescript.
} else {
console.log("Can't handle: ", SyntaxKind[kind])
}
}
}
function mapReturn(typeStr) {
if (!typeStr) return ""
if (typeStr.includes("void")) return ""
if (typeStr.includes("string")) return `return '';`
if (typeStr.includes("number")) return `return 0;`
if (typeStr.includes("boolean")) return `return false;`
if (typeStr.includes("[]")) return `return [];`
if (typeStr.includes("Thenable")) return `return Promise.resolve(null);`
return `return createStub("unknown");`
}
function sanitizeParam(name, index) {
return name || `arg${index}`
}
async function main() {
const inputPath = "node_modules/@types/vscode/index.d.ts"
const outputPath = "standalone/runtime-files/vscode/vscode-stubs.js"
const project = new Project()
const sourceFile = project.addSourceFileAtPath(inputPath)
const output = []
output.push("// GENERATED CODE -- DO NOT EDIT!")
output.push('console.log("Loading stubs...");')
output.push('const { createStub } = require("./stub-utils")')
traverse(sourceFile, output)
output.push("module.exports = vscode;")
output.push('console.log("Finished loading stubs");')
fs.mkdirSync(path.dirname(outputPath), { recursive: true })
fs.writeFileSync(outputPath, output.join("\n"))
console.log(`Wrote vscode SDK stubs to ${outputPath}`)
}
main().catch((err) => {
console.error(err)
process.exit(1)
})
-18
View File
@@ -1,18 +0,0 @@
#!/usr/bin/env bash
set -eu
DIR=${1:-src/}
DEST_DIR=dist-standalone
DEST=dist-standalone/vscode-uses.txt
mkdir -p $DEST_DIR
{
git grep -h 'vscode\.' $DIR |
grep -Ev '//.*vscode' | # remove commented out code
sed 's|.*vscode\.|vscode.|'| # remove everything before vscode.
sed 's/[^a-zA-Z0-9_.].*$//' | # remove everything after last identifier
sort | uniq > $DEST
}
echo Done, wrote uses of the vscode SDK to $(realpath $DEST)
-51
View File
@@ -1,51 +0,0 @@
import fs from "fs"
import path from "path"
import { glob } from "glob"
import archiver from "archiver"
import { cp } from "fs/promises"
import { execSync } from "child_process"
const BUILD_DIR = "dist-standalone"
const SOURCE_DIR = "standalone/runtime-files"
await cp(SOURCE_DIR, BUILD_DIR, { recursive: true })
// Run npm install in the distribution directory
console.log("Running npm install in distribution directory...")
const cwd = process.cwd()
process.chdir(BUILD_DIR)
try {
execSync("npm install", { stdio: "inherit" })
} catch (error) {
console.error("Error running npm install:", error)
process.exit(1)
} finally {
process.chdir(cwd)
}
// Check for native .node modules.
const nativeModules = await glob("**/*.node", { cwd: BUILD_DIR, nodir: true })
if (nativeModules.length > 0) {
console.error("Native node modules cannot be included in the standalone distribution:\n", nativeModules.join("\n"))
process.exit(1)
}
// Zip the build directory (excluding any pre-existing output zip).
const zipPath = path.join(BUILD_DIR, "standalone.zip")
const output = fs.createWriteStream(zipPath)
const archive = archiver("zip", { zlib: { level: 9 } })
output.on("close", () => {
console.log(`Created ${zipPath} (${archive.pointer()} bytes)`)
})
archive.on("error", (err) => {
throw err
})
archive.pipe(output)
archive.glob("**/*", {
cwd: BUILD_DIR,
ignore: ["standalone.zip"],
})
await archive.finalize()
-3
View File
@@ -19,7 +19,6 @@ import { DoubaoHandler } from "./providers/doubao"
import { VsCodeLmHandler } from "./providers/vscode-lm"
import { ClineHandler } from "./providers/cline"
import { LiteLlmHandler } from "./providers/litellm"
import { FireworksHandler } from "./providers/fireworks"
import { AskSageHandler } from "./providers/asksage"
import { XAIHandler } from "./providers/xai"
import { SambanovaHandler } from "./providers/sambanova"
@@ -59,8 +58,6 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
return new DeepSeekHandler(options)
case "requesty":
return new RequestyHandler(options)
case "fireworks":
return new FireworksHandler(options)
case "together":
return new TogetherHandler(options)
case "qwen":
-94
View File
@@ -1,94 +0,0 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { withRetry } from "../retry"
import { ApiHandler } from ".."
import {
ApiHandlerOptions,
DeepSeekModelId,
ModelInfo,
deepSeekDefaultModelId,
deepSeekModels,
openAiModelInfoSaneDefaults,
} from "../../shared/api"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
export class FireworksHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: "https://api.fireworks.ai/inference/v1",
apiKey: this.options.fireworksApiKey,
})
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const modelId = this.options.fireworksModelId ?? ""
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
]
const stream = await this.client.chat.completions.create({
model: modelId,
...(this.options.fireworksModelMaxCompletionTokens
? { max_completion_tokens: this.options.fireworksModelMaxCompletionTokens }
: {}),
...(this.options.fireworksModelMaxTokens ? { max_tokens: this.options.fireworksModelMaxTokens } : {}),
messages: openAiMessages,
stream: true,
stream_options: { include_usage: true },
temperature: 0,
})
let reasoning: string | null = null
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
if (reasoning || delta?.content?.includes("<think>")) {
reasoning = (reasoning || "") + (delta.content ?? "")
}
if (delta?.content && !reasoning) {
yield {
type: "text",
text: delta.content,
}
}
if (reasoning || ("reasoning_content" in delta && delta.reasoning_content)) {
yield {
type: "reasoning",
reasoning: delta.content || ((delta as any).reasoning_content as string | undefined) || "",
}
if (reasoning?.includes("</think>")) {
// Reset so the next chunk is regular content
reasoning = null
}
}
if (chunk.usage) {
yield {
type: "usage",
inputTokens: chunk.usage.prompt_tokens || 0, // (deepseek reports total input AND cache reads/writes, see context caching: https://api-docs.deepseek.com/guides/kv_cache) where the input tokens is the sum of the cache hits/misses, while anthropic reports them as separate tokens. This is important to know for 1) context management truncation algorithm, and 2) cost calculation (NOTE: we report both input and cache stats but for now set input price to 0 since all the cost calculation will be done using cache hits/misses)
outputTokens: chunk.usage.completion_tokens || 0,
// @ts-ignore-next-line
cacheReadTokens: chunk.usage.prompt_cache_hit_tokens || 0,
// @ts-ignore-next-line
cacheWriteTokens: chunk.usage.prompt_cache_miss_tokens || 0,
}
}
}
}
getModel(): { id: string; info: ModelInfo } {
return {
id: this.options.fireworksModelId ?? "",
info: openAiModelInfoSaneDefaults,
}
}
}
+44 -103
View File
@@ -1,12 +1,11 @@
import type { Anthropic } from "@anthropic-ai/sdk"
// Restore GenerateContentConfig import and add GenerateContentResponseUsageMetadata
import { GoogleGenAI, type GenerateContentConfig, type GenerateContentResponseUsageMetadata } from "@google/genai"
import { GoogleGenAI, type Content, type GenerateContentConfig, type GenerateContentResponseUsageMetadata } from "@google/genai"
import { withRetry } from "../retry"
import { ApiHandler } from "../"
import { ApiHandlerOptions, geminiDefaultModelId, GeminiModelId, geminiModels, ModelInfo } from "@shared/api"
import { convertAnthropicMessageToGemini } from "../transform/gemini-format"
import { ApiStream } from "../transform/stream"
import { telemetryService } from "@services/posthog/telemetry/TelemetryService"
// Define a default TTL for the cache (e.g., 15 minutes in seconds)
const DEFAULT_CACHE_TTL_SECONDS = 900
@@ -75,7 +74,7 @@ export class GeminiHandler implements ApiHandler {
*/
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const { id: modelId, info } = this.getModel()
const { id: model, info } = this.getModel()
const contents = messages.map(convertAnthropicMessageToGemini)
// Configure thinking budget if supported
@@ -99,110 +98,52 @@ export class GeminiHandler implements ApiHandler {
}
// Generate content using the configured parameters
const sdkCallStartTime = Date.now()
let sdkFirstChunkTime: number | undefined
let ttftSdkMs: number | undefined
let apiSuccess = false
let apiError: string | undefined
let promptTokens = 0
let outputTokens = 0
let cacheReadTokens = 0
const result = await this.client.models.generateContentStream({
model,
contents: contents,
config: {
...requestConfig,
},
})
// Track usage metadata
let lastUsageMetadata: GenerateContentResponseUsageMetadata | undefined
try {
const result = await this.client.models.generateContentStream({
model: modelId,
contents: contents,
config: {
...requestConfig,
},
// Process the stream
for await (const chunk of result) {
if (chunk.text) {
yield {
type: "text",
text: chunk.text,
}
}
if (chunk.usageMetadata) {
lastUsageMetadata = chunk.usageMetadata
}
}
// Yield usage information at the end
if (lastUsageMetadata) {
const inputTokens = lastUsageMetadata.promptTokenCount ?? 0
const outputTokens = lastUsageMetadata.candidatesTokenCount ?? 0
const cacheReadTokens = lastUsageMetadata.cachedContentTokenCount
// Calculate immediate costs
const totalCost = this.calculateCost({
info,
inputTokens,
outputTokens,
cacheReadTokens,
})
let isFirstSdkChunk = true
for await (const chunk of result) {
if (isFirstSdkChunk) {
sdkFirstChunkTime = Date.now()
ttftSdkMs = sdkFirstChunkTime - sdkCallStartTime
isFirstSdkChunk = false
}
if (chunk.text) {
yield {
type: "text",
text: chunk.text,
}
}
if (chunk.usageMetadata) {
lastUsageMetadata = chunk.usageMetadata
promptTokens = lastUsageMetadata.promptTokenCount ?? promptTokens
outputTokens = lastUsageMetadata.candidatesTokenCount ?? outputTokens
cacheReadTokens = lastUsageMetadata.cachedContentTokenCount ?? cacheReadTokens
}
}
apiSuccess = true
if (lastUsageMetadata) {
const totalCost = this.calculateCost({
info,
inputTokens: promptTokens,
outputTokens,
cacheReadTokens,
})
yield {
type: "usage",
inputTokens: promptTokens,
outputTokens,
cacheReadTokens,
cacheWriteTokens: 0,
totalCost,
}
}
} catch (error) {
apiSuccess = false
// Let the error propagate to be handled by withRetry or Task.ts
// Telemetry will be sent in the finally block.
if (error instanceof Error) {
apiError = error.message
// Gemini doesn't include status codes in their errors
// https://github.com/googleapis/js-genai/blob/61f7f27b866c74333ca6331883882489bcb708b9/src/_api_client.ts#L569
if (error.name === "ClientError" && error.message.includes("got status: 429 Too Many Requests.")) {
;(error as any).status = 429
}
} else {
apiError = String(error)
}
throw error
} finally {
const sdkCallEndTime = Date.now()
const totalDurationSdkMs = sdkCallEndTime - sdkCallStartTime
const cacheHit = cacheReadTokens > 0
const cacheHitPercentage = promptTokens > 0 ? (cacheReadTokens / promptTokens) * 100 : undefined
const throughputTokensPerSecSdk =
totalDurationSdkMs > 0 && outputTokens > 0 ? outputTokens / (totalDurationSdkMs / 1000) : undefined
if (this.options.taskId) {
telemetryService.captureGeminiApiPerformance(
this.options.taskId,
modelId,
{
ttftSec: ttftSdkMs !== undefined ? ttftSdkMs / 1000 : undefined,
totalDurationSec: totalDurationSdkMs / 1000,
promptTokens,
outputTokens,
cacheReadTokens,
cacheHit,
cacheHitPercentage,
apiSuccess,
apiError,
throughputTokensPerSec: throughputTokensPerSecSdk,
},
true,
)
} else {
console.warn("GeminiHandler: taskId not available for telemetry in createMessage.")
yield {
type: "usage",
inputTokens,
outputTokens,
cacheReadTokens,
cacheWriteTokens: 0,
totalCost,
}
}
}
+68
View File
@@ -75,6 +75,74 @@ export async function createOpenRouterStream(
break
}
// handles gemini caching logic
if (model.id.startsWith("google/") && model.info.supportsPromptCache) {
// gemini only uses the last breakpoint for caching, so the others will be ignored
openAiMessages[0] = {
role: "system",
content: [
{
type: "text",
text: systemPrompt,
// @ts-ignore-next-line
cache_control: { type: "ephemeral" },
},
],
}
// for safety, but this should always be the case
if (openAiMessages.length >= 2) {
const msg = openAiMessages[1]
if (msg) {
if (typeof msg.content === "string") {
msg.content = [{ type: "text", text: msg.content }]
}
if (Array.isArray(msg.content)) {
// NOTE: this is fine since env details will always be added at the end. but if it weren't there, and the user added a image_url type message, it would pop a text part before it and then move it after to the end.
let lastTextPart = msg.content.filter((part) => part.type === "text").pop()
if (!lastTextPart) {
lastTextPart = { type: "text", text: "..." }
msg.content.push(lastTextPart)
}
// @ts-ignore-next-line
lastTextPart["cache_control"] = { type: "ephemeral" }
}
}
}
// it doesn't make sense to alter breakpoints at all with the gemini cache implementation at this time
/*const GEMINI_CACHE_USER_MESSAGE_INTERVAL = 4 // add new breakpoint every 4 turns
const userMessages = openAiMessages.filter((msg) => msg.role === "user")
const userMessageCount = userMessages.length
const targetUserMessageNumber =
Math.floor(userMessageCount / GEMINI_CACHE_USER_MESSAGE_INTERVAL) * GEMINI_CACHE_USER_MESSAGE_INTERVAL
if (targetUserMessageNumber > 0) {
// otherwise dont need to add a breakpoint
const msg = userMessages[targetUserMessageNumber - 1]
if (msg) {
if (typeof msg.content === "string") {
msg.content = [{ type: "text", text: msg.content }]
}
if (Array.isArray(msg.content)) {
// NOTE: this is fine since env details will always be added at the end. but if it weren't there, and the user added a image_url type message, it would pop a text part before it and then move it after to the end.
let lastTextPart = msg.content.filter((part) => part.type === "text").pop()
if (!lastTextPart) {
lastTextPart = { type: "text", text: "..." }
msg.content.push(lastTextPart)
}
// @ts-ignore-next-line
lastTextPart["cache_control"] = { type: "ephemeral" }
}
}
}*/
}
// Not sure how openrouter defaults max tokens when no value is provided, but the anthropic api requires this value and since they offer both 4096 and 8192 variants, we should ensure 8192.
// (models usually default to max tokens allowed)
let maxTokens: number | undefined
+1 -1
View File
@@ -1,6 +1,6 @@
export type AssistantMessageContent = TextContent | ToolUse
export { parseAssistantMessageV1, parseAssistantMessageV2 } from "./parse-assistant-message"
export { parseAssistantMessage } from "./parse-assistant-message"
export interface TextContent {
type: "text"
@@ -1,24 +1,6 @@
import { AssistantMessageContent, TextContent, ToolUse, ToolParamName, toolParamNames, toolUseNames, ToolUseName } from "." // Assuming types are defined in index.ts or a similar file
import { AssistantMessageContent, TextContent, ToolUse, ToolParamName, toolParamNames, toolUseNames, ToolUseName } from "."
/**
* @description **Version 1**
* Parses an assistant message string potentially containing mixed text and tool usage blocks
* marked with XML-like tags into an array of structured content objects.
*
* This version iterates through the message character by character, building an accumulator string.
* It maintains state to track whether it's currently parsing text, a tool use block, or a specific tool parameter.
* It detects the start and end of tool uses and parameters by checking if the accumulator ends with
* the corresponding opening or closing tags.
* Special handling is included for `write_to_file` and `new_rule` tool uses to correctly parse
* the `content` parameter, which might contain the closing tag itself, by looking for the *last*
* occurrence of the closing tag.
* If the input string ends mid-tag or mid-content, the last block (text or tool use) is marked as partial.
*
* @param assistantMessage The raw string output from the assistant.
* @returns An array of `AssistantMessageContent` objects, which can be `TextContent` or `ToolUse`.
* Blocks that were not fully closed by the end of the input string will have their `partial` flag set to `true`.
*/
export function parseAssistantMessageV1(assistantMessage: string): AssistantMessageContent[] {
export function parseAssistantMessage(assistantMessage: string) {
const contentBlocks: AssistantMessageContent[] = []
let currentTextContent: TextContent | undefined = undefined
let currentTextContentStartIndex = 0
@@ -32,56 +14,46 @@ export function parseAssistantMessageV1(assistantMessage: string): AssistantMess
const char = assistantMessage[i]
accumulator += char
// --- State: Parsing a Tool Parameter ---
// there should not be a param without a tool use
if (currentToolUse && currentParamName) {
const currentParamValue = accumulator.slice(currentParamValueStartIndex)
const paramClosingTag = `</${currentParamName}>`
if (currentParamValue.endsWith(paramClosingTag)) {
// End of param value found
// end of param value
currentToolUse.params[currentParamName] = currentParamValue.slice(0, -paramClosingTag.length).trim()
currentParamName = undefined // Go back to parsing tool content or looking for next param
continue // Move to next character
currentParamName = undefined
continue
} else {
// Partial param value is accumulating
continue // Move to next character
// partial param value is accumulating
continue
}
}
// --- State: Parsing a Tool Use (but not a specific parameter) ---
// no currentParamName
if (currentToolUse) {
const currentToolValue = accumulator.slice(currentToolUseStartIndex)
const toolUseClosingTag = `</${currentToolUse.name}>`
if (currentToolValue.endsWith(toolUseClosingTag)) {
// End of a tool use found
// end of a tool use
currentToolUse.partial = false
contentBlocks.push(currentToolUse)
currentToolUse = undefined // Go back to parsing text or looking for next tool
// Reset text start index in case text follows immediately
currentTextContentStartIndex = i + 1
continue // Move to next character
currentToolUse = undefined
continue
} else {
// Check if starting a new parameter within the current tool use
const possibleParamOpeningTags = toolParamNames.map((name) => `<${name}>`)
let foundParamStart = false
for (const paramOpeningTag of possibleParamOpeningTags) {
if (accumulator.endsWith(paramOpeningTag)) {
// Start of a new parameter found
// start of a new parameter
currentParamName = paramOpeningTag.slice(1, -1) as ToolParamName
currentParamValueStartIndex = accumulator.length
foundParamStart = true
break
}
}
if (foundParamStart) {
continue // Move to next character
}
// Special case for write_to_file/new_rule content param allowing nested tags
// Check if a </content> tag appears, potentially indicating the end of the content param
// even if the main tool closing tag hasn't been seen yet.
// there's no current param, and not starting a new param
// special case for write_to_file where file contents could contain the closing tag, in which case the param would have closed and we end up with the rest of the file contents here. To work around this, we get the string between the starting content tag and the LAST content tag.
const contentParamName: ToolParamName = "content"
if (
(currentToolUse.name === "write_to_file" || currentToolUse.name === "new_rule") &&
@@ -91,384 +63,73 @@ export function parseAssistantMessageV1(assistantMessage: string): AssistantMess
const contentStartTag = `<${contentParamName}>`
const contentEndTag = `</${contentParamName}>`
const contentStartIndex = toolContent.indexOf(contentStartTag) + contentStartTag.length
// Use lastIndexOf to handle cases where </content> might appear within the content itself
const contentEndIndex = toolContent.lastIndexOf(contentEndTag)
// Ensure we found valid start/end tags and end is after start
if (
contentStartIndex !== -1 &&
contentEndIndex !== -1 &&
contentEndIndex > contentStartIndex - contentStartTag.length // Ensure end tag is after start tag begins
) {
// Check if this content param was already being parsed. If so, update it.
// If not, and we just found the closing tag, assign it.
// This handles cases where the </content> detection might fire before
// the <content> tag detection logic, or if the content is very short.
if (currentParamName === contentParamName) {
// Already parsing content, now we found the end tag
currentToolUse.params[contentParamName] = toolContent.slice(contentStartIndex, contentEndIndex).trim()
currentParamName = undefined // Finished with this param
} else if (currentParamName === undefined) {
// Not parsing a param, but found </content>. Assume it closes the content block.
currentToolUse.params[contentParamName] = toolContent.slice(contentStartIndex, contentEndIndex).trim()
// We stay in the "parsing tool use" state, looking for more params or the tool end tag.
}
if (contentStartIndex !== -1 && contentEndIndex !== -1 && contentEndIndex > contentStartIndex) {
currentToolUse.params[contentParamName] = toolContent.slice(contentStartIndex, contentEndIndex).trim()
}
}
// If none of the above, partial tool value is accumulating
continue // Move to next character
// partial tool value is accumulating
continue
}
}
// --- State: Parsing Text (or looking for start of a tool use) ---
// no currentToolUse
let didStartToolUse = false
const possibleToolUseOpeningTags = toolUseNames.map((name) => `<${name}>`)
for (const toolUseOpeningTag of possibleToolUseOpeningTags) {
if (accumulator.endsWith(toolUseOpeningTag)) {
// Start of a new tool use found
const toolName = toolUseOpeningTag.slice(1, -1) as ToolUseName
// start of a new tool use
currentToolUse = {
type: "tool_use",
name: toolName,
name: toolUseOpeningTag.slice(1, -1) as ToolUseName,
params: {},
partial: true,
}
currentToolUseStartIndex = accumulator.length
// This also indicates the end of the current text content block (if any)
// this also indicates the end of the current text content
if (currentTextContent) {
currentTextContent.partial = false
// Extract text content, removing the part that formed the tool opening tag
const textEndIndex = accumulator.length - toolUseOpeningTag.length
currentTextContent.content = accumulator.slice(currentTextContentStartIndex, textEndIndex).trim()
// Only add if there's actual content
if (currentTextContent.content.length > 0) {
contentBlocks.push(currentTextContent)
}
// remove the partially accumulated tool use tag from the end of text (<tool)
currentTextContent.content = currentTextContent.content
.slice(0, -toolUseOpeningTag.slice(0, -1).length)
.trim()
contentBlocks.push(currentTextContent)
currentTextContent = undefined
} else {
// Check if there was text before this tool use started
const textEndIndex = accumulator.length - toolUseOpeningTag.length
const potentialText = accumulator.slice(currentTextContentStartIndex, textEndIndex).trim()
if (potentialText.length > 0) {
contentBlocks.push({
type: "text",
content: potentialText,
partial: false, // Ended because tool use started
})
}
}
didStartToolUse = true
break // Found tool start, stop checking for others
break
}
}
if (!didStartToolUse) {
// No tool use started, so it must be text content accumulating
// (or continuing after a closed tool use)
// no tool use, so it must be text either at the beginning or between tools
if (currentTextContent === undefined) {
// Start of a new text block
currentTextContentStartIndex = i - (accumulator.length - currentTextContentStartIndex - 1) // Adjust start index based on how much we've accumulated since the last block ended or the beginning
// If accumulator starts from 0, start index is i
if (contentBlocks.length === 0 && currentToolUse === undefined) {
currentTextContentStartIndex = accumulator.length - 1 // i
} else {
// Re-calculate based on the actual start of the current text segment
// Find the end of the last block
let lastBlockEndIndex = 0
if (contentBlocks.length > 0) {
const lastBlock = contentBlocks[contentBlocks.length - 1]
// Approximation: find where the accumulator matches the end of the message string representation of the last block. This is complex.
// Simpler: Assume text starts right after the last block ended implicitly at index i.
lastBlockEndIndex = i // Where the loop *was* when the last block finished processing
// Need a more robust way to track the end index of the *raw string* corresponding to the last block.
// Let's stick to the accumulator slice approach for simplicity in this version.
// The start index should be where the current *unmatched* text began.
let lastProcessedIndex = -1
if (contentBlocks.length > 0) {
// This requires knowing the raw string length of the previous block, which V1 doesn't explicitly track easily.
// We'll approximate based on the current accumulator and start index logic.
// The issue arises if a tool tag was just closed. accumulator contains everything up to i.
// lastBlockEndIndex should point to the character *after* the closing tag of the last block.
}
// Reset start index to the beginning of the *current* potential text block
currentTextContentStartIndex = accumulator.length - 1 // Start accumulating from the current character `i`
}
// If we just closed a tool, text starts *after* its closing tag
// The logic needs refinement here for accurate start index after a tool closure.
// Let's assume for now the start index logic inside the loop handles it via slicing.
}
currentTextContent = {
type: "text",
content: "", // Content will be filled by slicing accumulator
partial: true,
}
currentTextContentStartIndex = i
}
currentTextContent = {
type: "text",
content: accumulator.slice(currentTextContentStartIndex).trim(),
partial: true,
}
// Update text content based on the accumulator from its start index
currentTextContent.content = accumulator.slice(currentTextContentStartIndex).trimStart() // Trim start to avoid leading space if text follows tool
}
} // End of loop
}
// --- Finalization after loop ---
// If a tool use was open at the end
if (currentToolUse) {
// If a parameter was open within that tool use
// stream did not complete tool call, add it as partial
if (currentParamName) {
// The remaining accumulator content belongs to this partial parameter
// tool call has a parameter that was not completed
currentToolUse.params[currentParamName] = accumulator.slice(currentParamValueStartIndex).trim()
}
// Add the potentially partial tool use block
contentBlocks.push(currentToolUse)
}
// If text content was being accumulated at the end
// Note: Only one of currentToolUse or currentTextContent can be defined here,
// as starting a tool use finalizes the preceding text block.
else if (currentTextContent) {
// Update content one last time
currentTextContent.content = accumulator.slice(currentTextContentStartIndex).trim()
// Add the potentially partial text block only if it contains content
if (currentTextContent.content.length > 0) {
contentBlocks.push(currentTextContent)
}
}
return contentBlocks
}
/**
* @description **Version 2**
* Parses an assistant message string potentially containing mixed text and tool usage blocks
* marked with XML-like tags into an array of structured content objects.
*
* This version aims for efficiency by avoiding the character-by-character accumulator of V1.
* It iterates through the string using an index `i`. At each position, it checks if the substring
* *ending* at `i` matches any known opening or closing tags for tools or parameters using `startsWith`
* with an offset.
* It uses pre-computed Maps (`toolUseOpenTags`, `toolParamOpenTags`) for quick tag lookups.
* State is managed using indices (`currentTextContentStart`, `currentToolUseStart`, `currentParamValueStart`)
* pointing to the start of the current block within the original `assistantMessage` string.
* Slicing is used to extract content only when a block (text, parameter, or tool use) is completed.
* Special handling for `write_to_file` and `new_rule` content parameters is included, using `indexOf`
* and `lastIndexOf` on the relevant slice to handle potentially nested closing tags.
* If the input string ends mid-block, the last open block is added and marked as partial.
*
* @param assistantMessage The raw string output from the assistant.
* @returns An array of `AssistantMessageContent` objects, which can be `TextContent` or `ToolUse`.
* Blocks that were not fully closed by the end of the input string will have their `partial` flag set to `true`.
*/
export function parseAssistantMessageV2(assistantMessage: string): AssistantMessageContent[] {
const contentBlocks: AssistantMessageContent[] = []
let currentTextContentStart = 0 // Index where the current text block started
let currentTextContent: TextContent | undefined = undefined
let currentToolUseStart = 0 // Index *after* the opening tag of the current tool use
let currentToolUse: ToolUse | undefined = undefined
let currentParamValueStart = 0 // Index *after* the opening tag of the current param
let currentParamName: ToolParamName | undefined = undefined
// Precompute tags for faster lookups
const toolUseOpenTags = new Map<string, ToolUseName>()
const toolParamOpenTags = new Map<string, ToolParamName>()
for (const name of toolUseNames) {
toolUseOpenTags.set(`<${name}>`, name)
}
for (const name of toolParamNames) {
toolParamOpenTags.set(`<${name}>`, name)
}
const len = assistantMessage.length
for (let i = 0; i < len; i++) {
const currentCharIndex = i
// --- State: Parsing a Tool Parameter ---
if (currentToolUse && currentParamName) {
const closeTag = `</${currentParamName}>`
// Check if the string *ending* at index `i` matches the closing tag
if (
currentCharIndex >= closeTag.length - 1 &&
assistantMessage.startsWith(
closeTag,
currentCharIndex - closeTag.length + 1, // Start checking from potential start of tag
)
) {
// Found the closing tag for the parameter
const value = assistantMessage
.slice(
currentParamValueStart, // Start after the opening tag
currentCharIndex - closeTag.length + 1, // End before the closing tag
)
.trim()
currentToolUse.params[currentParamName] = value
currentParamName = undefined // Go back to parsing tool content
// We don't continue loop here, need to check for tool close or other params at index i
} else {
continue // Still inside param value, move to next char
}
}
// --- State: Parsing a Tool Use (but not a specific parameter) ---
if (currentToolUse && !currentParamName) {
// Ensure we are not inside a parameter already
// Check if starting a new parameter
let startedNewParam = false
for (const [tag, paramName] of toolParamOpenTags.entries()) {
if (currentCharIndex >= tag.length - 1 && assistantMessage.startsWith(tag, currentCharIndex - tag.length + 1)) {
currentParamName = paramName
currentParamValueStart = currentCharIndex + 1 // Value starts after the tag
startedNewParam = true
break
}
}
if (startedNewParam) {
continue // Handled start of param, move to next char
}
// Check if closing the current tool use
const toolCloseTag = `</${currentToolUse.name}>`
if (
currentCharIndex >= toolCloseTag.length - 1 &&
assistantMessage.startsWith(toolCloseTag, currentCharIndex - toolCloseTag.length + 1)
) {
// End of the tool use found
// Special handling for content params *before* finalizing the tool
const toolContentSlice = assistantMessage.slice(
currentToolUseStart, // From after the tool opening tag
currentCharIndex - toolCloseTag.length + 1, // To before the tool closing tag
)
// Check if content parameter needs special handling (write_to_file/new_rule)
// This check is important if the closing </content> tag was missed by the parameter parsing logic
// (e.g., if content is empty or parsing logic prioritizes tool close)
const contentParamName: ToolParamName = "content"
if (
currentToolUse.name === "write_to_file" /* || currentToolUse.name === "new_rule" */ &&
toolContentSlice.includes(`<${contentParamName}>`)
) {
const contentStartTag = `<${contentParamName}>`
const contentEndTag = `</${contentParamName}>`
const contentStart = toolContentSlice.indexOf(contentStartTag)
// Use lastIndexOf for robustness against nested tags
const contentEnd = toolContentSlice.lastIndexOf(contentEndTag)
if (contentStart !== -1 && contentEnd !== -1 && contentEnd > contentStart) {
const contentValue = toolContentSlice.slice(contentStart + contentStartTag.length, contentEnd).trim()
currentToolUse.params[contentParamName] = contentValue
}
}
currentToolUse.partial = false // Mark as complete
contentBlocks.push(currentToolUse)
currentToolUse = undefined // Reset state
currentTextContentStart = currentCharIndex + 1 // Potential text starts after this tag
continue // Move to next char
}
// If not starting a param and not closing the tool, continue accumulating tool content implicitly
continue
}
// --- State: Parsing Text / Looking for Tool Start ---
if (!currentToolUse) {
// Check if starting a new tool use
let startedNewTool = false
for (const [tag, toolName] of toolUseOpenTags.entries()) {
if (currentCharIndex >= tag.length - 1 && assistantMessage.startsWith(tag, currentCharIndex - tag.length + 1)) {
// End current text block if one was active
if (currentTextContent) {
currentTextContent.content = assistantMessage
.slice(
currentTextContentStart, // From where text started
currentCharIndex - tag.length + 1, // To before the tool tag starts
)
.trim()
currentTextContent.partial = false // Ended because tool started
if (currentTextContent.content.length > 0) {
contentBlocks.push(currentTextContent)
}
currentTextContent = undefined
} else {
// Check for any text between the last block and this tag
const potentialText = assistantMessage
.slice(
currentTextContentStart, // From where text *might* have started
currentCharIndex - tag.length + 1, // To before the tool tag starts
)
.trim()
if (potentialText.length > 0) {
contentBlocks.push({
type: "text",
content: potentialText,
partial: false,
})
}
}
// Start the new tool use
currentToolUse = {
type: "tool_use",
name: toolName,
params: {},
partial: true, // Assume partial until closing tag is found
}
currentToolUseStart = currentCharIndex + 1 // Tool content starts after the opening tag
startedNewTool = true
break
}
}
if (startedNewTool) {
continue // Handled start of tool, move to next char
}
// If not starting a tool, it must be text content
if (!currentTextContent) {
// Start a new text block if we aren't already in one
currentTextContentStart = currentCharIndex // Text starts at the current character
// Check if the current char is the start of potential text *immediately* after a tag
// This needs the previous state - simpler to let slicing handle it later.
// Resetting start index accurately is key.
// It should be the index *after* the last processed tag.
// The logic managing currentTextContentStart after closing tags handles this.
currentTextContent = {
type: "text",
content: "", // Will be determined by slicing at the end or when a tool starts
partial: true,
}
}
// Continue accumulating text implicitly; content is extracted later.
}
} // End of loop
// --- Finalization after loop ---
// Finalize any open parameter within an open tool use
if (currentToolUse && currentParamName) {
currentToolUse.params[currentParamName] = assistantMessage
.slice(currentParamValueStart) // From param start to end of string
.trim()
// Tool use remains partial
}
// Finalize any open tool use (which might contain the finalized partial param)
if (currentToolUse) {
// Tool use is partial because the loop finished before its closing tag
contentBlocks.push(currentToolUse)
}
// Finalize any trailing text content
// Only possible if a tool use wasn't open at the very end
else if (currentTextContent) {
currentTextContent.content = assistantMessage
.slice(currentTextContentStart) // From text start to end of string
.trim()
// Text is partial because the loop finished
if (currentTextContent.content.length > 0) {
contentBlocks.push(currentTextContent)
}
// Note: it doesn't matter if check for currentToolUse or currentTextContent, only one of them will be defined since only one can be partial at a time
if (currentTextContent) {
// stream did not complete text content, add it as partial
contentBlocks.push(currentTextContent)
}
return contentBlocks
@@ -8,6 +8,45 @@ import { getGlobalState, getWorkspaceState, updateGlobalState, updateWorkspaceSt
import * as vscode from "vscode"
import { synchronizeRuleToggles, getRuleFilesTotalContent } from "@core/context/instructions/user-instructions/rule-helpers"
/**
* Converts .clinerules file to directory and places old .clinerule file inside directory, renaming it
* Doesn't do anything if .clinerules dir already exists or doesn't exist
* Returns whether there are any uncaught errors
*/
export async function ensureLocalClinerulesDirExists(cwd: string): Promise<boolean> {
const clinerulePath = path.resolve(cwd, GlobalFileNames.clineRules)
const defaultRuleFilename = "default-rules.md"
try {
const exists = await fileExistsAtPath(clinerulePath)
if (exists && !(await isDirectory(clinerulePath))) {
// logic to convert .clinerules file into directory, and rename the rules file to {defaultRuleFilename}
const content = await fs.readFile(clinerulePath, "utf8")
const tempPath = clinerulePath + ".bak"
await fs.rename(clinerulePath, tempPath) // create backup
try {
await fs.mkdir(clinerulePath, { recursive: true })
await fs.writeFile(path.join(clinerulePath, defaultRuleFilename), content, "utf8")
await fs.unlink(tempPath).catch(() => {}) // delete backup
return false // conversion successful with no errors
} catch (conversionError) {
// attempt to restore backup on conversion failure
try {
await fs.rm(clinerulePath, { recursive: true, force: true }).catch(() => {})
await fs.rename(tempPath, clinerulePath) // restore backup
} catch (restoreError) {}
return true // in either case here we consider this an error
}
}
// exists and is a dir or doesn't exist, either of these cases we dont need to handle here
return false
} catch (error) {
return true
}
}
export const getGlobalClineRules = async (globalClineRulesFilePath: string, toggles: ClineRulesToggles) => {
if (await fileExistsAtPath(globalClineRulesFilePath)) {
if (await isDirectory(globalClineRulesFilePath)) {
@@ -41,8 +80,7 @@ export const getLocalClineRules = async (cwd: string, toggles: ClineRulesToggles
if (await fileExistsAtPath(clineRulesFilePath)) {
if (await isDirectory(clineRulesFilePath)) {
try {
const rulesFilePaths = await readDirectory(clineRulesFilePath, [[".clinerules", "workflows"]])
const rulesFilePaths = await readDirectory(clineRulesFilePath)
const rulesFilesTotalContent = await getRuleFilesTotalContent(rulesFilePaths, cwd, toggles)
if (rulesFilesTotalContent) {
clineRulesFileInstructions = formatResponse.clineRulesLocalDirectoryInstructions(cwd, rulesFilesTotalContent)
@@ -83,9 +121,7 @@ export async function refreshClineRulesToggles(
// Local toggles
const localClineRulesToggles = ((await getWorkspaceState(context, "localClineRulesToggles")) as ClineRulesToggles) || {}
const localClineRulesFilePath = path.resolve(workingDirectory, GlobalFileNames.clineRules)
const updatedLocalToggles = await synchronizeRuleToggles(localClineRulesFilePath, localClineRulesToggles, "", [
[".clinerules", "workflows"],
])
const updatedLocalToggles = await synchronizeRuleToggles(localClineRulesFilePath, localClineRulesToggles)
await updateWorkspaceState(context, "localClineRulesToggles", updatedLocalToggles)
return {
@@ -93,3 +129,82 @@ export async function refreshClineRulesToggles(
localToggles: updatedLocalToggles,
}
}
export const createRuleFile = async (isGlobal: boolean, filename: string, cwd: string) => {
try {
let filePath: string
if (isGlobal) {
const globalClineRulesFilePath = await ensureRulesDirectoryExists()
filePath = path.join(globalClineRulesFilePath, filename)
} else {
const localClineRulesFilePath = path.resolve(cwd, GlobalFileNames.clineRules)
const hasError = await ensureLocalClinerulesDirExists(cwd)
if (hasError === true) {
return { filePath: null, fileExists: false }
}
await fs.mkdir(localClineRulesFilePath, { recursive: true })
filePath = path.join(localClineRulesFilePath, filename)
}
const fileExists = await fileExistsAtPath(filePath)
if (fileExists) {
return { filePath, fileExists }
}
await fs.writeFile(filePath, "", "utf8")
return { filePath, fileExists: false }
} catch (error) {
return { filePath: null, fileExists: false }
}
}
export async function deleteRuleFile(
context: vscode.ExtensionContext,
rulePath: string,
isGlobal: boolean,
): Promise<{ success: boolean; message: string }> {
try {
// Check if file exists
const fileExists = await fileExistsAtPath(rulePath)
if (!fileExists) {
return {
success: false,
message: `Rule file does not exist: ${rulePath}`,
}
}
// Delete the file from disk
await fs.unlink(rulePath)
// Get the filename for messages
const fileName = path.basename(rulePath)
// Update the appropriate toggles
if (isGlobal) {
const toggles = ((await getGlobalState(context, "globalClineRulesToggles")) as ClineRulesToggles) || {}
delete toggles[rulePath]
await updateGlobalState(context, "globalClineRulesToggles", toggles)
} else {
const toggles = ((await getWorkspaceState(context, "localClineRulesToggles")) as ClineRulesToggles) || {}
delete toggles[rulePath]
await updateWorkspaceState(context, "localClineRulesToggles", toggles)
}
return {
success: true,
message: `Rule file "${fileName}" deleted successfully`,
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
console.error(`Error deleting rule file: ${errorMessage}`, error)
return {
success: false,
message: `Failed to delete rule file.`,
}
}
}
@@ -1,21 +1,14 @@
import { fileExistsAtPath, isDirectory, readDirectory } from "@utils/fs"
import { ensureRulesDirectoryExists, GlobalFileNames } from "@core/storage/disk"
import { getGlobalState, getWorkspaceState, updateGlobalState, updateWorkspaceState } from "@core/storage/state"
import * as path from "path"
import fs from "fs/promises"
import { ClineRulesToggles } from "@shared/cline-rules"
import * as vscode from "vscode"
/**
* Recursively traverses directory and finds all files, including checking for optional whitelisted file extension
*/
export async function readDirectoryRecursive(
directoryPath: string,
allowedFileExtension: string,
excludedPaths: string[][] = [],
): Promise<string[]> {
export async function readDirectoryRecursive(directoryPath: string, allowedFileExtension: string): Promise<string[]> {
try {
const entries = await readDirectory(directoryPath, excludedPaths)
const entries = await readDirectory(directoryPath)
let results: string[] = []
for (const entry of entries) {
if (allowedFileExtension !== "") {
@@ -40,7 +33,6 @@ export async function synchronizeRuleToggles(
rulesDirectoryPath: string,
currentToggles: ClineRulesToggles,
allowedFileExtension: string = "",
excludedPaths: string[][] = [],
): Promise<ClineRulesToggles> {
// Create a copy of toggles to modify
const updatedToggles = { ...currentToggles }
@@ -53,7 +45,7 @@ export async function synchronizeRuleToggles(
if (isDir) {
// DIRECTORY CASE
const filePaths = await readDirectoryRecursive(rulesDirectoryPath, allowedFileExtension, excludedPaths)
const filePaths = await readDirectoryRecursive(rulesDirectoryPath, allowedFileExtension)
const existingRulePaths = new Set<string>()
for (const filePath of filePaths) {
@@ -127,155 +119,3 @@ export const getRuleFilesTotalContent = async (rulesFilePaths: string[], basePat
).then((contents) => contents.filter(Boolean).join("\n\n"))
return ruleFilesTotalContent
}
/**
* Handles converting any directory into a file (specifically used for .clinerules and .clinerules/workflows)
* The old .clinerules file or .clinerules/workflows file will be renamed to a default filename
* Doesn't do anything if the dir already exists or doesn't exist
* Returns whether there are any uncaught errors
*/
export async function ensureLocalClineDirExists(clinerulePath: string, defaultRuleFilename: string): Promise<boolean> {
try {
const exists = await fileExistsAtPath(clinerulePath)
if (exists && !(await isDirectory(clinerulePath))) {
// logic to convert .clinerules file into directory, and rename the rules file to {defaultRuleFilename}
const content = await fs.readFile(clinerulePath, "utf8")
const tempPath = clinerulePath + ".bak"
await fs.rename(clinerulePath, tempPath) // create backup
try {
await fs.mkdir(clinerulePath, { recursive: true })
await fs.writeFile(path.join(clinerulePath, defaultRuleFilename), content, "utf8")
await fs.unlink(tempPath).catch(() => {}) // delete backup
return false // conversion successful with no errors
} catch (conversionError) {
// attempt to restore backup on conversion failure
try {
await fs.rm(clinerulePath, { recursive: true, force: true }).catch(() => {})
await fs.rename(tempPath, clinerulePath) // restore backup
} catch (restoreError) {}
return true // in either case here we consider this an error
}
}
// exists and is a dir or doesn't exist, either of these cases we dont need to handle here
return false
} catch (error) {
return true
}
}
/**
* Create a rule file or workflow file
*/
export const createRuleFile = async (isGlobal: boolean, filename: string, cwd: string, type: string) => {
try {
let filePath: string
if (isGlobal) {
// global means its implicitly clinerules
const globalClineRulesFilePath = await ensureRulesDirectoryExists()
filePath = path.join(globalClineRulesFilePath, filename)
} else {
const localClineRulesFilePath = path.resolve(cwd, GlobalFileNames.clineRules)
const hasError = await ensureLocalClineDirExists(localClineRulesFilePath, "default-rules.md")
if (hasError === true) {
return { filePath: null, fileExists: false }
}
await fs.mkdir(localClineRulesFilePath, { recursive: true })
if (type === "workflow") {
const localWorkflowsFilePath = path.resolve(cwd, GlobalFileNames.workflows)
const hasError = await ensureLocalClineDirExists(localWorkflowsFilePath, "default-workflows.md")
if (hasError === true) {
return { filePath: null, fileExists: false }
}
await fs.mkdir(localWorkflowsFilePath, { recursive: true })
filePath = path.join(localWorkflowsFilePath, filename)
} else {
// clinerules file creation
filePath = path.join(localClineRulesFilePath, filename)
}
}
const fileExists = await fileExistsAtPath(filePath)
if (fileExists) {
return { filePath, fileExists }
}
await fs.writeFile(filePath, "", "utf8")
return { filePath, fileExists: false }
} catch (error) {
return { filePath: null, fileExists: false }
}
}
/**
* Delete a rule file or workflow file
*/
export async function deleteRuleFile(
context: vscode.ExtensionContext,
rulePath: string,
isGlobal: boolean,
type: string,
): Promise<{ success: boolean; message: string }> {
try {
// Check if file exists
const fileExists = await fileExistsAtPath(rulePath)
if (!fileExists) {
return {
success: false,
message: `File does not exist: ${rulePath}`,
}
}
// Delete the file from disk
await fs.unlink(rulePath)
// Get the filename for messages
const fileName = path.basename(rulePath)
// Update the appropriate toggles
if (isGlobal) {
const toggles = ((await getGlobalState(context, "globalClineRulesToggles")) as ClineRulesToggles) || {}
delete toggles[rulePath]
await updateGlobalState(context, "globalClineRulesToggles", toggles)
} else {
if (type === "workflow") {
const toggles = ((await getWorkspaceState(context, "workflowToggles")) as ClineRulesToggles) || {}
delete toggles[rulePath]
await updateWorkspaceState(context, "workflowToggles", toggles)
} else if (type === "cursor") {
const toggles = ((await getWorkspaceState(context, "localCursorRulesToggles")) as ClineRulesToggles) || {}
delete toggles[rulePath]
await updateWorkspaceState(context, "localCursorRulesToggles", toggles)
} else if (type === "windsurf") {
const toggles = ((await getWorkspaceState(context, "localWindsurfRulesToggles")) as ClineRulesToggles) || {}
delete toggles[rulePath]
await updateWorkspaceState(context, "localWindsurfRulesToggles", toggles)
} else {
const toggles = ((await getWorkspaceState(context, "localClineRulesToggles")) as ClineRulesToggles) || {}
delete toggles[rulePath]
await updateWorkspaceState(context, "localClineRulesToggles", toggles)
}
}
return {
success: true,
message: `File "${fileName}" deleted successfully`,
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
console.error(`Error deleting file: ${errorMessage}`, error)
return {
success: false,
message: `Failed to delete file.`,
}
}
}
@@ -1,20 +0,0 @@
import path from "path"
import { GlobalFileNames } from "@core/storage/disk"
import { ClineRulesToggles } from "@shared/cline-rules"
import { getWorkspaceState, updateWorkspaceState } from "@core/storage/state"
import * as vscode from "vscode"
import { synchronizeRuleToggles } from "@core/context/instructions/user-instructions/rule-helpers"
/**
* Refresh the workflow toggles
*/
export async function refreshWorkflowToggles(
context: vscode.ExtensionContext,
workingDirectory: string,
): Promise<ClineRulesToggles> {
const workflowRulesToggles = ((await getWorkspaceState(context, "workflowToggles")) as ClineRulesToggles) || {}
const workflowsDirPath = path.resolve(workingDirectory, GlobalFileNames.workflows)
const updatedWorkflowToggles = await synchronizeRuleToggles(workflowsDirPath, workflowRulesToggles)
await updateWorkspaceState(context, "workflowToggles", updatedWorkflowToggles)
return updatedWorkflowToggles
}
+10 -22
View File
@@ -1,13 +1,14 @@
import { Controller } from ".."
import { RuleFileRequest, RuleFile } from "@shared/proto/file"
import { FileMethodHandler } from "./index"
import { refreshClineRulesToggles } from "@core/context/instructions/user-instructions/cline-rules"
import { createRuleFile as createRuleFileImpl } from "@core/context/instructions/user-instructions/rule-helpers"
import {
createRuleFile as createRuleFileImpl,
refreshClineRulesToggles,
} from "@core/context/instructions/user-instructions/cline-rules"
import * as vscode from "vscode"
import * as path from "path"
import { handleFileServiceRequest } from "./index"
import { cwd } from "@core/task"
import { refreshWorkflowToggles } from "@/core/context/instructions/user-instructions/workflows"
/**
* Creates a rule file in either global or workspace rules directory
@@ -17,45 +18,32 @@ import { refreshWorkflowToggles } from "@/core/context/instructions/user-instruc
* @throws Error if operation fails
*/
export const createRuleFile: FileMethodHandler = async (controller: Controller, request: RuleFileRequest): Promise<RuleFile> => {
if (
typeof request.isGlobal !== "boolean" ||
!request.filename ||
typeof request.filename !== "string" ||
!request.type ||
typeof request.type !== "string"
) {
if (typeof request.isGlobal !== "boolean" || typeof request.filename !== "string" || !request.filename) {
console.error("createRuleFile: Missing or invalid parameters", {
isGlobal: typeof request.isGlobal === "boolean" ? request.isGlobal : `Invalid: ${typeof request.isGlobal}`,
filename: typeof request.filename === "string" ? request.filename : `Invalid: ${typeof request.filename}`,
type: typeof request.type === "string" ? request.type : `Invalid: ${typeof request.type}`,
})
throw new Error("Missing or invalid parameters")
}
const { filePath, fileExists } = await createRuleFileImpl(request.isGlobal, request.filename, cwd, request.type)
const { filePath, fileExists } = await createRuleFileImpl(request.isGlobal, request.filename, cwd)
if (!filePath) {
throw new Error("Failed to create file.")
throw new Error("Failed to create rule file.")
}
const fileTypeName = request.type === "workflow" ? "workflow" : "rule"
if (fileExists) {
vscode.window.showWarningMessage(`${fileTypeName} file "${request.filename}" already exists.`)
vscode.window.showWarningMessage(`Rule file "${request.filename}" already exists.`)
// Still open it for editing
await handleFileServiceRequest(controller, "openFile", { value: filePath })
} else {
if (request.type === "workflow") {
await refreshWorkflowToggles(controller.context, cwd)
} else {
await refreshClineRulesToggles(controller.context, cwd)
}
await refreshClineRulesToggles(controller.context, cwd)
await controller.postStateToWebview()
await handleFileServiceRequest(controller, "openFile", { value: filePath })
vscode.window.showInformationMessage(
`Created new ${request.isGlobal ? "global" : "workspace"} ${fileTypeName} file: ${request.filename}`,
`Created new ${request.isGlobal ? "global" : "workspace"} rule file: ${request.filename}`,
)
}
+9 -20
View File
@@ -1,10 +1,11 @@
import { Controller } from ".."
import { RuleFileRequest, RuleFile } from "@shared/proto/file"
import { FileMethodHandler } from "./index"
import { refreshClineRulesToggles } from "@core/context/instructions/user-instructions/cline-rules"
import { deleteRuleFile as deleteRuleFileImpl } from "@core/context/instructions/user-instructions/rule-helpers"
import {
deleteRuleFile as deleteRuleFileImpl,
refreshClineRulesToggles,
} from "@core/context/instructions/user-instructions/cline-rules"
import { refreshExternalRulesToggles } from "@core/context/instructions/user-instructions/external-rules"
import { refreshWorkflowToggles } from "@core/context/instructions/user-instructions/workflows"
import * as vscode from "vscode"
import * as path from "path"
import { cwd } from "@core/task"
@@ -17,38 +18,26 @@ import { cwd } from "@core/task"
* @throws Error if operation fails
*/
export const deleteRuleFile: FileMethodHandler = async (controller: Controller, request: RuleFileRequest): Promise<RuleFile> => {
if (
typeof request.isGlobal !== "boolean" ||
typeof request.rulePath !== "string" ||
!request.rulePath ||
!request.type ||
typeof request.type !== "string"
) {
if (typeof request.isGlobal !== "boolean" || typeof request.rulePath !== "string" || !request.rulePath) {
console.error("deleteRuleFile: Missing or invalid parameters", {
isGlobal: typeof request.isGlobal === "boolean" ? request.isGlobal : `Invalid: ${typeof request.isGlobal}`,
rulePath: typeof request.rulePath === "string" ? request.rulePath : `Invalid: ${typeof request.rulePath}`,
type: typeof request.type === "string" ? request.type : `Invalid: ${typeof request.type}`,
})
throw new Error("Missing or invalid parameters")
}
const result = await deleteRuleFileImpl(controller.context, request.rulePath, request.isGlobal, request.type)
const result = await deleteRuleFileImpl(controller.context, request.rulePath, request.isGlobal)
if (!result.success) {
throw new Error(result.message || "Failed to delete rule file")
}
// we refresh inside of the deleteRuleFileImpl(..) call
//await refreshClineRulesToggles(controller.context, cwd)
//await refreshExternalRulesToggles(controller.context, cwd)
//await refreshWorkflowToggles(controller.context, cwd)
await refreshClineRulesToggles(controller.context, cwd)
await refreshExternalRulesToggles(controller.context, cwd)
await controller.postStateToWebview()
const fileName = path.basename(request.rulePath)
const fileTypeName = request.type === "workflow" ? "workflow" : "rule"
vscode.window.showInformationMessage(`${fileTypeName} file "${fileName}" deleted successfully`)
vscode.window.showInformationMessage(`Rule file "${fileName}" deleted successfully`)
return RuleFile.create({
filePath: request.rulePath,
-2
View File
@@ -10,7 +10,6 @@ import { openFile } from "./openFile"
import { openImage } from "./openImage"
import { searchCommits } from "./searchCommits"
import { searchFiles } from "./searchFiles"
import { selectImages } from "./selectImages"
// Register all file service methods
export function registerAllMethods(): void {
@@ -22,5 +21,4 @@ export function registerAllMethods(): void {
registerMethod("openImage", openImage)
registerMethod("searchCommits", searchCommits)
registerMethod("searchFiles", searchFiles)
registerMethod("selectImages", selectImages)
}
-21
View File
@@ -1,21 +0,0 @@
import { Controller } from ".."
import { EmptyRequest, StringArray } from "@shared/proto/common"
import { selectImages as selectImagesIntegration } from "@integrations/misc/process-images"
import { FileMethodHandler } from "./index"
/**
* Prompts the user to select images from the file system and returns them as data URLs
* @param controller The controller instance
* @param request Empty request, no parameters needed
* @returns Array of image data URLs
*/
export const selectImages: FileMethodHandler = async (controller: Controller, request: EmptyRequest): Promise<StringArray> => {
try {
const images = await selectImagesIntegration()
return StringArray.create({ values: images })
} catch (error) {
console.error("Error selecting images:", error)
// Return empty array on error
return StringArray.create({ values: [] })
}
}
@@ -12,7 +12,6 @@ import { handleStateServiceRequest, handleStateServiceStreamingRequest } from ".
import { handleTaskServiceRequest, handleTaskServiceStreamingRequest } from "./task/index"
import { handleWebServiceRequest, handleWebServiceStreamingRequest } from "./web/index"
import { handleModelsServiceRequest, handleModelsServiceStreamingRequest } from "./models/index"
import { handleSlashServiceRequest, handleSlashServiceStreamingRequest } from "./slash/index"
/**
* Configuration for a service handler
@@ -68,8 +67,4 @@ export const serviceHandlers: Record<string, ServiceHandlerConfig> = {
requestHandler: handleModelsServiceRequest,
streamingHandler: handleModelsServiceStreamingRequest,
},
"cline.SlashService": {
requestHandler: handleSlashServiceRequest,
streamingHandler: handleSlashServiceStreamingRequest,
},
}
+139 -29
View File
@@ -14,6 +14,7 @@ import { cleanupLegacyCheckpoints } from "@integrations/checkpoints/CheckpointMi
import { downloadTask } from "@integrations/misc/export-markdown"
import { fetchOpenGraphData } from "@integrations/misc/link-preview"
import { handleFileServiceRequest } from "./file"
import { selectImages } from "@integrations/misc/process-images"
import { getTheme } from "@integrations/theme/getTheme"
import WorkspaceTracker from "@integrations/workspace/WorkspaceTracker"
import { ClineAccountService } from "@services/account/ClineAccountService"
@@ -50,7 +51,6 @@ import { ClineRulesToggles } from "@shared/cline-rules"
import { sendStateUpdate } from "./state/subscribeToState"
import { refreshClineRulesToggles } from "@core/context/instructions/user-instructions/cline-rules"
import { refreshExternalRulesToggles } from "@core/context/instructions/user-instructions/external-rules"
import { refreshWorkflowToggles } from "@core/context/instructions/user-instructions/workflows"
/*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -146,18 +146,8 @@ export class Controller {
chatSettings,
shellIntegrationTimeout,
enableCheckpointsSetting,
isNewUser,
taskHistory,
} = await getAllExtensionState(this.context)
const NEW_USER_TASK_COUNT_THRESHOLD = 10
// Check if the user has completed enough tasks to no longer be considered a "new user"
if (isNewUser && !historyItem && taskHistory && taskHistory.length >= NEW_USER_TASK_COUNT_THRESHOLD) {
await updateGlobalState(this.context, "isNewUser", false)
await this.postStateToWebview()
}
if (autoApprovalSettings) {
const updatedAutoApprovalSettings = {
...autoApprovalSettings,
@@ -261,7 +251,7 @@ export class Controller {
// If user already opted in to telemetry, enable telemetry service
this.getStateToPostToWebview().then((state) => {
const { telemetrySetting } = state
const isOptedIn = telemetrySetting !== "disabled"
const isOptedIn = telemetrySetting === "enabled"
telemetryService.updateTelemetryState(isOptedIn)
})
break
@@ -283,6 +273,12 @@ export class Controller {
// initializing new instance of Cline will make sure that any agentically running promises in old instance don't affect our new task. this essentially creates a fresh slate for the new task
await this.initTask(message.text, message.images)
break
case "condense":
this.task?.handleWebviewAskResponse("yesButtonClicked")
break
case "reportBug":
this.task?.handleWebviewAskResponse("yesButtonClicked")
break
case "apiConfiguration":
if (message.apiConfiguration) {
await updateApiConfiguration(this.context, message.apiConfiguration)
@@ -323,17 +319,26 @@ export class Controller {
const browserSession = new BrowserSession(this.context, browserSettings)
await browserSession.relaunchChromeDebugMode(this)
break
case "askResponse":
this.task?.handleWebviewAskResponse(message.askResponse!, message.text, message.images)
break
case "didShowAnnouncement":
await updateGlobalState(this.context, "lastShownAnnouncementId", this.latestAnnouncementId)
await this.postStateToWebview()
break
case "selectImages":
const images = await selectImages()
await this.postMessageToWebview({
type: "selectedImages",
images,
})
break
case "resetState":
await this.resetState()
break
case "refreshClineRules":
await refreshClineRulesToggles(this.context, cwd)
await refreshExternalRulesToggles(this.context, cwd)
await refreshWorkflowToggles(this.context, cwd)
await this.postStateToWebview()
break
case "openInBrowser":
@@ -376,6 +381,19 @@ export class Controller {
await this.fetchMcpMarketplace(message.bool)
break
}
case "downloadMcp": {
if (message.mcpId) {
// 1. Toggle to act mode if we are in plan mode
const { chatSettings } = await this.getStateToPostToWebview()
if (chatSettings.mode === "plan") {
await this.togglePlanActModeWithChatSettings({ mode: "act" })
}
// 2. download MCP
await this.downloadMcp(message.mcpId)
}
break
}
case "silentlyRefreshMcpMarketplace": {
await this.silentlyRefreshMcpMarketplace()
break
@@ -485,16 +503,6 @@ export class Controller {
}
break
}
case "toggleWorkflow": {
const { workflowPath, enabled } = message
if (workflowPath && typeof enabled === "boolean") {
const toggles = ((await getWorkspaceState(this.context, "workflowToggles")) as ClineRulesToggles) || {}
toggles[workflowPath] = enabled
await updateWorkspaceState(this.context, "workflowToggles", toggles)
await this.postStateToWebview()
}
break
}
case "requestTotalTasksSize": {
this.refreshTotalTasksSize()
break
@@ -619,6 +627,27 @@ export class Controller {
this.postMessageToWebview({ type: "relinquishControl" })
break
}
case "toggleFavoriteModel": {
if (message.modelId) {
const { apiConfiguration } = await getAllExtensionState(this.context)
const favoritedModelIds = apiConfiguration.favoritedModelIds || []
// Toggle favorite status
const updatedFavorites = favoritedModelIds.includes(message.modelId)
? favoritedModelIds.filter((id) => id !== message.modelId)
: [...favoritedModelIds, message.modelId]
await updateGlobalState(this.context, "favoritedModelIds", updatedFavorites)
// Capture telemetry for model favorite toggle
const isFavorited = !favoritedModelIds.includes(message.modelId)
telemetryService.captureModelFavoritesUsage(message.modelId, isFavorited)
// Post state to webview without changing any other configuration
await this.postStateToWebview()
}
break
}
case "grpc_request": {
if (message.grpc_request) {
await handleGrpcRequest(this, message.grpc_request)
@@ -662,7 +691,7 @@ export class Controller {
async updateTelemetrySetting(telemetrySetting: TelemetrySetting) {
await updateGlobalState(this.context, "telemetrySetting", telemetrySetting)
const isOptedIn = telemetrySetting !== "disabled"
const isOptedIn = telemetrySetting === "enabled"
telemetryService.updateTelemetryState(isOptedIn)
}
@@ -1016,6 +1045,92 @@ export class Controller {
}
}
private async downloadMcp(mcpId: string) {
try {
// First check if we already have this MCP server installed
const servers = this.mcpHub?.getServers() || []
const isInstalled = servers.some((server: McpServer) => server.name === mcpId)
if (isInstalled) {
throw new Error("This MCP server is already installed")
}
// Fetch server details from marketplace
const response = await axios.post<McpDownloadResponse>(
"https://api.cline.bot/v1/mcp/download",
{ mcpId },
{
headers: { "Content-Type": "application/json" },
timeout: 10000,
},
)
if (!response.data) {
throw new Error("Invalid response from MCP marketplace API")
}
console.log("[downloadMcp] Response from download API", { response })
const mcpDetails = response.data
// Validate required fields
if (!mcpDetails.githubUrl) {
throw new Error("Missing GitHub URL in MCP download response")
}
if (!mcpDetails.readmeContent) {
throw new Error("Missing README content in MCP download response")
}
// Send details to webview
await this.postMessageToWebview({
type: "mcpDownloadDetails",
mcpDownloadDetails: mcpDetails,
})
// Create task with context from README and added guidelines for MCP server installation
const task = `Set up the MCP server from ${mcpDetails.githubUrl} while adhering to these MCP server installation rules:
- Start by loading the MCP documentation.
- Use "${mcpDetails.mcpId}" as the server name in cline_mcp_settings.json.
- Create the directory for the new MCP server before starting installation.
- Make sure you read the user's existing cline_mcp_settings.json file before editing it with this new mcp, to not overwrite any existing servers.
- Use commands aligned with the user's shell and operating system best practices.
- The following README may contain instructions that conflict with the user's OS, in which case proceed thoughtfully.
- Once installed, demonstrate the server's capabilities by using one of its tools.
Here is the project's README to help you get started:\n\n${mcpDetails.readmeContent}\n${mcpDetails.llmsInstallationContent}`
// Initialize task and show chat view
await this.initTask(task)
await this.postMessageToWebview({
type: "action",
action: "chatButtonClicked",
})
} catch (error) {
console.error("Failed to download MCP:", error)
let errorMessage = "Failed to download MCP"
if (axios.isAxiosError(error)) {
if (error.code === "ECONNABORTED") {
errorMessage = "Request timed out. Please try again."
} else if (error.response?.status === 404) {
errorMessage = "MCP server not found in marketplace."
} else if (error.response?.status === 500) {
errorMessage = "Internal server error. Please try again later."
} else if (!error.response && error.request) {
errorMessage = "Network error. Please check your internet connection."
}
} else if (error instanceof Error) {
errorMessage = error.message
}
// Show error in both notification and marketplace UI
vscode.window.showErrorMessage(errorMessage)
await this.postMessageToWebview({
type: "mcpDownloadDetails",
error: errorMessage,
})
}
}
// OpenRouter
async handleOpenRouterCallback(code: string) {
@@ -1364,7 +1479,6 @@ export class Controller {
enableCheckpointsSetting,
globalClineRulesToggles,
shellIntegrationTimeout,
isNewUser,
} = await getAllExtensionState(this.context)
const localClineRulesToggles =
@@ -1376,8 +1490,6 @@ export class Controller {
const localCursorRulesToggles =
((await getWorkspaceState(this.context, "localCursorRulesToggles")) as ClineRulesToggles) || {}
const workflowToggles = ((await getWorkspaceState(this.context, "workflowToggles")) as ClineRulesToggles) || {}
return {
version: this.context.extension?.packageJSON?.version ?? "",
apiConfiguration,
@@ -1405,9 +1517,7 @@ export class Controller {
localClineRulesToggles: localClineRulesToggles || {},
localWindsurfRulesToggles: localWindsurfRulesToggles || {},
localCursorRulesToggles: localCursorRulesToggles || {},
workflowToggles: workflowToggles || {},
shellIntegrationTimeout,
isNewUser,
}
}
-114
View File
@@ -1,114 +0,0 @@
import { Controller } from ".."
import { Empty, StringRequest } from "../../../shared/proto/common"
import { McpServer, McpDownloadResponse } from "@shared/mcp"
import axios from "axios"
import * as vscode from "vscode"
/**
* Download an MCP server from the marketplace
* @param controller The controller instance
* @param request The request containing the MCP ID
* @returns Empty response
*/
export async function downloadMcp(controller: Controller, request: StringRequest): Promise<Empty> {
try {
// Check if mcpId is provided
if (!request.value) {
throw new Error("MCP ID is required")
}
const mcpId = request.value
// Check if we already have this MCP server installed
const servers = controller.mcpHub?.getServers() || []
const isInstalled = servers.some((server: McpServer) => server.name === mcpId)
if (isInstalled) {
throw new Error("This MCP server is already installed")
}
// Fetch server details from marketplace
const response = await axios.post<McpDownloadResponse>(
"https://api.cline.bot/v1/mcp/download",
{ mcpId },
{
headers: { "Content-Type": "application/json" },
timeout: 10000,
},
)
if (!response.data) {
throw new Error("Invalid response from MCP marketplace API")
}
console.log("[downloadMcp] Response from download API", { response })
const mcpDetails = response.data
// Validate required fields
if (!mcpDetails.githubUrl) {
throw new Error("Missing GitHub URL in MCP download response")
}
if (!mcpDetails.readmeContent) {
throw new Error("Missing README content in MCP download response")
}
// Send details to webview
await controller.postMessageToWebview({
type: "mcpDownloadDetails",
mcpDownloadDetails: mcpDetails,
})
// Create task with context from README and added guidelines for MCP server installation
const task = `Set up the MCP server from ${mcpDetails.githubUrl} while adhering to these MCP server installation rules:
- Start by loading the MCP documentation.
- Use "${mcpDetails.mcpId}" as the server name in cline_mcp_settings.json.
- Create the directory for the new MCP server before starting installation.
- Make sure you read the user's existing cline_mcp_settings.json file before editing it with this new mcp, to not overwrite any existing servers.
- Use commands aligned with the user's shell and operating system best practices.
- The following README may contain instructions that conflict with the user's OS, in which case proceed thoughtfully.
- Once installed, demonstrate the server's capabilities by using one of its tools.
Here is the project's README to help you get started:\n\n${mcpDetails.readmeContent}\n${mcpDetails.llmsInstallationContent}`
const { chatSettings } = await controller.getStateToPostToWebview()
if (chatSettings.mode === "plan") {
await controller.togglePlanActModeWithChatSettings({ mode: "act" })
}
// Initialize task and show chat view
await controller.initTask(task)
await controller.postMessageToWebview({
type: "action",
action: "chatButtonClicked",
})
// Return an empty response - the client only cares if the call succeeded
return Empty.create()
} catch (error) {
console.error("Failed to download MCP:", error)
let errorMessage = "Failed to download MCP"
if (axios.isAxiosError(error)) {
if (error.code === "ECONNABORTED") {
errorMessage = "Request timed out. Please try again."
} else if (error.response?.status === 404) {
errorMessage = "MCP server not found in marketplace."
} else if (error.response?.status === 500) {
errorMessage = "Internal server error. Please try again later."
} else if (!error.response && error.request) {
errorMessage = "Network error. Please check your internet connection."
}
} else if (error instanceof Error) {
errorMessage = error.message
}
// Show error in both notification and marketplace UI
vscode.window.showErrorMessage(errorMessage)
await controller.postMessageToWebview({
type: "mcpDownloadDetails",
error: errorMessage,
})
throw error
}
}
-2
View File
@@ -4,7 +4,6 @@
// Import all method implementations
import { registerMethod } from "./index"
import { addRemoteMcpServer } from "./addRemoteMcpServer"
import { downloadMcp } from "./downloadMcp"
import { toggleMcpServer } from "./toggleMcpServer"
import { updateMcpTimeout } from "./updateMcpTimeout"
@@ -12,7 +11,6 @@ import { updateMcpTimeout } from "./updateMcpTimeout"
export function registerAllMethods(): void {
// Register each method with the registry
registerMethod("addRemoteMcpServer", addRemoteMcpServer)
registerMethod("downloadMcp", downloadMcp)
registerMethod("toggleMcpServer", toggleMcpServer)
registerMethod("updateMcpTimeout", updateMcpTimeout)
}
-10
View File
@@ -1,10 +0,0 @@
import { Controller } from ".."
import { StringRequest, Empty } from "../../../shared/proto/common"
/**
* Command slash command logic
*/
export async function condense(controller: Controller, request: StringRequest): Promise<Empty> {
await controller.task?.handleWebviewAskResponse("yesButtonClicked")
return Empty.create()
}
-22
View File
@@ -1,22 +0,0 @@
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
import { createServiceRegistry, ServiceMethodHandler, StreamingMethodHandler } from "../grpc-service"
import { StreamingResponseHandler } from "../grpc-handler"
import { registerAllMethods } from "./methods"
// Create slash service registry
const slashService = createServiceRegistry("slash")
// Export the method handler types and registration function
export type SlashMethodHandler = ServiceMethodHandler
export type SlashStreamingMethodHandler = StreamingMethodHandler
export const registerMethod = slashService.registerMethod
// Export the request handlers
export const handleSlashServiceRequest = slashService.handleRequest
export const handleSlashServiceStreamingRequest = slashService.handleStreamingRequest
export const isStreamingMethod = slashService.isStreamingMethod
// Register all slash methods
registerAllMethods()
-14
View File
@@ -1,14 +0,0 @@
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
// Import all method implementations
import { registerMethod } from "./index"
import { condense } from "./condense"
import { reportBug } from "./reportBug"
// Register all slash service methods
export function registerAllMethods(): void {
// Register each method with the registry
registerMethod("condense", condense)
registerMethod("reportBug", reportBug)
}
-10
View File
@@ -1,10 +0,0 @@
import { Controller } from ".."
import { StringRequest, Empty } from "../../../shared/proto/common"
/**
* Report bug slash command logic
*/
export async function reportBug(controller: Controller, request: StringRequest): Promise<Empty> {
await controller.task?.handleWebviewAskResponse("yesButtonClicked")
return Empty.create()
}
-2
View File
@@ -5,7 +5,6 @@
import { registerMethod } from "./index"
import { getLatestState } from "./getLatestState"
import { subscribeToState } from "./subscribeToState"
import { toggleFavoriteModel } from "./toggleFavoriteModel"
// Streaming methods for this service
export const streamingMethods = ["subscribeToState"]
@@ -15,5 +14,4 @@ export function registerAllMethods(): void {
// Register each method with the registry
registerMethod("getLatestState", getLatestState)
registerMethod("subscribeToState", subscribeToState, { isStreaming: true })
registerMethod("toggleFavoriteModel", toggleFavoriteModel)
}
@@ -1,46 +0,0 @@
import { telemetryService } from "@/services/posthog/telemetry/TelemetryService"
import { Controller } from ".."
import { Empty, StringRequest } from "../../../shared/proto/common"
import { updateGlobalState } from "@/core/storage/state"
/**
* Toggles a model's favorite status
* @param controller The controller instance
* @param request The request containing the model ID to toggle
* @returns An empty response
*/
export async function toggleFavoriteModel(controller: Controller, request: StringRequest): Promise<Empty> {
try {
if (!request.value) {
throw new Error("Model ID is required")
}
const modelId = request.value
const { apiConfiguration } = await controller.getStateToPostToWebview()
if (!apiConfiguration) {
throw new Error("API configuration not found")
}
const favoritedModelIds = apiConfiguration.favoritedModelIds || []
// Toggle favorite status
const updatedFavorites = favoritedModelIds.includes(modelId)
? favoritedModelIds.filter((id) => id !== modelId)
: [...favoritedModelIds, modelId]
await updateGlobalState(controller.context, "favoritedModelIds", updatedFavorites)
// Capture telemetry for model favorite toggle
const isFavorited = !favoritedModelIds.includes(modelId)
telemetryService.captureModelFavoritesUsage(modelId, isFavorited)
// Post state to webview without changing any other configuration
await controller.postStateToWebview()
return Empty.create()
} catch (error) {
console.error(`Failed to toggle favorite status for model ${request.value}:`, error)
throw error
}
}
-45
View File
@@ -1,45 +0,0 @@
import { Controller } from ".."
import { Empty } from "../../../shared/proto/common"
import { AskResponseRequest } from "../../../shared/proto/task"
import { ClineAskResponse } from "../../../shared/WebviewMessage"
/**
* Handles a response from the webview for a previous ask operation
*
* @param controller The controller instance
* @param request The request containing response type, optional text and optional images
* @returns Empty response
*/
export async function askResponse(controller: Controller, request: AskResponseRequest): Promise<Empty> {
try {
if (!controller.task) {
console.warn("askResponse: No active task to receive response")
return Empty.create()
}
// Map the string responseType to the ClineAskResponse enum
let responseType: ClineAskResponse
switch (request.responseType) {
case "yesButtonClicked":
responseType = "yesButtonClicked"
break
case "noButtonClicked":
responseType = "noButtonClicked"
break
case "messageResponse":
responseType = "messageResponse"
break
default:
console.warn(`askResponse: Unknown response type: ${request.responseType}`)
return Empty.create()
}
// Call the task's handler for webview responses
await controller.task.handleWebviewAskResponse(responseType, request.text, request.images)
return Empty.create()
} catch (error) {
console.error("Error in askResponse handler:", error)
throw error
}
}
+4 -32
View File
@@ -1,7 +1,6 @@
import { Controller } from ".."
import { GetTaskHistoryRequest, TaskHistoryArray } from "../../../shared/proto/task"
import { getGlobalState } from "../../storage/state"
import { getWorkspacePath, arePathsEqual } from "../../../utils/path"
/**
* Gets filtered task history
@@ -11,49 +10,22 @@ import { getWorkspacePath, arePathsEqual } from "../../../utils/path"
*/
export async function getTaskHistory(controller: Controller, request: GetTaskHistoryRequest): Promise<TaskHistoryArray> {
try {
const { favoritesOnly, currentWorkspaceOnly, searchQuery, sortBy } = request
const { favoritesOnly, searchQuery, sortBy } = request
// Get task history from global state
const taskHistory = ((await getGlobalState(controller.context, "taskHistory")) as any[]) || []
const workspacePath = getWorkspacePath()
// Apply filters
let filteredTasks = taskHistory.filter((item) => {
// Basic filter: must have timestamp and task content
const hasRequiredFields = item.ts && item.task
if (!hasRequiredFields) {
return false
}
// Apply favorites filter if requested
if (favoritesOnly && !item.isFavorited) {
return false
if (favoritesOnly && hasRequiredFields) {
return item.isFavorited === true
}
// Apply current workspace filter if requested
if (currentWorkspaceOnly) {
let isInWorkspace = false
// First check the cwdOnTaskInitialization property - Only present on tasks from this change forward
if (item.cwdOnTaskInitialization) {
if (arePathsEqual(item.cwdOnTaskInitialization, workspacePath)) {
isInWorkspace = true
}
}
// For tasks without cwdOnTaskInitialization, check the older shadowGitConfigWorkTree property
if (!isInWorkspace && item.shadowGitConfigWorkTree) {
if (arePathsEqual(item.shadowGitConfigWorkTree, workspacePath)) {
isInWorkspace = true
}
}
if (!isInWorkspace) {
return false
}
}
return true
return hasRequiredFields
})
// Apply search if provided
-2
View File
@@ -3,7 +3,6 @@
// Import all method implementations
import { registerMethod } from "./index"
import { askResponse } from "./askResponse"
import { cancelTask } from "./cancelTask"
import { clearTask } from "./clearTask"
import { deleteNonFavoritedTasks } from "./deleteNonFavoritedTasks"
@@ -17,7 +16,6 @@ import { toggleTaskFavorite } from "./toggleTaskFavorite"
// Register all task service methods
export function registerAllMethods(): void {
// Register each method with the registry
registerMethod("askResponse", askResponse)
registerMethod("cancelTask", cancelTask)
registerMethod("clearTask", clearTask)
registerMethod("deleteNonFavoritedTasks", deleteNonFavoritedTasks)
+7 -55
View File
@@ -1,16 +1,11 @@
import { newTaskToolResponse, condenseToolResponse, newRuleToolResponse, reportBugToolResponse } from "../prompts/commands"
import { ClineRulesToggles } from "@shared/cline-rules"
import fs from "fs/promises"
/**
* Processes text for slash commands and transforms them with appropriate instructions
* This is called after parseMentions() to process any slash commands in the user's message
*/
export async function parseSlashCommands(
text: string,
workflowToggles: ClineRulesToggles,
): Promise<{ processedText: string; needsClinerulesFileCheck: boolean }> {
const SUPPORTED_DEFAULT_COMMANDS = ["newtask", "smol", "compact", "newrule", "reportbug"]
export function parseSlashCommands(text: string): { processedText: string; needsClinerulesFileCheck: boolean } {
const SUPPORTED_COMMANDS = ["newtask", "smol", "compact", "newrule", "reportbug"]
const commandReplacements: Record<string, string> = {
newtask: newTaskToolResponse(),
@@ -22,10 +17,10 @@ export async function parseSlashCommands(
// this currently allows matching prepended whitespace prior to /slash-command
const tagPatterns = [
{ tag: "task", regex: /<task>(\s*\/([a-zA-Z0-9_\.-]+))(\s+.+?)?\s*<\/task>/is },
{ tag: "feedback", regex: /<feedback>(\s*\/([a-zA-Z0-9_\.-]+))(\s+.+?)?\s*<\/feedback>/is },
{ tag: "answer", regex: /<answer>(\s*\/([a-zA-Z0-9_\.-]+))(\s+.+?)?\s*<\/answer>/is },
{ tag: "user_message", regex: /<user_message>(\s*\/([a-zA-Z0-9_\.-]+))(\s+.+?)?\s*<\/user_message>/is },
{ tag: "task", regex: /<task>(\s*\/([a-zA-Z0-9_-]+))(\s+.+?)?\s*<\/task>/is },
{ tag: "feedback", regex: /<feedback>(\s*\/([a-zA-Z0-9_-]+))(\s+.+?)?\s*<\/feedback>/is },
{ tag: "answer", regex: /<answer>(\s*\/([a-zA-Z0-9_-]+))(\s+.+?)?\s*<\/answer>/is },
{ tag: "user_message", regex: /<user_message>(\s*\/([a-zA-Z0-9_-]+))(\s+.+?)?\s*<\/user_message>/is },
]
// if we find a valid match, we will return inside that block
@@ -39,8 +34,7 @@ export async function parseSlashCommands(
const commandName = match[2] // casing matters
// we give preference to the default commands if the user has a file with the same name
if (SUPPORTED_DEFAULT_COMMANDS.includes(commandName)) {
if (SUPPORTED_COMMANDS.includes(commandName)) {
const fullMatchStartIndex = match.index
// find position of slash command within the full match
@@ -57,48 +51,6 @@ export async function parseSlashCommands(
return { processedText: processedText, needsClinerulesFileCheck: commandName === "newrule" ? true : false }
}
// in practice we want to minimize this work, so we only do it if theres a possible match
const enabledWorkflows = Object.entries(workflowToggles)
.filter(([_, enabled]) => enabled)
.map(([filePath, _]) => {
const fileName = filePath.replace(/^.*[/\\]/, "")
return {
fullPath: filePath,
fileName: fileName,
}
})
// Then check if the command matches any enabled workflow filename
const matchingWorkflow = enabledWorkflows.find((workflow) => workflow.fileName === commandName)
if (matchingWorkflow) {
try {
// Read workflow file content from the full path
const workflowContent = (await fs.readFile(matchingWorkflow.fullPath, "utf8")).trim()
// find position of slash command within the full match
const fullMatchStartIndex = match.index
const fullMatch = match[0]
const relativeStartIndex = fullMatch.indexOf(match[1])
// calculate absolute indices in the original string
const slashCommandStartIndex = fullMatchStartIndex + relativeStartIndex
const slashCommandEndIndex = slashCommandStartIndex + match[1].length
// remove the slash command and add custom instructions at the top of this message
const textWithoutSlashCommand =
text.substring(0, slashCommandStartIndex) + text.substring(slashCommandEndIndex)
const processedText =
`<explicit_instructions type="${matchingWorkflow.fileName}">\n${workflowContent}\n</explicit_instructions>\n` +
textWithoutSlashCommand
return { processedText, needsClinerulesFileCheck: false }
} catch (error) {
console.error(`Error reading workflow file ${matchingWorkflow.fullPath}: ${error}`)
}
}
}
}
-1
View File
@@ -15,7 +15,6 @@ export const GlobalFileNames = {
openRouterModels: "openrouter_models.json",
mcpSettings: "cline_mcp_settings.json",
clineRules: ".clinerules",
workflows: ".clinerules/workflows",
cursorRulesDir: ".cursor/rules",
cursorRulesFile: ".cursorrules",
windsurfRules: ".windsurfrules",
-5
View File
@@ -11,7 +11,6 @@ export type SecretKey =
| "deepSeekApiKey"
| "requestyApiKey"
| "togetherApiKey"
| "fireworksApiKey"
| "qwenApiKey"
| "doubaoApiKey"
| "mistralApiKey"
@@ -70,9 +69,6 @@ export type GlobalStateKey =
| "liteLlmModelId"
| "liteLlmModelInfo"
| "liteLlmUsePromptCache"
| "fireworksModelId"
| "fireworksModelMaxCompletionTokens"
| "fireworksModelMaxTokens"
| "qwenApiLine"
| "requestyModelId"
| "requestyModelInfo"
@@ -88,6 +84,5 @@ export type GlobalStateKey =
| "favoritedModelIds"
| "requestTimeoutMs"
| "shellIntegrationTimeout"
| "isNewUser"
export type LocalStateKey = "localClineRulesToggles"
-16
View File
@@ -76,7 +76,6 @@ async function migrateEnableCheckpointsSetting(enableCheckpointsSettingRaw: bool
export async function getAllExtensionState(context: vscode.ExtensionContext) {
const [
isNewUser,
storedApiProvider,
apiModelId,
apiKey,
@@ -133,10 +132,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
liteLlmModelId,
liteLlmModelInfo,
liteLlmUsePromptCache,
fireworksApiKey,
fireworksModelId,
fireworksModelMaxCompletionTokens,
fireworksModelMaxTokens,
userInfo,
previousModeApiProvider,
previousModeModelId,
@@ -163,7 +158,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
enableCheckpointsSettingRaw,
mcpMarketplaceEnabledRaw,
] = await Promise.all([
getGlobalState(context, "isNewUser") as Promise<boolean | undefined>,
getGlobalState(context, "apiProvider") as Promise<ApiProvider | undefined>,
getGlobalState(context, "apiModelId") as Promise<string | undefined>,
getSecret(context, "apiKey") as Promise<string | undefined>,
@@ -220,10 +214,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
getGlobalState(context, "liteLlmModelId") as Promise<string | undefined>,
getGlobalState(context, "liteLlmModelInfo") as Promise<ModelInfo | undefined>,
getGlobalState(context, "liteLlmUsePromptCache") as Promise<boolean | undefined>,
getSecret(context, "fireworksApiKey") as Promise<string | undefined>,
getGlobalState(context, "fireworksModelId") as Promise<string | undefined>,
getGlobalState(context, "fireworksModelMaxCompletionTokens") as Promise<number | undefined>,
getGlobalState(context, "fireworksModelMaxTokens") as Promise<number | undefined>,
getGlobalState(context, "userInfo") as Promise<UserInfo | undefined>,
getGlobalState(context, "previousModeApiProvider") as Promise<ApiProvider | undefined>,
getGlobalState(context, "previousModeModelId") as Promise<string | undefined>,
@@ -345,10 +335,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
liteLlmModelInfo,
liteLlmApiKey,
liteLlmUsePromptCache,
fireworksApiKey,
fireworksModelId,
fireworksModelMaxCompletionTokens,
fireworksModelMaxTokens,
asksageApiKey,
asksageApiUrl,
xaiApiKey,
@@ -356,7 +342,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
favoritedModelIds,
requestTimeoutMs,
},
isNewUser: isNewUser ?? true,
lastShownAnnouncementId,
customInstructions,
taskHistory,
@@ -530,7 +515,6 @@ export async function resetExtensionState(context: vscode.ExtensionContext) {
"mistralApiKey",
"clineApiKey",
"liteLlmApiKey",
"fireworksApiKey",
"asksageApiKey",
"xaiApiKey",
"sambanovaApiKey",
+7 -17
View File
@@ -60,7 +60,7 @@ import { fileExistsAtPath } from "@utils/fs"
import { createAndOpenGitHubIssue } from "@utils/github-url-utils"
import { arePathsEqual, getReadablePath, isLocatedInWorkspace } from "@utils/path"
import { fixModelHtmlEscaping, removeInvalidChars } from "@utils/string"
import { AssistantMessageContent, parseAssistantMessageV2, ToolParamName, ToolUseName } from "@core/assistant-message"
import { AssistantMessageContent, parseAssistantMessage, ToolParamName, ToolUseName } from "@core/assistant-message"
import { constructNewFileContent } from "@core/assistant-message/diff"
import { ClineIgnoreController } from "@core/ignore/ClineIgnoreController"
import { parseMentions } from "@core/mentions"
@@ -80,7 +80,6 @@ import {
ensureTaskDirectoryExists,
getSavedApiConversationHistory,
getSavedClineMessages,
GlobalFileNames,
saveApiConversationHistory,
saveClineMessages,
} from "@core/storage/disk"
@@ -88,14 +87,13 @@ import {
getGlobalClineRules,
getLocalClineRules,
refreshClineRulesToggles,
ensureLocalClinerulesDirExists,
} from "@core/context/instructions/user-instructions/cline-rules"
import { ensureLocalClineDirExists } from "../context/instructions/user-instructions/rule-helpers"
import {
refreshExternalRulesToggles,
getLocalWindsurfRules,
getLocalCursorRules,
} from "@core/context/instructions/user-instructions/external-rules"
import { refreshWorkflowToggles } from "../context/instructions/user-instructions/workflows"
import { getGlobalState } from "@core/storage/state"
import { parseSlashCommands } from "@core/slash-commands"
import WorkspaceTracker from "@integrations/workspace/WorkspaceTracker"
@@ -327,7 +325,6 @@ export class Task {
totalCost: apiMetrics.totalCost,
size: taskDirSize,
shadowGitConfigWorkTree: await this.checkpointTracker?.getShadowGitConfigWorkTree(),
cwdOnTaskInitialization: cwd,
conversationHistoryDeletedRange: this.conversationHistoryDeletedRange,
isFavorited: this.taskIsFavorited,
})
@@ -600,9 +597,7 @@ export class Task {
}
async doesLatestTaskCompletionHaveNewChanges() {
if (!this.enableCheckpoints) {
return false
}
if (!this.enableCheckpoints) return false
const messageIndex = findLastIndex(this.clineMessages, (m) => m.say === "completion_result")
const message = this.clineMessages[messageIndex]
@@ -3313,7 +3308,7 @@ export class Task {
await this.say("user_feedback", text ?? "", images)
pushToolResult(
formatResponse.toolResult(
`The user did not submit the bug, and provided feedback on the Github issue generated instead:\n<feedback>\n${text}\n</feedback>`,
`The user provided feedback on the Github issue generated:\n<feedback>\n${text}\n</feedback>`,
images,
),
)
@@ -3915,7 +3910,7 @@ export class Task {
assistantMessage += chunk.text
// parse raw assistant message into content blocks
const prevLength = this.assistantMessageContent.length
this.assistantMessageContent = parseAssistantMessageV2(assistantMessage)
this.assistantMessageContent = parseAssistantMessage(assistantMessage)
if (this.assistantMessageContent.length > prevLength) {
this.userMessageContentReady = false // new content we need to present, reset to false in case previous content set this to true
}
@@ -4069,8 +4064,6 @@ export class Task {
// Track if we need to check clinerulesFile
let needsClinerulesFileCheck = false
const workflowToggles = await refreshWorkflowToggles(this.getContext(), cwd)
const processUserContent = async () => {
// This is a temporary solution to dynamically load context mentions from tool results. It checks for the presence of tags that indicate that the tool was rejected and feedback was provided (see formatToolDeniedFeedback, attemptCompletion, executeCommand, and consecutiveMistakeCount >= 3) or "<answer>" (see askFollowupQuestion), we place all user generated content in these tags so they can effectively be used as markers for when we should parse mentions). However if we allow multiple tools responses in the future, we will need to parse mentions specifically within the user content tags.
// (Note: this caused the @/ import alias bug where file contents were being parsed as well, since v2 converted tool results to text blocks)
@@ -4093,10 +4086,7 @@ export class Task {
)
// when parsing slash commands, we still want to allow the user to provide their desired context
const { processedText, needsClinerulesFileCheck: needsCheck } = await parseSlashCommands(
parsedText,
workflowToggles,
)
const { processedText, needsClinerulesFileCheck: needsCheck } = parseSlashCommands(parsedText)
if (needsCheck) {
needsClinerulesFileCheck = true
@@ -4122,7 +4112,7 @@ export class Task {
// After processing content, check clinerulesData if needed
let clinerulesError = false
if (needsClinerulesFileCheck) {
clinerulesError = await ensureLocalClineDirExists(cwd, GlobalFileNames.clineRules)
clinerulesError = await ensureLocalClinerulesDirExists(cwd)
}
// Return all results
-1
View File
@@ -328,7 +328,6 @@ export class WebviewProvider implements vscode.WebviewViewProvider {
<!DOCTYPE html>
<html lang="en">
<head>
<script src="http://localhost:8097"></script>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
<meta http-equiv="Content-Security-Policy" content="${csp.join("; ")}">
+9 -3
View File
@@ -91,9 +91,9 @@ export class DiffViewProvider {
const currentFirstVisibleLine = e.visibleRanges[0]?.start.line || 0
// If the first visible line moved upward, user scrolled up
// if (currentFirstVisibleLine < this.lastFirstVisibleLine) {
// this.shouldAutoScroll = false
// }
if (currentFirstVisibleLine < this.lastFirstVisibleLine) {
this.shouldAutoScroll = false
}
// Always update our tracking variable
this.lastFirstVisibleLine = currentFirstVisibleLine
@@ -433,6 +433,12 @@ export class DiffViewProvider {
// close editor if open?
async reset() {
// releasing memory by clearing the diff editor
try {
await this.closeAllDiffViews()
} catch (error) {
console.error("Error closing diff views:", error)
}
this.editType = undefined
this.isEditing = false
this.originalContent = undefined
@@ -108,47 +108,6 @@ export class TerminalManager {
if (disposable) {
this.disposables.push(disposable)
}
// Add a listener for terminal state changes to detect CWD updates
try {
const stateChangeDisposable = vscode.window.onDidChangeTerminalState((terminal) => {
const terminalInfo = this.findTerminalInfoByTerminal(terminal)
if (terminalInfo && terminalInfo.pendingCwdChange && terminalInfo.cwdResolved) {
// Check if CWD has been updated to match the expected path
if (this.isCwdMatchingExpected(terminalInfo)) {
const resolver = terminalInfo.cwdResolved.resolve
terminalInfo.pendingCwdChange = undefined
terminalInfo.cwdResolved = undefined
resolver()
}
}
})
this.disposables.push(stateChangeDisposable)
} catch (error) {
console.error("Error setting up onDidChangeTerminalState", error)
}
}
//Find a TerminalInfo by its VSCode Terminal instance
private findTerminalInfoByTerminal(terminal: vscode.Terminal): TerminalInfo | undefined {
const terminals = TerminalRegistry.getAllTerminals()
return terminals.find((t) => t.terminal === terminal)
}
//Check if a terminal's CWD matches its expected pending change
private isCwdMatchingExpected(terminalInfo: TerminalInfo): boolean {
if (!terminalInfo.pendingCwdChange) {
return false
}
const currentCwd = terminalInfo.terminal.shellIntegration?.cwd?.fsPath
const targetCwd = vscode.Uri.file(terminalInfo.pendingCwdChange).fsPath
if (!currentCwd) {
return false
}
return arePathsEqual(currentCwd, targetCwd)
}
runCommand(terminalInfo: TerminalInfo, command: string): TerminalProcessResultPromise {
@@ -237,37 +196,8 @@ export class TerminalManager {
// If no matching terminal exists, try to find any non-busy terminal
const availableTerminal = terminals.find((t) => !t.busy)
if (availableTerminal) {
// Set up promise and tracking for CWD change
const cwdPromise = new Promise<void>((resolve, reject) => {
availableTerminal.pendingCwdChange = cwd
availableTerminal.cwdResolved = { resolve, reject }
})
// Navigate back to the desired directory
await this.runCommand(availableTerminal, `cd "${cwd}"`)
// Either resolve immediately if CWD already updated or wait for event/timeout
if (this.isCwdMatchingExpected(availableTerminal)) {
if (availableTerminal.cwdResolved) {
availableTerminal.cwdResolved.resolve()
}
availableTerminal.pendingCwdChange = undefined
availableTerminal.cwdResolved = undefined
} else {
try {
// Wait with a timeout for state change event to resolve
await Promise.race([
cwdPromise,
new Promise<void>((_, reject) =>
setTimeout(() => reject(new Error(`CWD timeout: Failed to update to ${cwd}`)), 1000),
),
])
} catch (err) {
// Clear pending state on timeout
availableTerminal.pendingCwdChange = undefined
availableTerminal.cwdResolved = undefined
}
}
this.terminalIds.add(availableTerminal.id)
return availableTerminal
}
@@ -5,11 +5,6 @@ export interface TerminalInfo {
busy: boolean
lastCommand: string
id: number
pendingCwdChange?: string
cwdResolved?: {
resolve: () => void
reject: (error: Error) => void
}
}
// Although vscode.window.terminals provides a list of all open terminals, there's no way to know whether they're busy or not (exitStatus does not provide useful information for most commands). In order to prevent creating too many terminals, we need to keep track of terminals through the life of the extension, as well as session specific terminals for the life of a task (to get latest unretrieved output).
+32 -8
View File
@@ -16,7 +16,6 @@ import { discoverChromeInstances, testBrowserConnection, isPortOpen } from "./Br
import * as chromeLauncher from "chrome-launcher"
import { Controller } from "@core/controller"
import { telemetryService } from "@/services/posthog/telemetry/TelemetryService"
import os from "os"
interface PCRStats {
puppeteer: { launch: typeof launch }
@@ -124,20 +123,45 @@ export class BrowserSession {
}
async relaunchChromeDebugMode(controller: Controller) {
const result = await vscode.window.showWarningMessage(
"This will close your existing Chrome tabs and relaunch Chrome in debug mode. Are you sure?",
{ modal: true },
"Yes",
)
if (result !== "Yes") {
controller?.postMessageToWebview({
type: "browserRelaunchResult",
success: false,
text: "Operation cancelled by user",
})
return
}
try {
const userDataDir = path.join(os.tmpdir(), "chrome-debug-profile")
// Chrome-launcher's killAll only kills instances it launched
// We need to handle system Chrome processes separately
await this.killAllChromeBrowsers()
// Wait a moment for Chrome to fully shut down
await new Promise((resolve) => setTimeout(resolve, 500))
// Instead of using any default flags, use a minimal set to ensure session persistence
// This closely mimics running "google-chrome-stable --remote-debugging-port=9222" from the CLI
const chromeFlags = [
"--remote-debugging-port=" + DEBUG_PORT,
"--disable-notifications",
// Do not add any flags that might interfere with profile data
]
const installation = chromeLauncher.Launcher.getFirstInstallation()
if (!installation) {
throw new Error("Could not find Chrome installation on this system")
}
console.info("chrome installation", installation)
const args = [
`--remote-debugging-port=${DEBUG_PORT}`,
`--user-data-dir=${userDataDir}`,
"--disable-notifications",
"chrome://newtab",
]
// Prepare the command arguments
const args = [`--remote-debugging-port=${DEBUG_PORT}`, "--disable-notifications", "chrome://newtab"]
// Spawn Chrome as a detached process
const chromeProcess = spawn(installation, args, {
@@ -9,7 +9,6 @@ class PostHogClientProvider {
this.client = new PostHog(posthogConfig.apiKey, {
host: posthogConfig.host,
enableExceptionAutocapture: false,
defaultOptIn: false,
})
}
@@ -76,8 +76,6 @@ class PostHogClient {
BROWSER_TOOL_END: "task.browser_tool_end",
// Tracks when browser errors occur
BROWSER_ERROR: "task.browser_error",
// Tracks Gemini API specific performance metrics
GEMINI_API_PERFORMANCE: "task.gemini_api_performance",
// Collection of all task events
TASK_COLLECTION: "task.collection",
},
@@ -732,43 +730,6 @@ class PostHogClient {
)
}
/**
* Captures Gemini API performance metrics.
* @param taskId Unique identifier for the task
* @param modelId Specific Gemini model ID
* @param data Performance data including TTFT, durations, token counts, cache stats, and API success status
* @param collect If true, collect event instead of sending
*/
public captureGeminiApiPerformance(
taskId: string,
modelId: string,
data: {
ttftSec?: number
totalDurationSec?: number
promptTokens: number
outputTokens: number
cacheReadTokens: number
cacheHit: boolean
cacheHitPercentage?: number
apiSuccess: boolean
apiError?: string
throughputTokensPerSec?: number
},
collect: boolean = false,
) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.GEMINI_API_PERFORMANCE,
properties: {
taskId,
modelId,
...data,
},
},
collect,
)
}
/**
* Records when the user uses the model favorite button in the model picker
* @param model The name of the model the user has interacted with
+24 -31
View File
@@ -5,7 +5,6 @@ import { execa } from "execa"
import { Logger } from "@services/logging/Logger"
import { WebviewProvider } from "@core/webview"
import { AutoApprovalSettings } from "@shared/AutoApprovalSettings"
import { TaskServiceClient } from "webview-ui/src/services/grpc-client"
import {
getWorkspacePath,
validateWorkspacePath,
@@ -16,6 +15,7 @@ import {
import { updateGlobalState, getAllExtensionState, updateApiConfiguration, storeSecret } from "@core/storage/state"
import { ClineAsk, ExtensionMessage } from "@shared/ExtensionMessage"
import { ApiProvider } from "@shared/api"
import { WebviewMessage } from "@shared/WebviewMessage"
import { HistoryItem } from "@shared/HistoryItem"
import { getSavedClineMessages, getSavedApiConversationHistory } from "@core/storage/disk"
@@ -515,8 +515,8 @@ export function createMessageCatcher(webviewProvider: WebviewProvider): vscode.D
const askText = message.partialMessage.text
// Automatically respond to different types of asks
setTimeout(async () => {
await autoRespondToAsk(webviewProvider, askType, askText)
setTimeout(() => {
autoRespondToAsk(webviewProvider, askType, askText)
}, 100) // Small delay to ensure the message is processed first
}
@@ -538,64 +538,65 @@ export function createMessageCatcher(webviewProvider: WebviewProvider): vscode.D
* @param askType The type of ask message
* @param askText The text content of the ask message
*/
async function autoRespondToAsk(webviewProvider: WebviewProvider, askType: ClineAsk, askText?: string): Promise<void> {
function autoRespondToAsk(webviewProvider: WebviewProvider, askType: ClineAsk, askText?: string): void {
if (!webviewProvider.controller) {
return
}
Logger.log(`Auto-responding to ask type: ${askType}`)
// Default to approving most actions
let responseType = "yesButtonClicked"
let responseText: string | undefined
let responseImages: string[] | undefined
// Create a response message based on the ask type
const response: WebviewMessage = {
type: "askResponse",
askResponse: "yesButtonClicked", // Default to approving most actions
}
// Handle specific ask types differently if needed
switch (askType) {
case "followup":
// For follow-up questions, provide a generic response
responseType = "messageResponse"
responseText = "I can't answer any questions right now, use your best judgment."
response.askResponse = "messageResponse"
response.text = "I can't answer any questions right now, use your best judgment."
break
case "api_req_failed":
// Always retry API requests
responseType = "yesButtonClicked" // "Retry" button
response.askResponse = "yesButtonClicked" // "Retry" button
break
case "completion_result":
// Accept the completion
responseType = "messageResponse"
responseText = "Task completed successfully."
response.askResponse = "messageResponse"
response.text = "Task completed successfully."
break
case "mistake_limit_reached":
// Provide guidance to continue
responseType = "messageResponse"
responseText = "Try breaking down the task into smaller steps."
response.askResponse = "messageResponse"
response.text = "Try breaking down the task into smaller steps."
break
case "auto_approval_max_req_reached":
// Reset the count to continue
responseType = "yesButtonClicked" // "Reset and continue" button
response.askResponse = "yesButtonClicked" // "Reset and continue" button
break
case "resume_task":
case "resume_completed_task":
// Resume the task
responseType = "messageResponse"
response.askResponse = "messageResponse"
break
case "new_task":
// Decline creating a new task to keep the current task running
responseType = "messageResponse"
responseText = "Continue with the current task."
response.askResponse = "messageResponse"
response.text = "Continue with the current task."
break
case "plan_mode_respond":
// Respond to plan mode with a message to toggle to Act mode
responseType = "messageResponse"
responseText = "PLAN_MODE_TOGGLE_RESPONSE" // Special marker to toggle to Act mode
response.askResponse = "messageResponse"
response.text = "PLAN_MODE_TOGGLE_RESPONSE" // Special marker to toggle to Act mode
// Automatically toggle to Act mode after responding
setTimeout(async () => {
@@ -615,16 +616,8 @@ async function autoRespondToAsk(webviewProvider: WebviewProvider, askType: Cline
}
// Send the response message
try {
await TaskServiceClient.askResponse({
responseType,
text: responseText,
images: responseImages,
})
Logger.log(`Auto-responded to ${askType} with ${responseType}`)
} catch (error) {
Logger.log(`Error sending askResponse: ${error}`)
}
webviewProvider.controller.handleWebviewMessage(response)
Logger.log(`Auto-responded to ${askType} with ${response.askResponse}`)
}
/**
+4 -4
View File
@@ -22,18 +22,18 @@ export interface AutoApprovalSettings {
export const DEFAULT_AUTO_APPROVAL_SETTINGS: AutoApprovalSettings = {
version: 1,
enabled: true,
enabled: false,
actions: {
readFiles: true,
readFiles: false,
readFilesExternally: false,
editFiles: false,
editFilesExternally: false,
executeSafeCommands: true,
executeSafeCommands: false,
executeAllCommands: false,
useBrowser: false,
useMcp: false,
},
maxRequests: 20,
enableNotifications: false,
favorites: ["enableAll", "readFiles", "editFiles"],
favorites: [],
}
-2
View File
@@ -115,7 +115,6 @@ export type Platform = "aix" | "darwin" | "freebsd" | "linux" | "openbsd" | "sun
export const DEFAULT_PLATFORM = "unknown"
export interface ExtensionState {
isNewUser: boolean
apiConfiguration?: ApiConfiguration
autoApprovalSettings: AutoApprovalSettings
browserSettings: BrowserSettings
@@ -143,7 +142,6 @@ export interface ExtensionState {
vscMachineId: string
globalClineRulesToggles: ClineRulesToggles
localClineRulesToggles: ClineRulesToggles
workflowToggles: ClineRulesToggles
localCursorRulesToggles: ClineRulesToggles
localWindsurfRulesToggles: ClineRulesToggles
}
-1
View File
@@ -10,7 +10,6 @@ export type HistoryItem = {
size?: number
shadowGitConfigWorkTree?: string
cwdOnTaskInitialization?: string
conversationHistoryDeletedRange?: [number, number]
isFavorited?: boolean
}
+6 -3
View File
@@ -14,7 +14,9 @@ export interface WebviewMessage {
| "newTask"
| "condense"
| "reportBug"
| "askResponse"
| "didShowAnnouncement"
| "selectImages"
| "resetState"
| "openInBrowser"
| "openMention"
@@ -35,6 +37,7 @@ export interface WebviewMessage {
| "authStateChanged"
| "authCallback"
| "fetchMcpMarketplace"
| "downloadMcp"
| "silentlyRefreshMcpMarketplace"
| "searchCommits"
| "fetchLatestMcpServersFromHub"
@@ -51,12 +54,12 @@ export interface WebviewMessage {
| "taskFeedback"
| "scrollToSettings"
| "searchFiles"
| "toggleFavoriteModel"
| "grpc_request"
| "grpc_request_cancel"
| "toggleClineRule"
| "toggleCursorRule"
| "toggleWindsurfRule"
| "toggleWorkflow"
| "deleteClineRule"
| "copyToClipboard"
| "updateTerminalConnectionTimeout"
@@ -65,6 +68,7 @@ export interface WebviewMessage {
// | "relaunchChromeDebugMode"
text?: string
disabled?: boolean
askResponse?: ClineAskResponse
apiConfiguration?: ApiConfiguration
images?: string[]
bool?: boolean
@@ -108,10 +112,9 @@ export interface WebviewMessage {
grpc_request_cancel?: {
request_id: string // ID of the request to cancel
}
// For cline rules and workflows
// For cline rules
isGlobal?: boolean
rulePath?: string
workflowPath?: string
enabled?: boolean
filename?: string
+8 -17
View File
@@ -19,7 +19,6 @@ export type ApiProvider =
| "vscode-lm"
| "cline"
| "litellm"
| "fireworks"
| "asksage"
| "xai"
| "sambanova"
@@ -71,10 +70,6 @@ export interface ApiHandlerOptions {
requestyModelInfo?: ModelInfo
togetherApiKey?: string
togetherModelId?: string
fireworksApiKey?: string
fireworksModelId?: string
fireworksModelMaxCompletionTokens?: number
fireworksModelMaxTokens?: number
qwenApiKey?: string
doubaoApiKey?: string
mistralApiKey?: string
@@ -114,7 +109,6 @@ export interface ModelInfo {
outputPrice?: number // Output price per million tokens when budget > 0
outputPriceTiers?: PriceTier[] // Optional: Tiered output price when budget > 0
}
supportsGlobalEndpoint?: boolean // Whether the model supports a global endpoint with Vertex AI
cacheWritesPrice?: number
cacheReadsPrice?: number
description?: string
@@ -404,7 +398,6 @@ export const vertexModels = {
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: true,
supportsGlobalEndpoint: true,
inputPrice: 0.15,
outputPrice: 0.6,
cacheWritesPrice: 1.0,
@@ -415,7 +408,6 @@ export const vertexModels = {
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: false,
supportsGlobalEndpoint: true,
inputPrice: 0.075,
outputPrice: 0.3,
},
@@ -424,7 +416,6 @@ export const vertexModels = {
contextWindow: 32_767,
supportsImages: true,
supportsPromptCache: false,
supportsGlobalEndpoint: true,
inputPrice: 0,
outputPrice: 0,
},
@@ -433,7 +424,6 @@ export const vertexModels = {
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: false,
supportsGlobalEndpoint: true,
inputPrice: 0,
outputPrice: 0,
},
@@ -450,7 +440,6 @@ export const vertexModels = {
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: true,
supportsGlobalEndpoint: true,
inputPrice: 2.5,
outputPrice: 15,
cacheReadsPrice: 0.31,
@@ -474,7 +463,6 @@ export const vertexModels = {
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: false,
supportsGlobalEndpoint: true,
inputPrice: 0.15,
outputPrice: 0.6,
thinkingConfig: {
@@ -487,7 +475,6 @@ export const vertexModels = {
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: false,
supportsGlobalEndpoint: true,
inputPrice: 0,
outputPrice: 0,
},
@@ -557,10 +544,6 @@ export const vertexModels = {
},
} as const satisfies Record<string, ModelInfo>
export const vertexGlobalModels: Record<string, ModelInfo> = Object.fromEntries(
Object.entries(vertexModels).filter(([_k, v]) => v.hasOwnProperty("supportsGlobalEndpoint")),
) as Record<string, ModelInfo>
export const openAiModelInfoSaneDefaults: OpenAiCompatibleModelInfo = {
maxTokens: -1,
contextWindow: 128_000,
@@ -577,6 +560,14 @@ export const openAiModelInfoSaneDefaults: OpenAiCompatibleModelInfo = {
export type GeminiModelId = keyof typeof geminiModels
export const geminiDefaultModelId: GeminiModelId = "gemini-2.0-flash-001"
export const geminiModels = {
"gemini-2.5-pro-exp-03-25": {
maxTokens: 65536,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"gemini-2.5-pro-preview-05-06": {
maxTokens: 65536,
contextWindow: 1_048_576,
@@ -2,24 +2,22 @@ import { RuleFileRequest } from "../../proto/file"
// Helper for creating delete requests
export const DeleteRuleFileRequest = {
create: (params: { rulePath: string; isGlobal: boolean; metadata?: any; type?: string }): RuleFileRequest => {
create: (params: { rulePath: string; isGlobal: boolean; metadata?: any }): RuleFileRequest => {
return RuleFileRequest.create({
rulePath: params.rulePath,
isGlobal: params.isGlobal,
metadata: params.metadata,
type: params.type,
})
},
}
// Helper for creating create requests
export const CreateRuleFileRequest = {
create: (params: { filename: string; isGlobal: boolean; metadata?: any; type?: string }): RuleFileRequest => {
create: (params: { filename: string; isGlobal: boolean; metadata?: any }): RuleFileRequest => {
return RuleFileRequest.create({
filename: params.filename,
isGlobal: params.isGlobal,
metadata: params.metadata,
type: params.type,
})
},
}
+2 -29
View File
@@ -6,7 +6,7 @@
/* eslint-disable */
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"
import { Empty, EmptyRequest, Metadata, StringArray, StringRequest } from "./common"
import { Empty, Metadata, StringRequest } from "./common"
export const protobufPackage = "cline"
@@ -73,8 +73,6 @@ export interface RuleFileRequest {
rulePath?: string | undefined
/** Filename field for createRuleFile (optional) */
filename?: string | undefined
/** Type of the file to create (optional) */
type?: string | undefined
}
/** Result for rule file operations with meaningful data only */
@@ -684,7 +682,7 @@ export const GitCommit: MessageFns<GitCommit> = {
}
function createBaseRuleFileRequest(): RuleFileRequest {
return { metadata: undefined, isGlobal: false, rulePath: undefined, filename: undefined, type: undefined }
return { metadata: undefined, isGlobal: false, rulePath: undefined, filename: undefined }
}
export const RuleFileRequest: MessageFns<RuleFileRequest> = {
@@ -701,9 +699,6 @@ export const RuleFileRequest: MessageFns<RuleFileRequest> = {
if (message.filename !== undefined) {
writer.uint32(34).string(message.filename)
}
if (message.type !== undefined) {
writer.uint32(42).string(message.type)
}
return writer
},
@@ -746,14 +741,6 @@ export const RuleFileRequest: MessageFns<RuleFileRequest> = {
message.filename = reader.string()
continue
}
case 5: {
if (tag !== 42) {
break
}
message.type = reader.string()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
@@ -769,7 +756,6 @@ export const RuleFileRequest: MessageFns<RuleFileRequest> = {
isGlobal: isSet(object.isGlobal) ? globalThis.Boolean(object.isGlobal) : false,
rulePath: isSet(object.rulePath) ? globalThis.String(object.rulePath) : undefined,
filename: isSet(object.filename) ? globalThis.String(object.filename) : undefined,
type: isSet(object.type) ? globalThis.String(object.type) : undefined,
}
},
@@ -787,9 +773,6 @@ export const RuleFileRequest: MessageFns<RuleFileRequest> = {
if (message.filename !== undefined) {
obj.filename = message.filename
}
if (message.type !== undefined) {
obj.type = message.type
}
return obj
},
@@ -803,7 +786,6 @@ export const RuleFileRequest: MessageFns<RuleFileRequest> = {
message.isGlobal = object.isGlobal ?? false
message.rulePath = object.rulePath ?? undefined
message.filename = object.filename ?? undefined
message.type = object.type ?? undefined
return message
},
}
@@ -915,15 +897,6 @@ export const FileServiceDefinition = {
responseStream: false,
options: {},
},
/** Select images from the file system and return as data URLs */
selectImages: {
name: "selectImages",
requestType: EmptyRequest,
requestStream: false,
responseType: StringArray,
responseStream: false,
options: {},
},
/** Opens an image in the system viewer */
openImage: {
name: "openImage",
+1 -9
View File
@@ -6,7 +6,7 @@
/* eslint-disable */
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"
import { Empty, Metadata, StringRequest } from "./common"
import { Metadata } from "./common"
export const protobufPackage = "cline"
@@ -1004,14 +1004,6 @@ export const McpServiceDefinition = {
responseStream: false,
options: {},
},
downloadMcp: {
name: "downloadMcp",
requestType: StringRequest,
requestStream: false,
responseType: Empty,
responseStream: false,
options: {},
},
},
} as const
-36
View File
@@ -1,36 +0,0 @@
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// versions:
// protoc-gen-ts_proto v2.7.0
// protoc v3.19.1
// source: slash.proto
/* eslint-disable */
import { Empty, StringRequest } from "./common"
export const protobufPackage = "cline"
/** SlashService provides methods for managing slash */
export type SlashServiceDefinition = typeof SlashServiceDefinition
export const SlashServiceDefinition = {
name: "SlashService",
fullName: "cline.SlashService",
methods: {
/** Sends button click message */
reportBug: {
name: "reportBug",
requestType: StringRequest,
requestStream: false,
responseType: Empty,
responseStream: false,
options: {},
},
condense: {
name: "condense",
requestType: StringRequest,
requestStream: false,
responseType: Empty,
responseStream: false,
options: {},
},
},
} as const
+1 -9
View File
@@ -6,7 +6,7 @@
/* eslint-disable */
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"
import { Empty, EmptyRequest, StringRequest } from "./common"
import { EmptyRequest } from "./common"
export const protobufPackage = "cline"
@@ -93,14 +93,6 @@ export const StateServiceDefinition = {
responseStream: true,
options: {},
},
toggleFavoriteModel: {
name: "toggleFavoriteModel",
requestType: StringRequest,
requestStream: false,
responseType: Empty,
responseStream: false,
options: {},
},
},
} as const
+1 -144
View File
@@ -50,7 +50,6 @@ export interface GetTaskHistoryRequest {
favoritesOnly: boolean
searchQuery: string
sortBy: string
currentWorkspaceOnly: boolean
}
/** Response for task history */
@@ -73,14 +72,6 @@ export interface TaskItem {
cacheReads: number
}
/** Request for ask response operation */
export interface AskResponseRequest {
metadata?: Metadata | undefined
responseType: string
text: string
images: string[]
}
function createBaseNewTaskRequest(): NewTaskRequest {
return { metadata: undefined, text: "", images: [] }
}
@@ -559,7 +550,7 @@ export const DeleteNonFavoritedTasksResults: MessageFns<DeleteNonFavoritedTasksR
}
function createBaseGetTaskHistoryRequest(): GetTaskHistoryRequest {
return { metadata: undefined, favoritesOnly: false, searchQuery: "", sortBy: "", currentWorkspaceOnly: false }
return { metadata: undefined, favoritesOnly: false, searchQuery: "", sortBy: "" }
}
export const GetTaskHistoryRequest: MessageFns<GetTaskHistoryRequest> = {
@@ -576,9 +567,6 @@ export const GetTaskHistoryRequest: MessageFns<GetTaskHistoryRequest> = {
if (message.sortBy !== "") {
writer.uint32(34).string(message.sortBy)
}
if (message.currentWorkspaceOnly !== false) {
writer.uint32(40).bool(message.currentWorkspaceOnly)
}
return writer
},
@@ -621,14 +609,6 @@ export const GetTaskHistoryRequest: MessageFns<GetTaskHistoryRequest> = {
message.sortBy = reader.string()
continue
}
case 5: {
if (tag !== 40) {
break
}
message.currentWorkspaceOnly = reader.bool()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
@@ -644,7 +624,6 @@ export const GetTaskHistoryRequest: MessageFns<GetTaskHistoryRequest> = {
favoritesOnly: isSet(object.favoritesOnly) ? globalThis.Boolean(object.favoritesOnly) : false,
searchQuery: isSet(object.searchQuery) ? globalThis.String(object.searchQuery) : "",
sortBy: isSet(object.sortBy) ? globalThis.String(object.sortBy) : "",
currentWorkspaceOnly: isSet(object.currentWorkspaceOnly) ? globalThis.Boolean(object.currentWorkspaceOnly) : false,
}
},
@@ -662,9 +641,6 @@ export const GetTaskHistoryRequest: MessageFns<GetTaskHistoryRequest> = {
if (message.sortBy !== "") {
obj.sortBy = message.sortBy
}
if (message.currentWorkspaceOnly !== false) {
obj.currentWorkspaceOnly = message.currentWorkspaceOnly
}
return obj
},
@@ -678,7 +654,6 @@ export const GetTaskHistoryRequest: MessageFns<GetTaskHistoryRequest> = {
message.favoritesOnly = object.favoritesOnly ?? false
message.searchQuery = object.searchQuery ?? ""
message.sortBy = object.sortBy ?? ""
message.currentWorkspaceOnly = object.currentWorkspaceOnly ?? false
return message
},
}
@@ -974,115 +949,6 @@ export const TaskItem: MessageFns<TaskItem> = {
},
}
function createBaseAskResponseRequest(): AskResponseRequest {
return { metadata: undefined, responseType: "", text: "", images: [] }
}
export const AskResponseRequest: MessageFns<AskResponseRequest> = {
encode(message: AskResponseRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.metadata !== undefined) {
Metadata.encode(message.metadata, writer.uint32(10).fork()).join()
}
if (message.responseType !== "") {
writer.uint32(18).string(message.responseType)
}
if (message.text !== "") {
writer.uint32(26).string(message.text)
}
for (const v of message.images) {
writer.uint32(34).string(v!)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): AskResponseRequest {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseAskResponseRequest()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break
}
message.metadata = Metadata.decode(reader, reader.uint32())
continue
}
case 2: {
if (tag !== 18) {
break
}
message.responseType = reader.string()
continue
}
case 3: {
if (tag !== 26) {
break
}
message.text = reader.string()
continue
}
case 4: {
if (tag !== 34) {
break
}
message.images.push(reader.string())
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): AskResponseRequest {
return {
metadata: isSet(object.metadata) ? Metadata.fromJSON(object.metadata) : undefined,
responseType: isSet(object.responseType) ? globalThis.String(object.responseType) : "",
text: isSet(object.text) ? globalThis.String(object.text) : "",
images: globalThis.Array.isArray(object?.images) ? object.images.map((e: any) => globalThis.String(e)) : [],
}
},
toJSON(message: AskResponseRequest): unknown {
const obj: any = {}
if (message.metadata !== undefined) {
obj.metadata = Metadata.toJSON(message.metadata)
}
if (message.responseType !== "") {
obj.responseType = message.responseType
}
if (message.text !== "") {
obj.text = message.text
}
if (message.images?.length) {
obj.images = message.images
}
return obj
},
create<I extends Exact<DeepPartial<AskResponseRequest>, I>>(base?: I): AskResponseRequest {
return AskResponseRequest.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<AskResponseRequest>, I>>(object: I): AskResponseRequest {
const message = createBaseAskResponseRequest()
message.metadata =
object.metadata !== undefined && object.metadata !== null ? Metadata.fromPartial(object.metadata) : undefined
message.responseType = object.responseType ?? ""
message.text = object.text ?? ""
message.images = object.images?.map((e) => e) || []
return message
},
}
export type TaskServiceDefinition = typeof TaskServiceDefinition
export const TaskServiceDefinition = {
name: "TaskService",
@@ -1169,15 +1035,6 @@ export const TaskServiceDefinition = {
responseStream: false,
options: {},
},
/** Sends a response to a previous ask operation */
askResponse: {
name: "askResponse",
requestType: AskResponseRequest,
requestStream: false,
responseType: Empty,
responseStream: false,
options: {},
},
},
} as const
+1 -2
View File
@@ -1,6 +1,5 @@
// Public PostHog key (safe for open source)
export const posthogConfig = {
apiKey: "phc_qfOAGxZw2TL5O8p9KYd9ak3bPBFzfjC8fy5L6jNWY7K",
host: "https://data.cline.bot",
uiHost: "https://us.posthog.com",
host: "https://us.i.posthog.com",
}
-38
View File
@@ -1,38 +0,0 @@
import * as grpc from "@grpc/grpc-js"
import { Controller } from "../core/controller"
/**
* Type definition for a gRPC handler function.
* This represents a function that takes a Controller instance and a request object,
* and returns a Promise of the response type.
*
* @template TRequest - The type of the request object
* @template TResponse - The type of the response object
*/
export type GrpcHandler<TRequest, TResponse> = (controller: Controller, req: TRequest) => Promise<TResponse>
export type GrpcStreamingResponseHandler<TRequest, TResponse> = (
controller: Controller,
req: TRequest,
streamResponseHandler: StreamingResponseWriter<TResponse>,
requestId?: string,
) => Promise<TResponse>
/**
* Type definition for the wrapper function that converts a Promise-based handler
* to a gRPC callback-style handler.
*
* @template TRequest - The type of the request object
* @template TResponse - The type of the response object
*/
export type GrpcHandlerWrapper = <TRequest, TResponse>(
handler: GrpcHandler<TRequest, TResponse>,
controller: Controller,
) => grpc.handleUnaryCall<TRequest, TResponse>
export type GrpcStreamingResponseHandlerWrapper = <TRequest, TResponse>(
handler: GrpcStreamingResponseHandler<TRequest, TResponse>,
controller: Controller,
) => grpc.handleServerStreamingCall<TRequest, TResponse>
export type StreamingResponseWriter<TResponse> = (response: TResponse, isLast?: boolean, sequenceNumber?: number) => Promise<void>
-157
View File
@@ -1,157 +0,0 @@
// GENERATED CODE -- DO NOT EDIT!
// Generated by generate-server-setup.mjs
import * as grpc from "@grpc/grpc-js"
import { Controller } from "../core/controller"
import { GrpcHandlerWrapper, GrpcStreamingResponseHandlerWrapper } from "./grpc-types"
// Account Service
import { accountLoginClicked } from "../core/controller/account/accountLoginClicked"
// Browser Service
import { getBrowserConnectionInfo } from "../core/controller/browser/getBrowserConnectionInfo"
import { testBrowserConnection } from "../core/controller/browser/testBrowserConnection"
import { discoverBrowser } from "../core/controller/browser/discoverBrowser"
import { getDetectedChromePath } from "../core/controller/browser/getDetectedChromePath"
import { updateBrowserSettings } from "../core/controller/browser/updateBrowserSettings"
// Checkpoints Service
import { checkpointDiff } from "../core/controller/checkpoints/checkpointDiff"
import { checkpointRestore } from "../core/controller/checkpoints/checkpointRestore"
// File Service
import { openFile } from "../core/controller/file/openFile"
import { selectImages } from "../core/controller/file/selectImages"
import { openImage } from "../core/controller/file/openImage"
import { deleteRuleFile } from "../core/controller/file/deleteRuleFile"
import { createRuleFile } from "../core/controller/file/createRuleFile"
import { searchCommits } from "../core/controller/file/searchCommits"
import { getRelativePaths } from "../core/controller/file/getRelativePaths"
import { searchFiles } from "../core/controller/file/searchFiles"
// Mcp Service
import { toggleMcpServer } from "../core/controller/mcp/toggleMcpServer"
import { updateMcpTimeout } from "../core/controller/mcp/updateMcpTimeout"
import { addRemoteMcpServer } from "../core/controller/mcp/addRemoteMcpServer"
import { downloadMcp } from "../core/controller/mcp/downloadMcp"
// Models Service
import { getOllamaModels } from "../core/controller/models/getOllamaModels"
import { getLmStudioModels } from "../core/controller/models/getLmStudioModels"
import { getVsCodeLmModels } from "../core/controller/models/getVsCodeLmModels"
import { refreshOpenRouterModels } from "../core/controller/models/refreshOpenRouterModels"
import { refreshOpenAiModels } from "../core/controller/models/refreshOpenAiModels"
import { refreshRequestyModels } from "../core/controller/models/refreshRequestyModels"
// Slash Service
import { reportBug } from "../core/controller/slash/reportBug"
import { condense } from "../core/controller/slash/condense"
// State Service
import { getLatestState } from "../core/controller/state/getLatestState"
import { subscribeToState } from "../core/controller/state/subscribeToState"
import { toggleFavoriteModel } from "../core/controller/state/toggleFavoriteModel"
// Task Service
import { cancelTask } from "../core/controller/task/cancelTask"
import { clearTask } from "../core/controller/task/clearTask"
import { deleteTasksWithIds } from "../core/controller/task/deleteTasksWithIds"
import { newTask } from "../core/controller/task/newTask"
import { showTaskWithId } from "../core/controller/task/showTaskWithId"
import { exportTaskWithId } from "../core/controller/task/exportTaskWithId"
import { toggleTaskFavorite } from "../core/controller/task/toggleTaskFavorite"
import { deleteNonFavoritedTasks } from "../core/controller/task/deleteNonFavoritedTasks"
import { getTaskHistory } from "../core/controller/task/getTaskHistory"
import { askResponse } from "../core/controller/task/askResponse"
// Web Service
import { checkIsImageUrl } from "../core/controller/web/checkIsImageUrl"
export function addServices(
server: grpc.Server,
proto: any,
controller: Controller,
wrapper: GrpcHandlerWrapper,
wrapStreamingResponse: GrpcStreamingResponseHandlerWrapper,
): void {
// Account Service
server.addService(proto.cline.AccountService.service, {
accountLoginClicked: wrapper(accountLoginClicked, controller),
})
// Browser Service
server.addService(proto.cline.BrowserService.service, {
getBrowserConnectionInfo: wrapper(getBrowserConnectionInfo, controller),
testBrowserConnection: wrapper(testBrowserConnection, controller),
discoverBrowser: wrapper(discoverBrowser, controller),
getDetectedChromePath: wrapper(getDetectedChromePath, controller),
updateBrowserSettings: wrapper(updateBrowserSettings, controller),
})
// Checkpoints Service
server.addService(proto.cline.CheckpointsService.service, {
checkpointDiff: wrapper(checkpointDiff, controller),
checkpointRestore: wrapper(checkpointRestore, controller),
})
// File Service
server.addService(proto.cline.FileService.service, {
openFile: wrapper(openFile, controller),
selectImages: wrapper(selectImages, controller),
openImage: wrapper(openImage, controller),
deleteRuleFile: wrapper(deleteRuleFile, controller),
createRuleFile: wrapper(createRuleFile, controller),
searchCommits: wrapper(searchCommits, controller),
getRelativePaths: wrapper(getRelativePaths, controller),
searchFiles: wrapper(searchFiles, controller),
})
// Mcp Service
server.addService(proto.cline.McpService.service, {
toggleMcpServer: wrapper(toggleMcpServer, controller),
updateMcpTimeout: wrapper(updateMcpTimeout, controller),
addRemoteMcpServer: wrapper(addRemoteMcpServer, controller),
downloadMcp: wrapper(downloadMcp, controller),
})
// Models Service
server.addService(proto.cline.ModelsService.service, {
getOllamaModels: wrapper(getOllamaModels, controller),
getLmStudioModels: wrapper(getLmStudioModels, controller),
getVsCodeLmModels: wrapper(getVsCodeLmModels, controller),
refreshOpenRouterModels: wrapper(refreshOpenRouterModels, controller),
refreshOpenAiModels: wrapper(refreshOpenAiModels, controller),
refreshRequestyModels: wrapper(refreshRequestyModels, controller),
})
// Slash Service
server.addService(proto.cline.SlashService.service, {
reportBug: wrapper(reportBug, controller),
condense: wrapper(condense, controller),
})
// State Service
server.addService(proto.cline.StateService.service, {
getLatestState: wrapper(getLatestState, controller),
subscribeToState: wrapStreamingResponse(subscribeToState, controller),
toggleFavoriteModel: wrapper(toggleFavoriteModel, controller),
})
// Task Service
server.addService(proto.cline.TaskService.service, {
cancelTask: wrapper(cancelTask, controller),
clearTask: wrapper(clearTask, controller),
deleteTasksWithIds: wrapper(deleteTasksWithIds, controller),
newTask: wrapper(newTask, controller),
showTaskWithId: wrapper(showTaskWithId, controller),
exportTaskWithId: wrapper(exportTaskWithId, controller),
toggleTaskFavorite: wrapper(toggleTaskFavorite, controller),
deleteNonFavoritedTasks: wrapper(deleteNonFavoritedTasks, controller),
getTaskHistory: wrapper(getTaskHistory, controller),
askResponse: wrapper(askResponse, controller),
})
// Web Service
server.addService(proto.cline.WebService.service, {
checkIsImageUrl: wrapper(checkIsImageUrl, controller),
})
}
-111
View File
@@ -1,111 +0,0 @@
import * as grpc from "@grpc/grpc-js"
import { ReflectionService } from "@grpc/reflection"
import * as health from "grpc-health-check"
import { activate } from "../extension"
import { Controller } from "../core/controller"
import { extensionContext, outputChannel, postMessage } from "./vscode-context"
import { packageDefinition, proto, log, camelToSnakeCase, snakeToCamelCase } from "./utils"
import { GrpcHandler, GrpcStreamingResponseHandler } from "./grpc-types"
import { addServices } from "./server-setup"
import { StreamingResponseHandler } from "@/core/controller/grpc-handler"
function main() {
log("Starting service...")
activate(extensionContext)
const controller = new Controller(extensionContext, outputChannel, postMessage)
const server = new grpc.Server()
// Set up health check.
const healthImpl = new health.HealthImplementation({ "": "SERVING" })
healthImpl.addToServer(server)
// Add all the handlers for the ProtoBus services to the server.
addServices(server, proto, controller, wrapHandler, wrapStreamingResponseHandler)
// Set up reflection.
const reflection = new ReflectionService(packageDefinition)
reflection.addToServer(server)
// Start the server.
const host = "127.0.0.1:50051"
server.bindAsync(host, grpc.ServerCredentials.createInsecure(), (err) => {
if (err) {
log(`Error: Failed to bind to ${host}, port may be unavailable ${err.message}`)
process.exit(1)
} else {
server.start()
log(`gRPC server listening on ${host}`)
}
})
}
/**
* Wraps a Promise-based handler function to make it compatible with gRPC's callback-based API.
* This function converts an async handler that returns a Promise into a function that uses
* the gRPC callback pattern.
*
* @template TRequest - The type of the request object
* @template TResponse - The type of the response object
* @param handler - The Promise-based handler function to wrap
* @param controllerInstance - The controller instance to pass to the handler
* @returns A gRPC-compatible callback-style handler function
*/
function wrapHandler<TRequest, TResponse>(
handler: GrpcHandler<TRequest, TResponse>,
controller: Controller,
): grpc.handleUnaryCall<TRequest, TResponse> {
return async (call: grpc.ServerUnaryCall<TRequest, TResponse>, callback: grpc.sendUnaryData<TResponse>) => {
try {
log(`gRPC request: ${call.getPath()}`)
const result = await handler(controller, snakeToCamelCase(call.request))
// The grpc-js serializer expects the proto message to be in the same
// case as the proto file. This is a work around until we find a solution.
callback(null, camelToSnakeCase(result))
} catch (err: any) {
log(`gRPC handler error: ${call.getPath()}\n${err.stack}`)
callback({
code: grpc.status.INTERNAL,
message: err.message || "Internal error",
} as grpc.ServiceError)
}
}
}
function wrapStreamingResponseHandler<TRequest, TResponse>(
handler: GrpcStreamingResponseHandler<TRequest, TResponse>,
controller: Controller,
): grpc.handleServerStreamingCall<TRequest, TResponse> {
return async (call: grpc.ServerWritableStream<TRequest, TResponse>) => {
try {
const requestId = call.metadata.get("request-id").pop()?.toString()
log(`gRPC streaming request: ${call.getPath()}`)
const responseHandler: StreamingResponseHandler = (response, isLast, sequenceNumber) => {
try {
// The grpc-js serializer expects the proto message to be in the same
// case as the proto file. This is a work around until we find a solution.
call.write(camelToSnakeCase(response)) // Use a bound version of call.write to maintain proper 'this' context
if (isLast === true) {
log(`Closing stream for ${requestId}`)
call.end()
}
return Promise.resolve()
} catch (error) {
return Promise.reject(error)
}
}
await handler(controller, snakeToCamelCase(call.request), responseHandler, requestId)
} catch (err: any) {
log(`gRPC handler error: ${call.getPath()}\n${err.stack}`)
call.destroy({
code: grpc.status.INTERNAL,
message: err.message || "Internal error",
} as grpc.ServiceError)
}
}
}
main()
-65
View File
@@ -1,65 +0,0 @@
import * as fs from "fs"
import * as grpc from "@grpc/grpc-js"
import * as protoLoader from "@grpc/proto-loader"
import * as health from "grpc-health-check"
const log = (...args: unknown[]) => {
const timestamp = new Date().toISOString()
console.log(`[${timestamp}]`, "#bot.cline.server.ts", ...args)
}
// Load service definitions.
const descriptorSet = fs.readFileSync("proto/descriptor_set.pb")
const clineDef = protoLoader.loadFileDescriptorSetFromBuffer(descriptorSet)
const healthDef = protoLoader.loadSync(health.protoPath)
const packageDefinition = { ...clineDef, ...healthDef }
const proto = grpc.loadPackageDefinition(packageDefinition) as unknown
// Helper function to convert camelCase to snake_case
function camelToSnakeCase(obj: any): any {
if (obj === null || typeof obj !== "object") {
return obj
}
if (Array.isArray(obj)) {
return obj.map(camelToSnakeCase)
}
return Object.keys(obj).reduce((acc: any, key: string) => {
// Convert key from camelCase to snake_case
const snakeKey = key
.replace(/([A-Z])/g, "_$1")
.replace(/^_+/, "")
.toLowerCase()
// Convert value recursively if it's an object
const value = obj[key]
acc[snakeKey] = camelToSnakeCase(value)
return acc
}, {})
}
// Helper function to convert snake_case to camelCase
function snakeToCamelCase(obj: any): any {
if (obj === null || typeof obj !== "object") {
return obj
}
if (Array.isArray(obj)) {
return obj.map(snakeToCamelCase)
}
return Object.keys(obj).reduce((acc: any, key: string) => {
// Convert key from snake_case to camelCase
const camelKey = key.replace(/_([a-z0-9])/g, (_, char) => char.toUpperCase())
// Convert value recursively if it's an object
const value = obj[key]
acc[camelKey] = snakeToCamelCase(value)
return acc
}, {})
}
export { packageDefinition, proto, log, camelToSnakeCase, snakeToCamelCase }
-164
View File
@@ -1,164 +0,0 @@
// @ts-nocheck
import * as vscode from "vscode"
import open from "open"
import { log } from "./utils"
function stubUri(path: string): vscode.Uri {
console.log(`Using file path: ${path}`)
return {
fsPath: path,
scheme: "",
authority: "",
path: "",
query: "",
fragment: "",
with: function (change: {
scheme?: string
authority?: string
path?: string
query?: string
fragment?: string
}): vscode.Uri {
return stubUri(path)
},
toString: function (skipEncoding?: boolean): string {
return path
},
toJSON: function () {
return {}
},
}
}
function createMemento(): vscode.Memento {
const store = {}
return {
keys: function (): readonly string[] {
return Object.keys(store)
},
get: function <T>(key: string): T | undefined {
return key in store ? store[key] : undefined
},
update: function (key: string, value: any): Thenable<void> {
store[key] = value
return Promise.resolve()
},
}
}
const extensionContext: vscode.ExtensionContext = {
extensionPath: "/tmp/vscode/extension",
extensionUri: stubUri("/tmp/vscode/extension"),
globalStoragePath: "/tmp/vscode/global",
globalStorageUri: stubUri("/tmp/vscode/global"),
storagePath: "/tmp/vscode/storage",
storageUri: stubUri("/tmp/vscode/storage"),
logPath: "/tmp/vscode/log",
logUri: stubUri("/tmp/vscode/log"),
globalState: createMemento(),
workspaceState: createMemento(),
storageState: createMemento(),
environmentVariableCollection: {
getScoped: function (scope: vscode.EnvironmentVariableScope): vscode.EnvironmentVariableCollection {
return {
persistent: false,
description: undefined,
replace: function (variable: string, value: string, options?: vscode.EnvironmentVariableMutatorOptions): void {},
append: function (variable: string, value: string, options?: vscode.EnvironmentVariableMutatorOptions): void {},
prepend: function (variable: string, value: string, options?: vscode.EnvironmentVariableMutatorOptions): void {},
get: function (variable: string): vscode.EnvironmentVariableMutator | undefined {
return undefined
},
forEach: function (
callback: (
variable: string,
mutator: vscode.EnvironmentVariableMutator,
collection: vscode.EnvironmentVariableCollection,
) => any,
thisArg?: any,
): void {},
delete: function (variable: string): void {},
clear: function (): void {},
[Symbol.iterator]: function (): Iterator<
[variable: string, mutator: vscode.EnvironmentVariableMutator],
any,
any
> {
throw new Error("environmentVariableCollection.getScoped.Iterator not implemented")
},
}
},
persistent: false,
description: undefined,
replace: function (variable: string, value: string, options?: vscode.EnvironmentVariableMutatorOptions): void {},
append: function (variable: string, value: string, options?: vscode.EnvironmentVariableMutatorOptions): void {},
prepend: function (variable: string, value: string, options?: vscode.EnvironmentVariableMutatorOptions): void {},
get: function (variable: string): vscode.EnvironmentVariableMutator | undefined {
return undefined
},
forEach: function (
callback: (
variable: string,
mutator: vscode.EnvironmentVariableMutator,
collection: vscode.EnvironmentVariableCollection,
) => any,
thisArg?: any,
): void {
throw new Error("environmentVariableCollection.forEach not implemented")
},
delete: function (variable: string): void {},
clear: function (): void {},
[Symbol.iterator]: function (): Iterator<[variable: string, mutator: vscode.EnvironmentVariableMutator], any, any> {
throw new Error("environmentVariableCollection.Iterator not implemented")
},
},
extensionMode: 1, // Development
extension: {
id: "your.extension.id",
isActive: true,
extensionPath: "/tmp/vscode/extension",
extensionUri: stubUri("/tmp/vscode/extension"),
packageJSON: {},
exports: {},
activate: async () => {},
extensionKind: vscode.ExtensionKind.UI,
},
subscriptions: [],
asAbsolutePath: (relPath) => `/tmp/vscode/extension/${relPath}`,
secrets: {
store: async () => {},
get: async () => undefined,
delete: async () => {},
onDidChange: {},
},
}
const outputChannel: vscode.OutputChannel = {
append: (text) => process.stdout.write(text),
appendLine: (line) => console.log(line),
clear: () => {},
show: () => {},
hide: () => {},
dispose: () => {},
name: "",
replace: function (value: string): void {},
}
function postMessage(message: ExtensionMessage): Promise<boolean> {
log("postMessage called:", message)
return Promise.resolve(true)
}
console.log("Finished loading vscode context...")
export { extensionContext, outputChannel, postMessage }
+6 -23
View File
@@ -37,18 +37,7 @@ describe("Chat Integration Tests", () => {
break;
case 'invoke':
if (message.invoke === 'primaryButtonClick') {
vscode.postMessage({
type: 'grpc_request',
grpc_request: {
service: 'cline.TaskService',
method: 'askResponse',
message: {
responseType: 'yesButtonClicked'
},
request_id: 'test-request-id',
is_streaming: false
}
});
vscode.postMessage({ type: 'askResponse', askResponse: 'yesButtonClicked' });
}
break;
}
@@ -128,14 +117,10 @@ describe("Chat Integration Tests", () => {
})
it("should handle tool approval flow", async () => {
// Set up approval listener for gRPC request
// Set up approval listener
const approvalPromise = new Promise<any>((resolve) => {
panel.webview.onDidReceiveMessage((message) => {
if (
message.type === "grpc_request" &&
message.grpc_request?.service === "cline.TaskService" &&
message.grpc_request?.method === "askResponse"
) {
if (message.type === "askResponse") {
resolve(message)
}
})
@@ -147,11 +132,9 @@ describe("Chat Integration Tests", () => {
invoke: "primaryButtonClick",
})
// Verify gRPC request was sent with correct parameters
// Verify approval was sent
const response = await approvalPromise
assert.equal(response.type, "grpc_request")
assert.equal(response.grpc_request.service, "cline.TaskService")
assert.equal(response.grpc_request.method, "askResponse")
assert.equal(response.grpc_request.message.responseType, "yesButtonClicked")
assert.equal(response.type, "askResponse")
assert.equal(response.askResponse, "yesButtonClicked")
})
})
+1 -169
View File
@@ -3,7 +3,7 @@ import { after, describe, it } from "mocha"
import * as os from "os"
import * as path from "path"
import "should"
import { createDirectoriesForFile, fileExistsAtPath, isDirectory, readDirectory } from "./fs"
import { createDirectoriesForFile, fileExistsAtPath, isDirectory } from "./fs"
describe("Filesystem Utilities", () => {
const tmpDir = path.join(os.tmpdir(), "cline-test-" + Math.random().toString(36).slice(2))
@@ -88,172 +88,4 @@ describe("Filesystem Utilities", () => {
isDir.should.be.false()
})
})
describe("readDirectory", () => {
it("should list files in a directory", async () => {
// Create test directory with files
const testDir = path.join(tmpDir, "read-test")
await fs.mkdir(testDir, { recursive: true })
await fs.writeFile(path.join(testDir, "file1.txt"), "content")
await fs.writeFile(path.join(testDir, "file2.txt"), "content")
// Get files
const files = await readDirectory(testDir)
files.length.should.equal(2)
files.should.containDeep([path.resolve(testDir, "file1.txt"), path.resolve(testDir, "file2.txt")])
})
it("should exclude specified directories", async () => {
// Create test directory with files and an excluded directory
const testDir = path.join(tmpDir, "exclude-test")
const excludeDir = path.join(testDir, "exclude-me")
await fs.mkdir(excludeDir, { recursive: true })
await fs.writeFile(path.join(testDir, "include.txt"), "content")
await fs.writeFile(path.join(excludeDir, "excluded.txt"), "content")
// Get files, excluding the "exclude-me" directory
const files = await readDirectory(testDir, [["exclude-me"]])
files.length.should.equal(1)
files.should.containDeep([path.resolve(testDir, "include.txt")])
files.should.not.containDeep([path.resolve(excludeDir, "excluded.txt")])
})
})
it("should correctly handle complex nested directory structures", async () => {
// Create a complex directory structure
const complexDir = path.join(tmpDir, "complex-test")
// Create main dir
await fs.mkdir(complexDir, { recursive: true })
await fs.writeFile(path.join(complexDir, "root.txt"), "content")
// Create first branch
await fs.mkdir(path.join(complexDir, "dir1"), { recursive: true })
await fs.writeFile(path.join(complexDir, "dir1", "file1.txt"), "content")
// Create second branch with nested structure
await fs.mkdir(path.join(complexDir, "dir2", "subdir1"), { recursive: true })
await fs.writeFile(path.join(complexDir, "dir2", "file2.txt"), "content")
await fs.writeFile(path.join(complexDir, "dir2", "subdir1", "file3.txt"), "content")
// Create third branch with deep nesting
await fs.mkdir(path.join(complexDir, "dir3", "subdir2", "deepdir"), { recursive: true })
await fs.writeFile(path.join(complexDir, "dir3", "file4.txt"), "content")
await fs.writeFile(path.join(complexDir, "dir3", "subdir2", "file5.txt"), "content")
await fs.writeFile(path.join(complexDir, "dir3", "subdir2", "deepdir", "file6.txt"), "content")
// Get all files
const files = await readDirectory(complexDir)
const expectedFiles = [
path.resolve(complexDir, "root.txt"),
path.resolve(complexDir, "dir1", "file1.txt"),
path.resolve(complexDir, "dir2", "file2.txt"),
path.resolve(complexDir, "dir2", "subdir1", "file3.txt"),
path.resolve(complexDir, "dir3", "file4.txt"),
path.resolve(complexDir, "dir3", "subdir2", "file5.txt"),
path.resolve(complexDir, "dir3", "subdir2", "deepdir", "file6.txt"),
]
files.length.should.equal(expectedFiles.length)
files.sort().should.deepEqual(expectedFiles.sort())
})
it("should correctly exclude multiple directories in complex structures", async () => {
// Use the same complex directory structure
const complexDir = path.join(tmpDir, "complex-exclude-test")
// Create main dir
await fs.mkdir(complexDir, { recursive: true })
await fs.writeFile(path.join(complexDir, "root.txt"), "content")
// Create first branch
await fs.mkdir(path.join(complexDir, "dir1"), { recursive: true })
await fs.writeFile(path.join(complexDir, "dir1", "file1.txt"), "content")
// Create second branch with nested structure
await fs.mkdir(path.join(complexDir, "dir2", "subdir1"), { recursive: true })
await fs.writeFile(path.join(complexDir, "dir2", "file2.txt"), "content")
await fs.writeFile(path.join(complexDir, "dir2", "subdir1", "file3.txt"), "content")
// Create third branch with deep nesting
await fs.mkdir(path.join(complexDir, "dir3", "subdir2", "deepdir"), { recursive: true })
await fs.writeFile(path.join(complexDir, "dir3", "file4.txt"), "content")
await fs.writeFile(path.join(complexDir, "dir3", "subdir2", "file5.txt"), "content")
await fs.writeFile(path.join(complexDir, "dir3", "subdir2", "deepdir", "file6.txt"), "content")
// Get files excluding multiple directories
const files = await readDirectory(complexDir, [["dir1"], ["subdir2"]])
const expectedFiles = [
path.resolve(complexDir, "root.txt"),
path.resolve(complexDir, "dir2", "file2.txt"),
path.resolve(complexDir, "dir2", "subdir1", "file3.txt"),
path.resolve(complexDir, "dir3", "file4.txt"),
]
files.length.should.equal(expectedFiles.length)
files.sort().should.deepEqual(expectedFiles.sort())
})
it("should exclude .clinerules/workflows directory specifically", async () => {
// Create a test directory structure
const clinerulesDirTest = path.join(tmpDir, "clinerules-test")
const clinerulesDirPath = path.join(clinerulesDirTest, ".clinerules")
// Create .clinerules directory and root files
await fs.mkdir(clinerulesDirPath, { recursive: true })
await fs.writeFile(path.join(clinerulesDirPath, "config.json"), "{}")
await fs.writeFile(path.join(clinerulesDirPath, "settings.js"), "// settings")
// Create .clinerules/other directory and files
const otherDirPath = path.join(clinerulesDirPath, "other")
await fs.mkdir(otherDirPath, { recursive: true })
await fs.writeFile(path.join(otherDirPath, "helper.js"), "// helper code")
await fs.writeFile(path.join(otherDirPath, "util.js"), "// util functions")
// Create .clinerules/workflows directory and files
const workflowsDirPath = path.join(clinerulesDirPath, "workflows")
await fs.mkdir(workflowsDirPath, { recursive: true })
await fs.writeFile(path.join(workflowsDirPath, "workflow1.js"), "// workflow1")
await fs.writeFile(path.join(workflowsDirPath, "workflow2.js"), "// workflow2")
// Get all files WITHOUT exclusion
const allFiles = await readDirectory(clinerulesDirPath)
// Verify all files are included
allFiles.length.should.equal(6) // 2 in root + 2 in other + 2 in workflows
allFiles.some((file) => file.includes("workflow1.js")).should.be.true()
allFiles.some((file) => file.includes("workflow2.js")).should.be.true()
// Get files WITH workflows directory excluded
const filteredFiles = await readDirectory(clinerulesDirPath, [[".clinerules", "workflows"]])
// Verify workflows files are excluded but others remain
filteredFiles.length.should.equal(4) // 2 in root + 2 in other
const expectedFiles = [
path.resolve(clinerulesDirPath, "config.json"),
path.resolve(clinerulesDirPath, "settings.js"),
path.resolve(otherDirPath, "helper.js"),
path.resolve(otherDirPath, "util.js"),
]
filteredFiles.sort().should.deepEqual(expectedFiles.sort())
// Test with multiple exclusions
const multiExcludeFiles = await readDirectory(clinerulesDirPath, [
[".clinerules", "workflows"],
[".clinerules", "other"],
])
// Verify both workflows and other directories are excluded
multiExcludeFiles.length.should.equal(2) // only the 2 files in root
const rootOnlyFiles = [path.resolve(clinerulesDirPath, "config.json"), path.resolve(clinerulesDirPath, "settings.js")]
multiExcludeFiles.sort().should.deepEqual(rootOnlyFiles.sort())
})
})
+1 -19
View File
@@ -86,34 +86,16 @@ const OS_GENERATED_FILES = [
* Recursively reads a directory and returns an array of absolute file paths.
*
* @param directoryPath - The path to the directory to read.
* @param excludedPaths - Nested array of paths to ignore.
* @returns A promise that resolves to an array of absolute file paths.
* @throws Error if the directory cannot be read.
*/
export const readDirectory = async (directoryPath: string, excludedPaths: string[][] = []) => {
export const readDirectory = async (directoryPath: string) => {
try {
const filePaths = await fs
.readdir(directoryPath, { withFileTypes: true, recursive: true })
.then((entries) => entries.filter((entry) => !OS_GENERATED_FILES.includes(entry.name)))
.then((entries) => entries.filter((entry) => entry.isFile()))
.then((files) => files.map((file) => path.resolve(file.parentPath, file.name)))
.then((filePaths) =>
filePaths.filter((filePath) => {
if (excludedPaths.length === 0) {
return true
}
for (const excludedPathList of excludedPaths) {
const pathToSearchFor = path.sep + excludedPathList.join(path.sep) + path.sep
if (filePath.includes(pathToSearchFor)) {
return false
}
}
return true
}),
)
return filePaths
} catch {
throw new Error(`Error reading directory at ${directoryPath}`)
-514
View File
@@ -1,514 +0,0 @@
{
"name": "Cline standalone",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "Cline standalone",
"version": "1.0.0",
"dependencies": {
"@grpc/grpc-js": "^1.13.3",
"@grpc/reflection": "^1.0.4",
"grpc-health-check": "^2.0.2",
"open": "^10.1.2",
"vscode": "file:./vscode"
}
},
"node_modules/@grpc/grpc-js": {
"version": "1.13.3",
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.3.tgz",
"integrity": "sha512-FTXHdOoPbZrBjlVLHuKbDZnsTxXv2BlHF57xw6LuThXacXvtkahEPED0CKMk6obZDf65Hv4k3z62eyPNpvinIg==",
"license": "Apache-2.0",
"dependencies": {
"@grpc/proto-loader": "^0.7.13",
"@js-sdsl/ordered-map": "^4.4.2"
},
"engines": {
"node": ">=12.10.0"
}
},
"node_modules/@grpc/proto-loader": {
"version": "0.7.15",
"resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz",
"integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==",
"license": "Apache-2.0",
"dependencies": {
"lodash.camelcase": "^4.3.0",
"long": "^5.0.0",
"protobufjs": "^7.2.5",
"yargs": "^17.7.2"
},
"bin": {
"proto-loader-gen-types": "build/bin/proto-loader-gen-types.js"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@grpc/reflection": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@grpc/reflection/-/reflection-1.0.4.tgz",
"integrity": "sha512-znA8v4AviOD3OPOxy11pxrtP8k8DanpefeTymS8iGW1fVr1U2cHuzfhYqDPHnVNDf4qvF9E25KtSihPy2DBWfQ==",
"license": "Apache-2.0",
"dependencies": {
"@grpc/proto-loader": "^0.7.13",
"protobufjs": "^7.2.5"
},
"peerDependencies": {
"@grpc/grpc-js": "^1.8.21"
}
},
"node_modules/@js-sdsl/ordered-map": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz",
"integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/js-sdsl"
}
},
"node_modules/@protobufjs/aspromise": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/base64": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/codegen": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/eventemitter": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/fetch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
"license": "BSD-3-Clause",
"dependencies": {
"@protobufjs/aspromise": "^1.1.1",
"@protobufjs/inquire": "^1.1.0"
}
},
"node_modules/@protobufjs/float": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/inquire": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/path": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/pool": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/utf8": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
"license": "BSD-3-Clause"
},
"node_modules/@types/node": {
"version": "22.15.18",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.18.tgz",
"integrity": "sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg==",
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/bundle-name": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz",
"integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==",
"license": "MIT",
"dependencies": {
"run-applescript": "^7.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.1",
"wrap-ansi": "^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
"node_modules/default-browser": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz",
"integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==",
"license": "MIT",
"dependencies": {
"bundle-name": "^4.1.0",
"default-browser-id": "^5.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/default-browser-id": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz",
"integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/define-lazy-prop": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
"integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"license": "ISC",
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/grpc-health-check": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/grpc-health-check/-/grpc-health-check-2.0.2.tgz",
"integrity": "sha512-5XKOdg/gIlTsZZR+8QJjzmW2CHBnn+NfM8zevIpzg+96i9dAuoH+Guu/L/5vuUkSbVZG69wVPnLpEnW+Smon1A==",
"license": "Apache-2.0",
"dependencies": {
"@grpc/proto-loader": "^0.7.13"
}
},
"node_modules/is-docker": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
"integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==",
"license": "MIT",
"bin": {
"is-docker": "cli.js"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/is-inside-container": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
"integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==",
"license": "MIT",
"dependencies": {
"is-docker": "^3.0.0"
},
"bin": {
"is-inside-container": "cli.js"
},
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-wsl": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz",
"integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==",
"license": "MIT",
"dependencies": {
"is-inside-container": "^1.0.0"
},
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/lodash.camelcase": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
"license": "MIT"
},
"node_modules/long": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
"license": "Apache-2.0"
},
"node_modules/open": {
"version": "10.1.2",
"resolved": "https://registry.npmjs.org/open/-/open-10.1.2.tgz",
"integrity": "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw==",
"license": "MIT",
"dependencies": {
"default-browser": "^5.2.1",
"define-lazy-prop": "^3.0.0",
"is-inside-container": "^1.0.0",
"is-wsl": "^3.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/protobufjs": {
"version": "7.5.2",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.2.tgz",
"integrity": "sha512-f2ls6rpO6G153Cy+o2XQ+Y0sARLOZ17+OGVLHrc3VUKcLHYKEKWbkSujdBWQXM7gKn5NTfp0XnRPZn1MIu8n9w==",
"hasInstallScript": true,
"license": "BSD-3-Clause",
"dependencies": {
"@protobufjs/aspromise": "^1.1.2",
"@protobufjs/base64": "^1.1.2",
"@protobufjs/codegen": "^2.0.4",
"@protobufjs/eventemitter": "^1.1.0",
"@protobufjs/fetch": "^1.1.0",
"@protobufjs/float": "^1.0.2",
"@protobufjs/inquire": "^1.1.0",
"@protobufjs/path": "^1.1.2",
"@protobufjs/pool": "^1.1.0",
"@protobufjs/utf8": "^1.1.0",
"@types/node": ">=13.7.0",
"long": "^5.0.0"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/run-applescript": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz",
"integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"license": "MIT"
},
"node_modules/vscode": {
"resolved": "vscode",
"link": true
},
"node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"license": "ISC",
"engines": {
"node": ">=10"
}
},
"node_modules/yargs": {
"version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
"license": "MIT",
"dependencies": {
"cliui": "^8.0.1",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.3",
"y18n": "^5.0.5",
"yargs-parser": "^21.1.1"
},
"engines": {
"node": ">=12"
}
},
"node_modules/yargs-parser": {
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"vscode": {
"version": "1.0.0"
}
}
}
-12
View File
@@ -1,12 +0,0 @@
{
"name": "Cline standalone",
"version": "1.0.0",
"main": "standalone.js",
"dependencies": {
"@grpc/grpc-js": "^1.13.3",
"@grpc/reflection": "^1.0.4",
"grpc-health-check": "^2.0.2",
"open": "^10.1.2",
"vscode": "file:./vscode"
}
}
-7
View File
@@ -1,7 +0,0 @@
const stubs = require("./vscode-stubs.js")
const impls = require("./vscode-impls.js")
module.exports = {
...stubs,
...impls,
}
@@ -1,5 +0,0 @@
{
"name": "vscode",
"version": "1.0.0",
"main": "index.js"
}
@@ -1,19 +0,0 @@
function createStub(path) {
return new Proxy(function () {}, {
get: (target, prop) => {
const fullPath = `${path}.${String(prop)}`
console.log(`Accessed stub: ${fullPath}`)
return createStub(fullPath)
},
apply: (target, thisArg, args) => {
console.log(`Called stub: ${path} with args:`, args)
return createStub(path)
},
construct: (target, args) => {
console.log(`Constructed stub: ${path} with args:`, args)
return createStub(path)
},
})
}
module.exports = { createStub }

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