mirror of
https://github.com/cline/cline.git
synced 2026-09-07 12:58:33 +08:00
Compare commits
79 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6a5e5faa19 | |||
| 7a24c10188 | |||
| 6cf5fdadb9 | |||
| ee9ddef7c5 | |||
| 23cd51a53f | |||
| 699d312dbb | |||
| c3caec253f | |||
| c47503affe | |||
| 5efbf77f7c | |||
| 1700c0e4f8 | |||
| c535a5ec73 | |||
| 644280bbb4 | |||
| 43357c1100 | |||
| eb19731843 | |||
| 1231113719 | |||
| cd8f572456 | |||
| 95750f8c9c | |||
| 12820a4042 | |||
| 8e3adb42d6 | |||
| 8ab35a5b06 | |||
| ba64d9fafb | |||
| 2ba2b5b264 | |||
| 0dad8e178a | |||
| 1470563142 | |||
| 0ca16961ee | |||
| 8d8452e668 | |||
| 6c18d5154f | |||
| aabe4ae1e1 | |||
| 5147e28aaf | |||
| c6e8b04b86 | |||
| c0b3c69a8f | |||
| 080ed7c1c6 | |||
| 570ece3284 | |||
| 8f6f6464a0 | |||
| 8c565b5a7c | |||
| cd1ff2ad25 | |||
| d2979631d8 | |||
| 4dfc1358c5 | |||
| 6a96c183a3 | |||
| 9df023b9d0 | |||
| 19e4387b86 | |||
| ab01a518d1 | |||
| a66724e312 | |||
| cc56486814 | |||
| 277b20a1b2 | |||
| 55d12d7556 | |||
| a527acc56c | |||
| dc1d7f51cb | |||
| 4ff7e06044 | |||
| 2968c8d99c | |||
| c617d2550e | |||
| 7937530c74 | |||
| 3657e903f5 | |||
| 0fcab4d989 | |||
| afb64c896e | |||
| 65f1b05420 | |||
| 8f37543800 | |||
| abbe40ee9c | |||
| 5c082762c4 | |||
| 4a230ad878 | |||
| 94c432f3f3 | |||
| d88c07c932 | |||
| 5ee5577010 | |||
| 01877c1629 | |||
| f8a7b563aa | |||
| 915555f80f | |||
| 26eafd96dd | |||
| e504b9d414 | |||
| 312777ddc5 | |||
| c79acf5ffe | |||
| 7d5d347cdd | |||
| 95cc15a142 | |||
| f7d464a51d | |||
| a6c4c0c0ea | |||
| 976a8fa85e | |||
| 33413e91c6 | |||
| 801c59e75e | |||
| df9c8e2e80 | |||
| 4d480ea3fe |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Put all the navigation state and message handling and navigation functions in the extension state context instead of the app.tsx
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Migrate toggleToolAutoApprove to protobus
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
togglePlanActMode protobus migration
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Migrate toggleClineRule to protobus
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
taskCompletionViewChanges protobus migration
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
copyToClipboard protobus migration
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
migrate accountLogoutClicked to protobus
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Migrate restartMcpServer to protobus
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Migrate the deleteMcpServer message to protobus
|
||||
@@ -209,7 +209,7 @@ class Task {
|
||||
switch (chunk.type) {
|
||||
case "text":
|
||||
// Parse into content blocks
|
||||
this.assistantMessageContent = parseAssistantMessage(chunk.text)
|
||||
this.assistantMessageContent = parseAssistantMessageV2(chunk.text)
|
||||
// Present blocks to user
|
||||
await this.presentAssistantMessage()
|
||||
break
|
||||
|
||||
@@ -0,0 +1,549 @@
|
||||
The goal of this workflow is to take a changeset for a release of Cline, an autonomous coding agent extension that plugs right into your IDE, and write the updated announcement component, and the updated changelog.
|
||||
|
||||
|
||||
For reference, here are some examples of how we converted previous changesets to announcement components / changelogs.
|
||||
|
||||
|
||||
- 3.14
|
||||
<changeset>
|
||||
This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and publish to npm yourself or setup this action to publish automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.
|
||||
|
||||
Releases
|
||||
claude-dev@3.14.0
|
||||
Minor Changes
|
||||
77c9863: create clinerules folder if its currently a file and creating new rule
|
||||
0ffb7dd: disabling shift hint for now & improving tooltip behavior
|
||||
79b76fd: Add support for custom model ID in AWS Bedrock provider, enabling use of Application Inference Profile.
|
||||
eb6e481: Full support for LaTeX rendering
|
||||
df37f29: Add support for custom API request timeout. Previously, timeouts were hardcoded to 30 seconds for providers like Ollama or 15 seconds for OpenRouter and Cline. Now users can set a custom timeout value in milliseconds through the settings interface.
|
||||
e4d26be: allow cursorrules and windsurfrules
|
||||
c5de50f: Fix Handle @withRetry() SyntaxError when running extension locally issue
|
||||
61d2f42: enabled pricing calculation for gemini and vertex + more robust caching & cache tracking for gemini & vertex
|
||||
aed152b: add truncation notice when truncating manually
|
||||
2fe2405: Migrate Cline Tools Section to new docs
|
||||
19cc8bc: Add a timeout setting for the terminal connection, allowing users to adjust this if they are having timeout issues
|
||||
03d4410: Added copy button to code blocks.
|
||||
c78fe23: addressed race condition in terminal command usage
|
||||
91e222f: add checkpoints after more messages
|
||||
14230e7: add newrule slash command
|
||||
1c7d33a: Add remote config with posthog allowing for disabling new features until they're reading, making for a better developer experience.
|
||||
4196c14: add cache ui for open router and cline provider
|
||||
d97424f: showing expanded task by default
|
||||
5294e78: Refactor to not pass a message for showing the MCP View from the servers modal
|
||||
70cc437: Fix Windows path issue: Correct handling of import.meta.url to avoid leading slash in pathname
|
||||
4b697d8: Migrate the addRemoteServer to protobus
|
||||
Patch Changes
|
||||
c63d9a1: updated drag and drop text to say "drop" instead of "drag"
|
||||
459adf0: Add markdown copy to chat
|
||||
74ec823: Minor UX improvement to drag and drop ux
|
||||
b0961f4: Remove linear pull request action
|
||||
e9ce384: searchCommits protobus migration
|
||||
5802b68: createRuleFile protobus migration
|
||||
df7f9fc: Add dependsOn to more blocks in the tasks.json
|
||||
41ae732: Fix for git commit mentions in repos with no git commits
|
||||
7e78445: Adding args to allow Cursor to open workspaces (for checkpoint testing/development)
|
||||
bdfda6f: feat(bedrock): Introduce Amazon Nova Premier
|
||||
65243ad: Introduce UI library for future UI development
|
||||
4565e06: checkIsImageURL migrated to protobus
|
||||
5a8e9d8: protobus migration for openImage
|
||||
deeda6e: Lowering Gemini cache TTL time
|
||||
db0b022: Adding UI to show openrouter balance next to provider
|
||||
4650ffa: deleteRuleFile protobus migration
|
||||
d4bd755: fix cost calculation
|
||||
</changeset>
|
||||
|
||||
<changelog>
|
||||
## [3.14.0]
|
||||
|
||||
- Add UI to show openrouter balance next to provider
|
||||
- Add support for custom model ID in AWS Bedrock provider, enabling use of Application Inference Profile (Thanks @clicube!)
|
||||
- Add more robust caching & cache tracking for gemini & vertex providers
|
||||
- Add support for LaTeX rendering
|
||||
- Add support for custom API request timeout. Timeouts were 15-30s, but can now be configured via settings for OpenRouter/Cline & Ollama (Thanks @WingsDrafterwork!)
|
||||
- Add truncation notice when truncating manually
|
||||
- Add a timeout setting for the terminal connection, allowing users to set a time to wait for terminal startup
|
||||
- Add copy button to code blocks
|
||||
- Add copy button to markdown blocks (Thanks @weshoke!)
|
||||
- Add checkpoints to more messages
|
||||
- Add slash command to create a new rules file (/newrule)
|
||||
- Add cache ui for open router and cline provider
|
||||
- Add Amazon Nova Premier model to Bedrock (Thanks @watany!)
|
||||
- Add support for cursorrules and windsurfrules
|
||||
- Add support for batch history deletion (Thanks @danix800!)
|
||||
- Improve Drag & Drop experience
|
||||
- Create clinerules folder creating new rule if it's needed
|
||||
- Enable pricing calculation for gemini and vertex providers
|
||||
- Refactor message handling to not show the MCP View of the server modal
|
||||
- Migrate the addRemoteServer to protobus (Thanks @DaveFres!)
|
||||
- Update task header to be expanded by default
|
||||
- Update Gemini cache TTL time to 15 minutes
|
||||
- Fix race condition in terminal command usage
|
||||
- Fix to correctly handle `import.meta.url`, avoiding leading slash in pathname for Windows (Thanks @DaveFres!)
|
||||
- Fix @withRetry() decoration syntax error when running extension locally (Thanks @DaveFres!)
|
||||
- Fix for git commit mentions in repos with no git commits
|
||||
- Fix cost calculation (Thanks @BarreiroT!)
|
||||
</changelog>
|
||||
|
||||
|
||||
<announcement-component>
|
||||
const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
|
||||
const minorVersion = version.split(".").slice(0, 2).join(".") // 2.0.0 -> 2.0
|
||||
return (
|
||||
<div style={containerStyle}>
|
||||
<VSCodeButton appearance="icon" onClick={hideAnnouncement} style={closeIconStyle}>
|
||||
<span className="codicon codicon-close"></span>
|
||||
</VSCodeButton>
|
||||
<h3 style={h3TitleStyle}>
|
||||
🎉{" "}New in v{minorVersion}
|
||||
</h3>
|
||||
<ul style={ulStyle}>
|
||||
<li>
|
||||
<b>Gemini prompt caching:</b> Gemini and Vertex providers now support prompt caching and price tracking for
|
||||
Gemini models.
|
||||
</li>
|
||||
<li>
|
||||
<b>Copy Buttons:</b> Buttons were added to Markdown and Code blocks that allow you to copy their contents
|
||||
easily.
|
||||
</li>
|
||||
<li>
|
||||
<b>/newrule command:</b> New slash command to have cline write your .clinerules for you based on your
|
||||
workflow.
|
||||
</li>
|
||||
<li>
|
||||
<b>Drag and drop improvements:</b> Don't forget to hold shift while dragging files!
|
||||
</li>
|
||||
<li>Added more checkpoints across the task, allowing you to restore from more than just file changes.</li>
|
||||
<li>Added support for rendering LaTeX in message responses. (Try asking Cline to show the quadratic formula)</li>
|
||||
</ul>
|
||||
<Accordion isCompact className="pl-0">
|
||||
<AccordionItem
|
||||
key="1"
|
||||
aria-label="Previous Updates"
|
||||
title="Previous Updates:"
|
||||
classNames={{
|
||||
trigger: "bg-transparent border-0 pl-0 pb-0 w-fit",
|
||||
title: "font-bold text-[var(--vscode-foreground)]",
|
||||
indicator:
|
||||
"text-[var(--vscode-foreground)] mb-0.5 -rotate-180 data-[open=true]:-rotate-90 rtl:rotate-0 rtl:data-[open=true]:-rotate-90",
|
||||
}}>
|
||||
<ul style={ulStyle}>
|
||||
<li>
|
||||
<b>Global Cline Rules:</b> store multiple rules files in Documents/Cline/Rules to share between
|
||||
projects.
|
||||
</li>
|
||||
<li>
|
||||
<b>Cline Rules Popup:</b> New button in the chat area to view workspace and global cline rules files
|
||||
to plug and play specific rules for the task
|
||||
</li>
|
||||
<li>
|
||||
<b>Slash Commands:</b> Type <code>/</code> in chat to see the list of quick actions, like starting a
|
||||
new task (more coming soon!)
|
||||
</li>
|
||||
<li>
|
||||
<b>Edit Messages:</b> You can now edit a message you sent previously by clicking on it. Optionally
|
||||
restore your project when the message was sent!
|
||||
</li>
|
||||
</ul>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
|
||||
{/*
|
||||
// Leave this here for an example of how to structure the announcement
|
||||
<ul style={{ margin: "0 0 8px", paddingLeft: "12px" }}>
|
||||
<li>
|
||||
OpenRouter now supports prompt caching! They also have much higher rate limits than other providers,
|
||||
so I recommend trying them out.
|
||||
<br />
|
||||
{!apiConfiguration?.openRouterApiKey && (
|
||||
<VSCodeButtonLink
|
||||
href={getOpenRouterAuthUrl(vscodeUriScheme)}
|
||||
style={{
|
||||
transform: "scale(0.85)",
|
||||
transformOrigin: "left center",
|
||||
margin: "4px -30px 2px 0",
|
||||
}}>
|
||||
Get OpenRouter API Key
|
||||
</VSCodeButtonLink>
|
||||
)}
|
||||
{apiConfiguration?.openRouterApiKey && apiConfiguration?.apiProvider !== "openrouter" && (
|
||||
<VSCodeButton
|
||||
onClick={() => {
|
||||
vscode.postMessage({
|
||||
type: "apiConfiguration",
|
||||
apiConfiguration: { ...apiConfiguration, apiProvider: "openrouter" },
|
||||
})
|
||||
}}
|
||||
style={{
|
||||
transform: "scale(0.85)",
|
||||
transformOrigin: "left center",
|
||||
margin: "4px -30px 2px 0",
|
||||
}}>
|
||||
Switch to OpenRouter
|
||||
</VSCodeButton>
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
<b>Edit Cline's changes before accepting!</b> When he creates or edits a file, you can modify his
|
||||
changes directly in the right side of the diff view (+ hover over the 'Revert Block' arrow button in
|
||||
the center to undo "<code>{"// rest of code here"}</code>" shenanigans)
|
||||
</li>
|
||||
<li>
|
||||
New <code>search_files</code> tool that lets Cline perform regex searches in your project, letting
|
||||
him refactor code, address TODOs and FIXMEs, remove dead code, and more!
|
||||
</li>
|
||||
<li>
|
||||
When Cline runs commands, you can now type directly in the terminal (+ support for Python
|
||||
environments)
|
||||
</li>
|
||||
</ul>*/}
|
||||
<div style={hrStyle} />
|
||||
<p style={linkContainerStyle}>
|
||||
Join us on{" "}
|
||||
<VSCodeLink style={linkStyle} href="https://x.com/cline">
|
||||
X,
|
||||
</VSCodeLink>{" "}
|
||||
<VSCodeLink style={linkStyle} href="https://discord.gg/cline">
|
||||
discord,
|
||||
</VSCodeLink>{" "}
|
||||
or{" "}
|
||||
<VSCodeLink style={linkStyle} href="https://www.reddit.com/r/cline/">
|
||||
r/cline
|
||||
</VSCodeLink>
|
||||
for more updates!
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</announcement-component>
|
||||
|
||||
- 3.13
|
||||
|
||||
<changeset>
|
||||
Minor Changes
|
||||
2964388: Added copy button to MermaidBlock component
|
||||
75143a7: Add the ability to fetch from global cline rules files
|
||||
Patch Changes
|
||||
a0252e7: convert inline style to tailwind css of file SettingsView.tsx
|
||||
ab59bd9: Add stream options back to xai provider
|
||||
7276f50: Icons to indicate an action is occuring outside of the users workspace
|
||||
0b19ba6: update to NEW model
|
||||
</changeset>
|
||||
|
||||
<changelog>
|
||||
## [3.13.0]
|
||||
|
||||
- Add Cline rules popover under the chat field, allowing you to easily add, enable & disable workspace level or global rule files
|
||||
- Add new slash command menu letting you type “/“ to do quick actions like creating new tasks
|
||||
- Add ability to edit past messages, with options to restore your workspace back to that point
|
||||
- Allow sending a message when selecting an option provided by the question or plan tool
|
||||
- Add command to jump to Cline's chat input
|
||||
- Add support for OpenAI o3 & 4o-mini (Thanks @PeterDaveHello and @arafatkatze!)
|
||||
- Add baseURL option for Google Gemini provider (Thanks @owengo and @olivierhub!)
|
||||
- Add support for Azure's DeepSeek model. (Thanks @yt3trees!)
|
||||
- Add ability for models that support it to receive image responses from MCP servers (Thanks @rikaaa0928!)
|
||||
- Improve search and replace diff editing by making it more flexible with models that fail to follow structured output instructions. (Thanks @chi-cat!)
|
||||
- Add detection of Ctrl+C termination in terminal, improving output reading issues
|
||||
- Fix issue where some commands with large output would cause UI to freeze
|
||||
- Fix token usage tracking issues with vertex provider (Thanks @mzsima!)
|
||||
- Fix issue with xAI reasoning content not being parsed (Thanks @mrubens!)
|
||||
</changelog>
|
||||
|
||||
<announcement-component>
|
||||
const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
|
||||
const minorVersion = version.split(".").slice(0, 2).join(".") // 2.0.0 -> 2.0
|
||||
return (
|
||||
<div style={containerStyle}>
|
||||
<VSCodeButton appearance="icon" onClick={hideAnnouncement} style={closeIconStyle}>
|
||||
<span className="codicon codicon-close"></span>
|
||||
</VSCodeButton>
|
||||
<h3 style={h3TitleStyle}>
|
||||
🎉{" "}New in v{minorVersion}
|
||||
</h3>
|
||||
<ul style={ulStyle}>
|
||||
<li>
|
||||
<b>Global Cline Rules:</b> store multiple rules files in Documents/Cline/Rules to share between projects.
|
||||
</li>
|
||||
<li>
|
||||
<b>Cline Rules Popup:</b> New button in the chat area to view workspace and global cline rules files to plug
|
||||
and play specific rules for the task
|
||||
</li>
|
||||
<li>
|
||||
<b>Slash Commands:</b> Type <code>/</code> in chat to see the list of quick actions, like starting a new task
|
||||
(more coming soon!)
|
||||
</li>
|
||||
<li>
|
||||
<b>Edit Messages:</b> You can now edit a message you sent previously by clicking on it. Optionally restore
|
||||
your project when the message was sent!
|
||||
</li>
|
||||
</ul>
|
||||
<h4 style={{ margin: "5px 0 5px" }}>Previous Updates:</h4>
|
||||
<ul style={ulStyle}>
|
||||
<li>
|
||||
<b>Model Favorites:</b> You can now mark your favorite models when using Cline & OpenRouter providers for
|
||||
quick access!
|
||||
</li>
|
||||
<li>
|
||||
<b>Faster Diff Editing:</b> Improved animation performance for large files, plus a new indicator in chat
|
||||
showing the number of edits Cline makes.
|
||||
</li>
|
||||
<li>
|
||||
<b>New Auto-Approve Options:</b> Turn off Cline's ability to read and edit files outside your workspace.
|
||||
</li>
|
||||
</ul>
|
||||
{/*
|
||||
// Leave this here for an example of how to structure the announcement
|
||||
<ul style={{ margin: "0 0 8px", paddingLeft: "12px" }}>
|
||||
<li>
|
||||
OpenRouter now supports prompt caching! They also have much higher rate limits than other providers,
|
||||
so I recommend trying them out.
|
||||
<br />
|
||||
{!apiConfiguration?.openRouterApiKey && (
|
||||
<VSCodeButtonLink
|
||||
href={getOpenRouterAuthUrl(vscodeUriScheme)}
|
||||
style={{
|
||||
transform: "scale(0.85)",
|
||||
transformOrigin: "left center",
|
||||
margin: "4px -30px 2px 0",
|
||||
}}>
|
||||
Get OpenRouter API Key
|
||||
</VSCodeButtonLink>
|
||||
)}
|
||||
{apiConfiguration?.openRouterApiKey && apiConfiguration?.apiProvider !== "openrouter" && (
|
||||
<VSCodeButton
|
||||
onClick={() => {
|
||||
vscode.postMessage({
|
||||
type: "apiConfiguration",
|
||||
apiConfiguration: { ...apiConfiguration, apiProvider: "openrouter" },
|
||||
})
|
||||
}}
|
||||
style={{
|
||||
transform: "scale(0.85)",
|
||||
transformOrigin: "left center",
|
||||
margin: "4px -30px 2px 0",
|
||||
}}>
|
||||
Switch to OpenRouter
|
||||
</VSCodeButton>
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
<b>Edit Cline's changes before accepting!</b> When he creates or edits a file, you can modify his
|
||||
changes directly in the right side of the diff view (+ hover over the 'Revert Block' arrow button in
|
||||
the center to undo "<code>{"// rest of code here"}</code>" shenanigans)
|
||||
</li>
|
||||
<li>
|
||||
New <code>search_files</code> tool that lets Cline perform regex searches in your project, letting
|
||||
him refactor code, address TODOs and FIXMEs, remove dead code, and more!
|
||||
</li>
|
||||
<li>
|
||||
When Cline runs commands, you can now type directly in the terminal (+ support for Python
|
||||
environments)
|
||||
</li>
|
||||
</ul>*/}
|
||||
<div style={hrStyle} />
|
||||
<p style={linkContainerStyle}>
|
||||
Join us on{" "}
|
||||
<VSCodeLink style={linkStyle} href="https://x.com/cline">
|
||||
X,
|
||||
</VSCodeLink>{" "}
|
||||
<VSCodeLink style={linkStyle} href="https://discord.gg/cline">
|
||||
discord,
|
||||
</VSCodeLink>{" "}
|
||||
or{" "}
|
||||
<VSCodeLink style={linkStyle} href="https://www.reddit.com/r/cline/">
|
||||
r/cline
|
||||
</VSCodeLink>
|
||||
for more updates!
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</announcement-component>
|
||||
|
||||
|
||||
We have a changeset PR that automatically generated as new unreleased PRs are merged into main, the PR is always called "Changeset version bump" and the author is github-actions.
|
||||
|
||||
The Changeset PR description looks something like this:
|
||||
|
||||
<changeset-pr-description>
|
||||
This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and publish to npm yourself or [setup this action to publish automatically](https://github.com/changesets/action#with-publishing). If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.
|
||||
|
||||
|
||||
# Releases
|
||||
## claude-dev@3.16.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- c6e8b04: Recent task list is now collapsible, allowing users to hide their recent tasks (e.g. when sharing their screen).
|
||||
- aabe4ae: Add detection for new users to display special components
|
||||
- 6c18d51: adds global endpoint for vertex ai users
|
||||
- 080ed7c: Add Tailwind CSS IntelliSense to the the recommended extensions list
|
||||
- 5147e28: new workflow feature
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- c0b3c69: fix eternal loading states when the last message is a checkpoint
|
||||
- 570ece3: selectImages protos migration
|
||||
- 8d8452e: askResponse protobus migration
|
||||
- cd1ff2a: Finishing the migration of Vscode Advanced settings to Settings Webview
|
||||
</changeset-pr-description>
|
||||
|
||||
The changeset pr is ALWAYS on the following branch: `changeset-release/main`.
|
||||
|
||||
I have the `gh` command line tool set up and authenticated, so you have everything you need.
|
||||
|
||||
The first step is to get the full diff from the changeset PR to look at the changes that were automatically made to the `CHANGELOG.md` file. By default it will automatically add a new section to the changelog.md file with the new version. The problem with the automatically generated section is that it just takes the text that the developers threw into their changeset files for each corresponding PR, and they can be pretty vague and bad. Additionally there's some stuff that is totally irrelevant for the end user, like minor refactoring changes. So I manually typically go in and update this section to be a proper changelog that will show up in our patchnotes. You can look at how the rest of the file is done because those are all good examples of us updating this to use good language for the end user. We usually put new features up top (and the most exciting flagship features at the very top), and then bug fixes/improvements at the bottom. Having some basic organization to the ordering of the bullet points by content is nice. But use common sense.
|
||||
|
||||
To handle this process effectively, do the following:
|
||||
|
||||
For each of the automatically generated bullet points in the Changelog.md, you should
|
||||
1. Take the commit hash at the start of the bullet point, and use the `gh` command line tool find the PR that it was associated with.
|
||||
2. Use the `gh` command to get the PR title/description/discussion to understand the context surrounding the PR.
|
||||
3. Use the `gh` command line tool to get the full PR diff to fully understand the changes made in the code.
|
||||
4. Synthesize that knowledge to determine (a) whether or not this change is relevant to end users and (b) what the text & ordering of the line should be.
|
||||
5. Update the `CHANGELOG.md` accordingly
|
||||
|
||||
Do this for every single item in the list from the autogenerated bullet points. We want to be diligent and have a full understanding of every feature so we can make the best changelog ever!
|
||||
|
||||
Here are some principles for good changelogs from keepchangelog.com, a handy guide:
|
||||
|
||||
<keepachangelog-pinciples-for-good-changelogs>
|
||||
### Guiding Principles
|
||||
- Changelogs are for humans, not machines.
|
||||
- There should be an entry for every single version.
|
||||
- The same types of changes should be grouped.
|
||||
- The latest version comes first.
|
||||
|
||||
### Bullet points in the changelog should follow these principles:
|
||||
- Types of changes
|
||||
- Added for new features.
|
||||
- Changed for changes in existing functionality.
|
||||
- Deprecated for soon-to-be removed features.
|
||||
- Removed for now removed features.
|
||||
- Fixed for any bug fixes.
|
||||
- Security in case of vulnerabilities.
|
||||
</keepachangelog-pinciples-for-good-changelogs>
|
||||
|
||||
Lastly, when developers make a PR, they typically make a changeset. And they have 3 options when making the changeset:
|
||||
|
||||
1. Patch
|
||||
2. Minor
|
||||
3. Major
|
||||
|
||||
Sometimes they label something as minor when really it should just be a patch. Or vice versa. Because of this, the automatic version bump may be incorrect. So when starting out this workflow, you should use the <ask_followup_question> tool to confirm with me whether or not this should be a patch bump (show the old version number and what the proposed new version number would be) or a minor bump. Part of the release process is making sure the version in package.json that is automatically changed actually corresponds with what we decided the bump should actually be based on the features. ALL these modifications happen in the `changeset-release/main` branch btw.
|
||||
|
||||
<important_note>
|
||||
Before doing any of this, make sure you check out the `changeset-release/main` and pull the most recent up to date changes. Then perform all this work in that branch.
|
||||
|
||||
New announcement banners should ONLY be made for minor version bumps or higher. That's another reason why double checking if the changelog warrants the bump is important.
|
||||
|
||||
Also, SUPER important: For any external contributors that aren't part of the cline github organization, we always want to add a (Thanks @username!) at the end of the changelog to attribute them properly. We're an open source project and it's ethical to do this.
|
||||
</important_note>
|
||||
|
||||
Once the changelog looks good, and the version number looks good, we gotta double check that the version number in the changelog has the brackets around it. And as a final step, double check the package.json version number matches the latest number in the changelog. And as the ultimate final step we run `npm run install:all` to make sure the package version number permiates through the lock file.
|
||||
|
||||
|
||||
<detailed_sequence_of_steps>
|
||||
# Cline Release Process - Detailed Sequence of Steps
|
||||
|
||||
## Before Starting
|
||||
1. First, examine the changeset PR without checking it out:
|
||||
```bash
|
||||
gh pr view changeset-release/main
|
||||
```
|
||||
|
||||
2. View the PR diff to see the auto-generated CHANGELOG.md changes:
|
||||
```bash
|
||||
gh pr diff changeset-release/main > changeset-diff.txt
|
||||
cat changeset-diff.txt | grep -A 50 "CHANGELOG.md"
|
||||
```
|
||||
|
||||
## Initial Setup
|
||||
3. Once you're ready to start, checkout and update the changeset release branch:
|
||||
```bash
|
||||
git checkout changeset-release/main
|
||||
git pull origin changeset-release/main
|
||||
```
|
||||
|
||||
## Analyzing Each Change
|
||||
4. For each commit hash in the auto-generated changelog entries:
|
||||
|
||||
a. Find the PR number associated with a commit hash:
|
||||
```bash
|
||||
gh pr list --search "<commit-hash>" --state merged
|
||||
```
|
||||
|
||||
b. Get PR details for better context:
|
||||
```bash
|
||||
gh pr view <PR-number>
|
||||
```
|
||||
|
||||
c. Check if the contributor is external to determine if attribution is needed:
|
||||
```bash
|
||||
# Extract username from PR
|
||||
USERNAME=$(gh pr view <PR-number> --json author --jq .author.login)
|
||||
|
||||
# Check if user is a member of the Cline organization
|
||||
# this command is a bit finnicky, but it 100% works.
|
||||
# if you see a `Error executing command: The command ran successfully, but we couldn't capture its output. Please proceed accordingly.` error, just retry it until you actually get the output
|
||||
# don't make any assumptions, just retry the command to actually get the output and determine if they're external or not.
|
||||
# no output means they are an external contributor, otherwise if there is output they are an internal contributor (part of our github org)
|
||||
gh api "orgs/cline/members" --jq "map(.login)" | grep -i "pashpashpash"
|
||||
```
|
||||
|
||||
d. View the full PR diff to understand code changes:
|
||||
```bash
|
||||
gh pr diff <PR-number> > pr-diff-<PR-number>.txt
|
||||
cat pr-diff-<PR-number>.txt
|
||||
```
|
||||
|
||||
## Updating the Changelog
|
||||
5. Based on PR analysis, update the CHANGELOG.md with user-friendly descriptions:
|
||||
- Use the `<replace_in_file>` tool to edit the CHANGELOG.md file
|
||||
- Group by feature type (Added, Changed, Fixed)
|
||||
- Put most exciting features at the top
|
||||
- Move bug fixes and small improvements to the bottom
|
||||
- Use clear, end-user focused language
|
||||
- For external contributors, add attribution at the end of the relevant entry: `(Thanks @username!)`
|
||||
|
||||
## Version Number Verification
|
||||
6. Confirm the version bump is appropriate:
|
||||
- Check package.json to verify the auto-generated version number:
|
||||
```bash
|
||||
cat package.json | grep "\"version\""
|
||||
```
|
||||
- If the feature set doesn't warrant a minor bump, use the `<replace_in_file>` tool to modify package.json
|
||||
|
||||
7. Ensure the version in CHANGELOG.md has brackets around it:
|
||||
```
|
||||
## [3.16.0]
|
||||
```
|
||||
|
||||
## Creating the Announcement (for minor/major versions only)
|
||||
8. If this is a minor version bump, create/update the announcement component:
|
||||
- Use the `<replace_in_file>` tool to edit the src/views/components/announcement.tsx file
|
||||
- Update the highlights based on key features
|
||||
- Move previous version highlights to the "Previous Updates" section
|
||||
- Use the previous announcement components as reference for structure
|
||||
|
||||
## Finalizing the Release
|
||||
9. Update dependencies with the new version number:
|
||||
```bash
|
||||
npm run install:all
|
||||
```
|
||||
|
||||
10. Commit your changes:
|
||||
```bash
|
||||
git add CHANGELOG.md package.json package-lock.json src/views/components/announcement.tsx
|
||||
git commit -m "Update CHANGELOG.md and announcement for version 3.16.0"
|
||||
```
|
||||
|
||||
11. Push your changes to the changeset branch:
|
||||
```bash
|
||||
git push origin changeset-release/main
|
||||
```
|
||||
|
||||
12. Check that your changes pushed successfully:
|
||||
```bash
|
||||
git status
|
||||
```
|
||||
</detailed_sequence_of_steps>
|
||||
@@ -1,2 +1,4 @@
|
||||
demo.gif filter=lfs diff=lfs merge=lfs -text
|
||||
assets/docs/demo.gif filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
* text=auto eol=lf
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
blank_issues_enabled: false
|
||||
blank_issues_enabled: true
|
||||
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
|
||||
|
||||
@@ -15,7 +15,15 @@ permissions:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: ${{ matrix.os == 'ubuntu-latest' && 'test' || format('test ({0})', matrix.os) }}
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
@@ -60,6 +68,11 @@ jobs:
|
||||
if: steps.webview-cache.outputs.cache-hit != 'true'
|
||||
run: cd webview-ui && npm ci
|
||||
|
||||
- name: Set up NPM on Windows
|
||||
if: runner.os == 'Windows'
|
||||
run: |
|
||||
npm config set script-shell "C:\\Program Files\\Git\\bin\\bash.exe"
|
||||
|
||||
- name: Type Check
|
||||
run: npm run check-types
|
||||
|
||||
@@ -81,8 +94,9 @@ jobs:
|
||||
id: extension_coverage
|
||||
continue-on-error: true
|
||||
run: |
|
||||
xvfb-run -a npm run test:coverage > extension_coverage.txt 2>&1
|
||||
PYTHONPATH=.github/scripts python -m coverage_check extract-coverage extension_coverage.txt --type=extension --github-output --verbose
|
||||
node ./scripts/test-ci.js > extension_coverage.txt 2>&1
|
||||
# Default the encoding to UTF-8 - It's not the default on Windows
|
||||
PYTHONUTF8=1 PYTHONPATH=.github/scripts python -m coverage_check extract-coverage extension_coverage.txt --type=extension --github-output --verbose
|
||||
|
||||
# Run webview tests with coverage
|
||||
- name: Webview Tests with Coverage
|
||||
@@ -92,13 +106,16 @@ jobs:
|
||||
cd webview-ui
|
||||
# Ensure coverage dependency is installed
|
||||
npm install --no-save @vitest/coverage-v8
|
||||
npm run test:coverage > webview_coverage.txt 2>&1 || true
|
||||
npm run test:coverage > webview_coverage.txt 2>&1
|
||||
cd ..
|
||||
PYTHONPATH=.github/scripts python -m coverage_check extract-coverage webview-ui/webview_coverage.txt --type=webview --github-output --verbose
|
||||
# Default the encoding to UTF-8 - It's not the default on Windows
|
||||
PYTHONUTF8=1 PYTHONPATH=.github/scripts python -m coverage_check extract-coverage webview-ui/webview_coverage.txt --type=webview --github-output --verbose
|
||||
|
||||
# Save coverage reports as artifacts (workflow-scoped)
|
||||
- name: Save Coverage Reports
|
||||
uses: actions/upload-artifact@v4
|
||||
# Only upload artifacts on Linux - We only need coverage from one OS
|
||||
if: runner.os == 'Linux'
|
||||
with:
|
||||
name: pr-coverage-reports
|
||||
path: |
|
||||
@@ -107,14 +124,18 @@ jobs:
|
||||
retention-period: workflow # Artifacts are automatically deleted when the workflow completes
|
||||
|
||||
# Set the check as failed if any of the tests failed
|
||||
- name: Check for test failures
|
||||
- name: Print test results and check for failures
|
||||
run: |
|
||||
echo "Extension Tests Result: ${{ steps.extension_coverage.outcome }}"
|
||||
cat extension_coverage.txt
|
||||
|
||||
echo "Webview Tests Result: ${{ steps.webview_coverage.outcome }}"
|
||||
cat webview-ui/webview_coverage.txt
|
||||
|
||||
# Check if any of the test steps failed
|
||||
# https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/accessing-contextual-information-about-workflow-runs#steps-context
|
||||
if [ "${{ steps.extension_coverage.outcome }}" != "success" ] || [ "${{ steps.webview_coverage.outcome }}" != "success" ]; then
|
||||
echo "Tests failed."
|
||||
cat extension_coverage.txt
|
||||
cat webview-ui/webview_coverage.txt
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
out
|
||||
dist
|
||||
dist-standalone
|
||||
node_modules
|
||||
tmp
|
||||
.vscode-test/
|
||||
@@ -18,4 +19,4 @@ coverage
|
||||
# But don't ignore the coverage scripts in .github/scripts/
|
||||
!.github/scripts/coverage/
|
||||
|
||||
*evals.env
|
||||
*evals.env
|
||||
|
||||
+2
-1
@@ -3,5 +3,6 @@
|
||||
"useTabs": true,
|
||||
"printWidth": 130,
|
||||
"semi": false,
|
||||
"bracketSameLine": true
|
||||
"bracketSameLine": true,
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
|
||||
Vendored
+6
-1
@@ -1,5 +1,10 @@
|
||||
{
|
||||
// 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"]
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"connor4312.esbuild-problem-matchers",
|
||||
"ms-vscode.extension-test-runner",
|
||||
"bradlc.vscode-tailwindcss"
|
||||
]
|
||||
}
|
||||
|
||||
Vendored
+39
@@ -16,6 +16,45 @@
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Vendored
+16
@@ -3,6 +3,16 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "compile-standalone",
|
||||
"type": "npm",
|
||||
"script": "compile-standalone",
|
||||
"group": "build",
|
||||
"problemMatcher": [],
|
||||
"presentation": {
|
||||
"reveal": "always"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "npm: protos",
|
||||
"type": "npm",
|
||||
@@ -185,6 +195,12 @@
|
||||
"label": "stop",
|
||||
"command": "echo ${input:terminate}",
|
||||
"type": "shell"
|
||||
},
|
||||
{
|
||||
"label": "clean-sandbox",
|
||||
"type": "shell",
|
||||
"dependsOn": ["watch"],
|
||||
"command": "rm -rf .vscode-dev"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
|
||||
@@ -1,5 +1,53 @@
|
||||
# Changelog
|
||||
|
||||
## [3.16.1]
|
||||
|
||||
- Add Enable auto approve toggle switch, allowing users to easily turn auto-approve functionality on or off without losing their action settings
|
||||
- Improve Gemini retry handling with better UI feedback, showing retry progress during API request attempts
|
||||
- Fix memory leak issue that could occur during long sessions with multiple tasks
|
||||
- Improve UI for Gemini model retry attempts with clearer status updates
|
||||
- Fix quick actions functionality in auto-approve settings
|
||||
- Update UI styling for auto-approve menu items to conserve space
|
||||
|
||||
## [3.16.0]
|
||||
|
||||
- Add new workflow feature allowing users to create and manage workflow files that can be injected into conversations via slash commands
|
||||
- Add collapsible recent task list, allowing users to hide their task history when sharing their screen (Thanks @cosmix!)
|
||||
- Add global endpoint option for Vertex AI users, providing higher availability and reducing 429 errors (Thanks @soniqua!)
|
||||
- Add detection for new users to display special components and guidance
|
||||
- Add Tailwind CSS IntelliSense to the recommended extensions list
|
||||
- Fix eternal loading states when the last message is a checkpoint (Thanks @BarreiroT!)
|
||||
- Improve settings organization by migrating VSCode Advanced settings to Settings Webview
|
||||
|
||||
## [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 auto‑sending edited message
|
||||
|
||||
## [3.15.1]
|
||||
|
||||
- Fix bug where PowerShell commands weren't given enough time before giving up and showing an error
|
||||
|
||||
@@ -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 eight regions. Select a region that meets your latency, compliance, and capacity needs. Examples include:
|
||||
Vertex AI supports multiple 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
|
||||
|
||||
|
||||
@@ -71,6 +71,22 @@
|
||||
"group": "Improving Your Prompting Skills",
|
||||
"pages": ["prompting/prompt-engineering-guide", "prompting/cline-memory-bank"]
|
||||
},
|
||||
{
|
||||
"group": "Features",
|
||||
"pages": [
|
||||
"features/cline-rules",
|
||||
"features/slash-commands/workflows",
|
||||
{
|
||||
"group": "Slash Commands",
|
||||
"pages": [
|
||||
"features/slash-commands/new-task",
|
||||
"features/slash-commands/new-rule",
|
||||
"features/slash-commands/smol",
|
||||
"features/slash-commands/report-bug"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Exploring Cline's Tools",
|
||||
"pages": [
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
Cline Rules allow you to provide Cline with system-level guidance. Think of them as a persistent way to include context and preferences for your projects or globally for every conversation.
|
||||
|
||||
## Creating a Rule
|
||||
|
||||
You can create a rule by clicking the `+` button in the Rules tab. This will open a new file in your IDE which you can use to write your rule.
|
||||
|
||||
<Frame>
|
||||
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/cline-rules.png" alt="Create a Rule" />
|
||||
</Frame>
|
||||
|
||||
Once you save the file:
|
||||
|
||||
- Your rule will be stored in the `.clinerules/` directory in your project (if it's a Workspace Rule)
|
||||
- Or in the `Documents/Cline/Rules` directory (if it's a Global Rule).
|
||||
|
||||
You can also have Cline create a rule for you by using the [`/newrule` slash command](/features/slash-commands/new-rule) in the chat.
|
||||
|
||||
```markdown Example Cline Rule Structure [expandable]
|
||||
# Project Guidelines
|
||||
|
||||
## Documentation Requirements
|
||||
|
||||
- Update relevant documentation in /docs when modifying features
|
||||
- Keep README.md in sync with new capabilities
|
||||
- Maintain changelog entries in CHANGELOG.md
|
||||
|
||||
## Architecture Decision Records
|
||||
|
||||
Create ADRs in /docs/adr for:
|
||||
|
||||
- Major dependency changes
|
||||
- Architectural pattern changes
|
||||
- New integration patterns
|
||||
- Database schema changes
|
||||
Follow template in /docs/adr/template.md
|
||||
|
||||
## Code Style & Patterns
|
||||
|
||||
- Generate API clients using OpenAPI Generator
|
||||
- Use TypeScript axios template
|
||||
- Place generated code in /src/generated
|
||||
- Prefer composition over inheritance
|
||||
- Use repository pattern for data access
|
||||
- Follow error handling pattern in /src/utils/errors.ts
|
||||
|
||||
## Testing Standards
|
||||
|
||||
- Unit tests required for business logic
|
||||
- Integration tests for API endpoints
|
||||
- E2E tests for critical user flows
|
||||
```
|
||||
|
||||
### Key Benefits
|
||||
|
||||
1. **Version Controlled**: The `.clinerules` file becomes part of your project's source code
|
||||
2. **Team Consistency**: Ensures consistent behavior across all team members
|
||||
3. **Project-Specific**: Rules and standards tailored to each project's needs
|
||||
4. **Institutional Knowledge**: Maintains project standards and practices in code
|
||||
|
||||
Place the `.clinerules` file in your project's root directory:
|
||||
|
||||
```
|
||||
your-project/
|
||||
├── .clinerules
|
||||
├── src/
|
||||
├── docs/
|
||||
└── ...
|
||||
```
|
||||
|
||||
Cline's system prompt, on the other hand, is not user-editable ([here's where you can find it](https://github.com/cline/cline/blob/main/src/core/prompts/system.ts)). For a broader look at prompt engineering best practices, check out [this resource](https://docs.anthropic.com/en/docs/build-with-claude/prompt-engineering/overview).
|
||||
|
||||
### Tips for Writing Effective Cline Rules
|
||||
|
||||
- Be Clear and Concise: Use simple language and avoid ambiguity.
|
||||
- Focus on Desired Outcomes: Describe the results you want, not the specific steps.
|
||||
- Test and Iterate: Experiment to find what works best for your workflow.
|
||||
|
||||
### .clinerules/ Folder System
|
||||
|
||||
```
|
||||
your-project/
|
||||
├── .clinerules/ # Folder containing active rules
|
||||
│ ├── 01-coding.md # Core coding standards
|
||||
│ ├── 02-documentation.md # Documentation requirements
|
||||
│ └── current-sprint.md # Rules specific to current work
|
||||
├── src/
|
||||
└── ...
|
||||
```
|
||||
|
||||
Cline automatically processes **all Markdown files** inside the `.clinerules/` directory, combining them into a unified set of rules. The numeric prefixes (optional) help organize files in a logical sequence.
|
||||
|
||||
#### Using a Rules Bank
|
||||
|
||||
For projects with multiple contexts or teams, maintain a rules bank directory:
|
||||
|
||||
```
|
||||
your-project/
|
||||
├── .clinerules/ # Active rules - automatically applied
|
||||
│ ├── 01-coding.md
|
||||
│ └── client-a.md
|
||||
│
|
||||
├── clinerules-bank/ # Repository of available but inactive rules
|
||||
│ ├── clients/ # Client-specific rule sets
|
||||
│ │ ├── client-a.md
|
||||
│ │ └── client-b.md
|
||||
│ ├── frameworks/ # Framework-specific rules
|
||||
│ │ ├── react.md
|
||||
│ │ └── vue.md
|
||||
│ └── project-types/ # Project type standards
|
||||
│ ├── api-service.md
|
||||
│ └── frontend-app.md
|
||||
└── ...
|
||||
```
|
||||
|
||||
#### Benefits of the Folder Approach
|
||||
|
||||
1. **Contextual Activation**: Copy only relevant rules from the bank to the active folder
|
||||
2. **Easier Maintenance**: Update individual rule files without affecting others
|
||||
3. **Team Flexibility**: Different team members can activate rules specific to their current task
|
||||
4. **Reduced Noise**: Keep the active ruleset focused and relevant
|
||||
|
||||
#### Usage Examples
|
||||
|
||||
Switch between client projects:
|
||||
|
||||
```bash
|
||||
# Switch to Client B project
|
||||
rm .clinerules/client-a.md
|
||||
cp clinerules-bank/clients/client-b.md .clinerules/
|
||||
```
|
||||
|
||||
Adapt to different tech stacks:
|
||||
|
||||
```bash
|
||||
# Frontend React project
|
||||
cp clinerules-bank/frameworks/react.md .clinerules/
|
||||
```
|
||||
|
||||
#### Implementation Tips
|
||||
|
||||
- Keep individual rule files focused on specific concerns
|
||||
- Use descriptive filenames that clearly indicate the rule's purpose
|
||||
- Consider git-ignoring the active `.clinerules/` folder while tracking the `clinerules-bank/`
|
||||
- Create team scripts to quickly activate common rule combinations
|
||||
|
||||
The folder system transforms your Cline rules from a static document into a dynamic knowledge system that adapts to your team's changing contexts and requirements.
|
||||
|
||||
### Managing Rules with the Toggleable Popover
|
||||
|
||||
To make managing both single `.clinerules` files and the folder system even easier, Cline v3.13 introduces a dedicated popover UI directly accessible from the chat interface.
|
||||
|
||||
Located conveniently under the chat input field, this popover allows you to:
|
||||
|
||||
- **Instantly See Active Rules:** View which global rules (from your user settings) and workspace rules (`.clinerules` file or folder contents) are currently active.
|
||||
- **Quickly Toggle Rules:** Enable or disable specific rule files within your workspace `.clinerules/` folder with a single click. This is perfect for activating context-specific rules (like `react-rules.md` or `memory-bank.md`) only when needed.
|
||||
- **Easily Add/Manage Rules:** Quickly create a workspace `.clinerules` file or folder if one doesn't exist, or add new rule files to an existing folder.
|
||||
|
||||
This UI significantly simplifies switching contexts and managing different sets of instructions without needing to manually edit files or configurations during a conversation.
|
||||
|
||||
<Frame>
|
||||
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(1).png" alt="Cline Logo" />
|
||||
</Frame>
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
title: "New Rule Command"
|
||||
sidebarTitle: "/newrule"
|
||||
---
|
||||
|
||||
`/newrule` is a slash command that lets you teach Cline your preferred way of working. It creates a markdown file in your `.clinerules` directory that acts like persistent instructions for how Cline should behave when helping with your projects.
|
||||
|
||||
Think of it as setting up house rules that Cline will always follow, so you don't have to repeat your preferences in every conversation.
|
||||
|
||||
#### Using the `/newrule` Slash Command
|
||||
|
||||
When you want Cline to consistently follow certain guidelines:
|
||||
|
||||
- Type `/newrule` in the chat
|
||||
- Cline will help you create a structured rule file by asking about your preferences for:
|
||||
- Communication style (verbose vs. concise)
|
||||
- Development workflows
|
||||
- Coding standards
|
||||
- Project context
|
||||
- Any other specific guidelines
|
||||
- You'll review the rule file before it's created
|
||||
- Once approved, Cline creates a markdown file in your `.clinerules` directory that will automatically be loaded for future conversations
|
||||
|
||||
#### Example
|
||||
|
||||
I used `/newrule` when I was fed up with repeating the same instructions on every new task. I had specific preferences for how I wanted my React components structured, which testing library to use, and even my preferred variable naming style.
|
||||
|
||||
Instead of typing these preferences each time, I just used `/newrule` and worked with Cline to create a detailed rule file. We built a markdown file that covered everything from code organization to my preference for functional components over class components.
|
||||
|
||||
Now whenever I chat with Cline about my React project, it automatically follows these guidelines without me having to remind it. The best part is that I can create different rule files for different projects, so Cline adapts to whatever codebase I'm working on.
|
||||
|
||||
#### Inspiration
|
||||
|
||||
Here's how I use `/newrule` to make my development smoother:
|
||||
|
||||
- I created a rule file for each major project with specific architectural patterns and library preferences, so Cline always generates code that matches our existing codebase.
|
||||
|
||||
- For my team's shared projects, we have a common rule file that ensures consistent code style and documentation practices regardless of who's using Cline.
|
||||
|
||||
- When working with legacy code, I made a rule file that reminds Cline about the quirks and constraints of the old system, so it never suggests modern approaches that won't integrate well.
|
||||
|
||||
- I even have a personal rule file for my side projects with all my opinionated preferences - two-space indentation, arrow functions everywhere, and my exact folder structure requirements.
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
title: "New Task Command"
|
||||
sidebarTitle: "/newtask"
|
||||
---
|
||||
|
||||
`/newtask` is a slash command that works like a perfect developer handoff. It intelligently packages what matters - the overall plan, work accomplished, relevant files, and next steps - into a fresh task with a clean context window. All while leaving behind the noise of tool calls, documentation searches, and implementation details.
|
||||
|
||||
It's exactly what you'd do when bringing a new developer onto your project: provide the essential context they need to continue the work without overwhelming them with every keystroke that came before.
|
||||
|
||||
#### Using the `/newtask` Slash Command
|
||||
|
||||
When your context window is filling up but you're not done with your project:
|
||||
|
||||
<Frame>
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/docs/assets/newtask.png"
|
||||
alt="Using the /newtask slash command"
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
- Type `/newtask` in the chat input field
|
||||
- Cline will analyze your conversation and propose a distilled version of the context to carry forward
|
||||
- You can refine this proposed context through conversation before committing
|
||||
- Once satisfied, a button appears to create the new task with your refined context
|
||||
|
||||
#### Example
|
||||
|
||||
I regularly use `/newtask` when working through complex implementations with multiple steps. For instance, if I've completed 3 steps of a 10-step process and my context is already 75% full with documentation snippets, file contents, and detailed discussions.
|
||||
|
||||
Rather than losing those insights or starting from scratch, I use `/newtask` to have Cline extract what matters - the key decisions, file changes, and progress so far - without all the noise of individual tool calls and research steps.
|
||||
|
||||
I like to think of `/newtask` as a new developer joining the project. I need to give them the full understanding of the work that has been done, awareness of the relevant files, any other context that would be helpful, and where to go next.
|
||||
|
||||
#### Inspiration
|
||||
|
||||
Here are some popular ways to use `/newtask`:
|
||||
|
||||
- I research complex APIs using the Context7 MCP server, filling my context with documentation. Once I understand the concepts, I use `/newtask` to start fresh with just the essential knowledge needed for implementation.
|
||||
- After identifying the root cause of a tough bug through multiple debugging attempts and file explorations, I use `/newtask` to continue with a clean slate that includes the solution but discards all the failed attempts.
|
||||
- When a client discussion explores multiple approaches and finally settles on one direction, I use `/newtask` to focus solely on implementing the chosen solution.
|
||||
- For complex projects spanning multiple days, I use `/newtask` at logical stopping points to maintain a clean workspace while carrying forward my progress.
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
title: "Report Bug Command"
|
||||
sidebarTitle: "/reportbug"
|
||||
---
|
||||
|
||||
`/reportbug` is an absolute lifesaver when you hit a weird issue with Cline. Instead of having to remember all the details GitHub wants for a bug report, this command turns Cline into your personal bug reporting assistant.
|
||||
|
||||
It walks you through collecting all the info needed for a proper bug report and then shoots it straight to our GitHub issues page with all the right formatting and system details included.
|
||||
|
||||
#### Using the `/reportbug` Slash Command
|
||||
|
||||
When you run into something funky that doesn't seem right:
|
||||
|
||||
- Just type `/reportbug` in the chat
|
||||
- Cline will guide you through all the details we need:
|
||||
- A quick title describing the issue
|
||||
- What actually happened vs. what you expected
|
||||
- Steps to reproduce the bug
|
||||
- Any relevant output or errors you saw
|
||||
- Additional context that might help us fix it
|
||||
- You'll get to review everything before it's submitted
|
||||
- Once you approve, it opens a perfectly formatted GitHub issue with all your info plus automatic system details
|
||||
|
||||
#### Example
|
||||
|
||||
Last week I hit a weird bug where Cline kept timing out when reading large files. Instead of trying to remember all the GitHub template fields, I just typed `/reportbug` and Cline guided me through the whole process.
|
||||
|
||||
It asked me about what I was trying to do, what happened instead, and the exact steps that led to the issue. The best part was that it automatically included my OS version, Cline version, and all the technical details our devs would need.
|
||||
|
||||
A few seconds later, I had a properly formatted GitHub issue created without having to hunt down any of that info myself.
|
||||
@@ -0,0 +1,47 @@
|
||||
---
|
||||
title: "Smol Command"
|
||||
sidebarTitle: "/smol"
|
||||
---
|
||||
|
||||
`/smol` (or its alias, `/compact`) is a slash command that compresses your conversation history while preserving essential context.
|
||||
|
||||
Unlike `/newtask` which creates a new task, `/smol` condenses your current conversation into a comprehensive summary, freeing up context window space while allowing you to continue working in the same task.
|
||||
|
||||
Think of it like summarizing the relevant parts of a conversation while discarding the rest.
|
||||
|
||||
#### Using the `/smol` Slash Command
|
||||
|
||||
When your context window is getting full but you want to continue in the same task:
|
||||
|
||||
<Frame>
|
||||
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/smol.png" alt="Using the /smol slash command" />
|
||||
</Frame>
|
||||
|
||||
- Type `/smol` (or its alias `/compact`) in the chat input field
|
||||
- Cline will analyze your conversation and create a detailed summary that preserves essential information
|
||||
- You'll have a chance to review this summary and provide feedback if needed
|
||||
- Once accepted, the detailed conversation history is replaced with this condensed version
|
||||
|
||||
#### Example
|
||||
|
||||
I use `/smol` when I'm deep into a complex debugging session and need to continue in the same task. After exploring multiple approaches and examining several files, my context window gets crowded with all the back-and-forth.
|
||||
|
||||
By using `/smol`, I can condense all that exploration into a concise summary that captures what we've learned, which files we've examined, and what approaches we've tried. This frees up space to continue the debugging without losing the insights we've gained.
|
||||
|
||||
The key difference from `/newtask` is that I'm staying in the same conversation flow rather than creating a separate task. This is particularly useful when I'm in the middle of something and don't want to context switch.
|
||||
|
||||
#### Inspiration
|
||||
|
||||
Here are powerful ways I use `/smol` in my workflow:
|
||||
|
||||
- During lengthy brainstorming sessions, I use `/smol` to condense our exploration before implementing the chosen solution, all within the same task.
|
||||
- When debugging complex issues that involve multiple file checks and test runs, I use `/smol` to summarize what we've learned while continuing the debugging process.
|
||||
- For iterative development, I use `/smol` after completing each feature to compress the implementation details while keeping the key decisions and approaches accessible.
|
||||
- When gathering requirements from multiple sources, I use `/smol` to distill the essential needs into a concise summary before moving to the design phase.
|
||||
|
||||
#### Smol vs Newtask
|
||||
|
||||
People often ask me when to use `/smol` vs `/newtask`. Frankly, it's a matter of personal preference and what you're trying to achieve. Here are some guidelines:
|
||||
|
||||
- Use `/smol` when you're in the middle of something and want to keep going in the same task. It's perfect when you're deep in a debugging flow or brainstorming session and don't want to break your momentum. The downside? Once you compress your history, you can't get those detailed conversations back.
|
||||
- Use `/newtask` when you're at a logical transition point and want to start fresh. It's great for moving from planning to implementation, or when you want to preserve your full conversation history (since it creates a new task rather than overwriting your current one).
|
||||
@@ -0,0 +1,323 @@
|
||||
---
|
||||
title: "Workflows"
|
||||
sidebarTitle: "Workflows"
|
||||
---
|
||||
|
||||
Workflows allow you to define a series of steps to guide Cline through a repetitive set of tasks, such as deploying a service or submitting a PR.
|
||||
|
||||
To invoke a workflow, type `/[workflow-name.md]` in the chat.
|
||||
|
||||
## How to Create and Use Workflows
|
||||
|
||||
Workflows live alongside Cline Rules. Creating one is straightforward:
|
||||
|
||||
<Frame>
|
||||
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/workflows.png" alt="Workflows tab in Cline" />
|
||||
</Frame>
|
||||
|
||||
1. Create a markdown file with clear instructions for the steps Cline should take
|
||||
2. Save it with a `.md` extension in your workflows directory
|
||||
3. To trigger a workflow, just type `/` followed by the workflow filename
|
||||
4. Provide any required parameters when prompted
|
||||
|
||||
The real power comes from how you structure your workflow files. You can:
|
||||
|
||||
- Leverage Cline's built-in tools like `ask_followup_question`, `read_file`, and `search_files`
|
||||
- Use command-line tools you already have installed like `gh` or `docker`
|
||||
- Reference external MCP tool calls like Slack or Whatsapp
|
||||
- Chain multiple actions together in a specific sequence
|
||||
|
||||
## Real-world Example
|
||||
|
||||
I created a PR Review workflow that's already saving me tons of time.
|
||||
|
||||
````md pr-review.md [expandable]
|
||||
You have access to the `gh` terminal command. I already authenticated it for you. Please review it to use the PR that I asked you to review. You're already in the `cline` repo.
|
||||
|
||||
<detailed_sequence_of_steps>
|
||||
|
||||
# GitHub PR Review Process - Detailed Sequence of Steps
|
||||
|
||||
## 1. Gather PR Information
|
||||
|
||||
1. Get the PR title, description, and comments:
|
||||
|
||||
```bash
|
||||
gh pr view <PR-number> --json title,body,comments
|
||||
```
|
||||
|
||||
2. Get the full diff of the PR:
|
||||
```bash
|
||||
gh pr diff <PR-number>
|
||||
```
|
||||
|
||||
## 2. Understand the Context
|
||||
|
||||
1. Identify which files were modified in the PR:
|
||||
|
||||
```bash
|
||||
gh pr view <PR-number> --json files
|
||||
```
|
||||
|
||||
2. Examine the original files in the main branch to understand the context:
|
||||
|
||||
```xml
|
||||
<read_file>
|
||||
<path>path/to/file</path>
|
||||
</read_file>
|
||||
```
|
||||
|
||||
3. For specific sections of a file, you can use search_files:
|
||||
```xml
|
||||
<search_files>
|
||||
<path>path/to/directory</path>
|
||||
<regex>search term</regex>
|
||||
<file_pattern>*.ts</file_pattern>
|
||||
</search_files>
|
||||
```
|
||||
|
||||
## 3. Analyze the Changes
|
||||
|
||||
1. For each modified file, understand:
|
||||
|
||||
- What was changed
|
||||
- Why it was changed (based on PR description)
|
||||
- How it affects the codebase
|
||||
- Potential side effects
|
||||
|
||||
2. Look for:
|
||||
- Code quality issues
|
||||
- Potential bugs
|
||||
- Performance implications
|
||||
- Security concerns
|
||||
- Test coverage
|
||||
|
||||
## 4. Ask for User Confirmation
|
||||
|
||||
1. Before making a decision, ask the user if you should approve the PR, providing your assessment and justification:
|
||||
|
||||
```xml
|
||||
<ask_followup_question>
|
||||
<question>Based on my review of PR #<PR-number>, I recommend [approving/requesting changes]. Here's my justification:
|
||||
|
||||
[Detailed justification with key points about the PR quality, implementation, and any concerns]
|
||||
|
||||
Would you like me to proceed with this recommendation?</question>
|
||||
<options>["Yes, approve the PR", "Yes, request changes", "No, I'd like to discuss further"]</options>
|
||||
</ask_followup_question>
|
||||
```
|
||||
|
||||
## 5. Ask if User Wants a Comment Drafted
|
||||
|
||||
1. After the user decides on approval/rejection, ask if they would like a comment drafted:
|
||||
|
||||
```xml
|
||||
<ask_followup_question>
|
||||
<question>Would you like me to draft a comment for this PR that you can copy and paste?</question>
|
||||
<options>["Yes, please draft a comment", "No, I'll handle the comment myself"]</options>
|
||||
</ask_followup_question>
|
||||
```
|
||||
|
||||
2. If the user wants a comment drafted, provide a well-structured comment they can copy:
|
||||
|
||||
```
|
||||
Thank you for this PR! Here's my assessment:
|
||||
|
||||
[Detailed assessment with key points about the PR quality, implementation, and any suggestions]
|
||||
|
||||
[Include specific feedback on code quality, functionality, and testing]
|
||||
```
|
||||
|
||||
## 6. Make a Decision
|
||||
|
||||
1. Approve the PR if it meets quality standards:
|
||||
|
||||
```bash
|
||||
gh pr review <PR-number> --approve --body "Your approval message"
|
||||
```
|
||||
|
||||
2. Request changes if improvements are needed:
|
||||
```bash
|
||||
gh pr review <PR-number> --request-changes --body "Your feedback message"
|
||||
```
|
||||
</detailed_sequence_of_steps>
|
||||
|
||||
<example_review_process>
|
||||
|
||||
# Example PR Review Process
|
||||
|
||||
Let's walk through a real example of reviewing PR #3627 which fixes the thinking mode calculation for Claude 3.7 models.
|
||||
|
||||
## Step 1: Gather PR Information
|
||||
|
||||
```bash
|
||||
# Get PR details
|
||||
gh pr view 3627 --json title,body,comments
|
||||
|
||||
# Get the full diff
|
||||
gh pr diff 3627
|
||||
```
|
||||
|
||||
## Step 2: Understand the Context
|
||||
|
||||
```xml
|
||||
# Examine the original files to understand what's being changed
|
||||
<read_file>
|
||||
<path>src/shared/api.ts</path>
|
||||
</read_file>
|
||||
|
||||
# Look at the ThinkingBudgetSlider component implementation
|
||||
<read_file>
|
||||
<path>webview-ui/src/components/settings/ThinkingBudgetSlider.tsx</path>
|
||||
</read_file>
|
||||
|
||||
# Check how thinking mode is currently handled in API providers
|
||||
<search_files>
|
||||
<path>src/api/providers</path>
|
||||
<regex>reasoningOn</regex>
|
||||
<file_pattern>*.ts</file_pattern>
|
||||
</search_files>
|
||||
```
|
||||
|
||||
## Step 3: Analyze the Changes
|
||||
|
||||
From the PR information and code examination, we can see that:
|
||||
|
||||
1. The PR fixes an issue with the thinking mode budget calculation for Claude 3.7 models
|
||||
2. Currently, the thinking budget is incorrectly calculated as 80% of maxTokens (8192), resulting in 6553 tokens
|
||||
3. Claude 3.7 actually supports a much larger thinking budget (up to 64000 tokens)
|
||||
4. The PR adds a `thinkingConfig` property with `maxBudget: 64000` to model definitions
|
||||
5. It updates API handlers to use this value when reasoning mode is enabled
|
||||
6. It enhances the slider component to calculate max values based on model-specific percentages
|
||||
7. Comprehensive tests were added to verify the calculations
|
||||
|
||||
## Step 4: Ask for User Confirmation
|
||||
|
||||
```xml
|
||||
<ask_followup_question>
|
||||
<question>Based on my review of PR #3627, I recommend approving this PR. Here's my justification:
|
||||
|
||||
1. The PR correctly fixes the thinking mode budget calculation for Claude 3.7 models
|
||||
2. It properly implements the thinkingConfig.maxBudget property (64000) for Claude 3.7 models
|
||||
3. It applies the appropriate percentage (50%) for the slider calculation
|
||||
4. The changes are well-tested with comprehensive unit tests
|
||||
5. The implementation is clean and follows the project's coding standards
|
||||
6. The PR includes helpful screenshots demonstrating the fix
|
||||
|
||||
Would you like me to proceed with approving this PR?</question>
|
||||
<options>["Yes, approve the PR", "No, I'd like to discuss further", "Let me review it myself first"]</options>
|
||||
</ask_followup_question>
|
||||
```
|
||||
|
||||
## Step 5: Ask if User Wants a Comment Drafted
|
||||
|
||||
```xml
|
||||
<ask_followup_question>
|
||||
<question>Would you like me to draft a comment for this PR that you can copy and paste?</question>
|
||||
<options>["Yes, please draft a comment", "No, I'll handle the comment myself"]</options>
|
||||
</ask_followup_question>
|
||||
```
|
||||
|
||||
## Step 6: Make a Decision
|
||||
|
||||
```bash
|
||||
gh pr review 3627 --approve --body "This PR looks good! It correctly fixes the thinking mode budget calculation for Claude 3.7 models by using the appropriate maxBudget value (64000) and applying the right percentage (50%) for the slider. The changes are well-tested and the implementation is clean."
|
||||
```
|
||||
|
||||
</example_review_process>
|
||||
|
||||
<common_gh_commands>
|
||||
|
||||
# Common GitHub CLI Commands for PR Review
|
||||
|
||||
## Basic PR Commands
|
||||
|
||||
```bash
|
||||
# List open PRs
|
||||
gh pr list
|
||||
|
||||
# View a specific PR
|
||||
gh pr view <PR-number>
|
||||
|
||||
# View PR with specific fields
|
||||
gh pr view <PR-number> --json title,body,comments,files,commits
|
||||
|
||||
# Check PR status
|
||||
gh pr status
|
||||
```
|
||||
|
||||
## Diff and File Commands
|
||||
|
||||
```bash
|
||||
# Get the full diff of a PR
|
||||
gh pr diff <PR-number>
|
||||
|
||||
# List files changed in a PR
|
||||
gh pr view <PR-number> --json files
|
||||
|
||||
# Check out a PR locally
|
||||
gh pr checkout <PR-number>
|
||||
```
|
||||
|
||||
## Review Commands
|
||||
|
||||
```bash
|
||||
# Approve a PR
|
||||
gh pr review <PR-number> --approve --body "Your approval message"
|
||||
|
||||
# Request changes on a PR
|
||||
gh pr review <PR-number> --request-changes --body "Your feedback message"
|
||||
|
||||
# Add a comment review (without approval/rejection)
|
||||
gh pr review <PR-number> --comment --body "Your comment message"
|
||||
```
|
||||
|
||||
## Additional Commands
|
||||
|
||||
```bash
|
||||
# View PR checks status
|
||||
gh pr checks <PR-number>
|
||||
|
||||
# View PR commits
|
||||
gh pr view <PR-number> --json commits
|
||||
|
||||
# Merge a PR (if you have permission)
|
||||
gh pr merge <PR-number> --merge
|
||||
```
|
||||
|
||||
</common_gh_commands>
|
||||
````
|
||||
|
||||
When I get a new PR to review, I used to manually gather context: checking the PR description, examining the diff, looking at surrounding files, and finally forming an opinion. Now I just:
|
||||
|
||||
1. Type `/pr-review.md` in chat
|
||||
2. Paste in the PR number
|
||||
3. Let Cline handle everything else
|
||||
|
||||
My workflow uses the `gh` command-line tool to:
|
||||
|
||||
- Pull the PR description and comments
|
||||
- Examine the diff
|
||||
- Check surrounding files for context
|
||||
- Analyze potential issues
|
||||
- Asks me if it's cool approve it if everything looks good, with justification for why it should be approved
|
||||
- If I say "yes," Cline automatically approves the PR with the `gh` command
|
||||
|
||||
This has taken my PR review process from a manual, multi-step operation to a single command that gives me everything I need to make an informed decision.
|
||||
|
||||
> This is just one example of a workflow file. You can find more in our [prompts repository](https://github.com/cline/prompts) for inspiration.
|
||||
|
||||
## Building Your Own Workflows
|
||||
|
||||
The beauty of workflows is they're completely customizable to your needs. You might create workflows for all kinds of repetitive tasks:
|
||||
|
||||
- For releases, you could have a workflow that grabs all merged PRs, builds a changelog, and handles version bumps.
|
||||
- Setting up new projects is perfect for workflows. Just run one command to create your folder structure, install dependencies, and set up configs.
|
||||
- Need to create a report? Create a workflow that grabs stats from different sources and formats them exactly how you like. You can even visualize them with a charting library and then make a presentation out of it with a library like [slidev](https://sli.dev/).
|
||||
- You can even use workflows to draft messages to your team using an MCP server like Slack or Whatsapp after you submit a PR.
|
||||
|
||||
With Workflows, your imagination is the limit. The true potential comes from spotting those annoying repetitive tasks you do all the time.
|
||||
|
||||
If you can describe something as "first I do X, then Y, then Z" - that's a perfect workflow candidate.
|
||||
|
||||
Start with something small that bugs you, turn it into a workflow, and keep refining it. You'll be shocked how much of your day can be automated this way.
|
||||
@@ -26,9 +26,9 @@ For complete transparency, you can inspect our [telemetry implementation](https:
|
||||
|
||||
### How to Opt Out
|
||||
|
||||
Telemetry in Cline is entirely optional and requires your explicit consent:
|
||||
Telemetry in Cline is entirely optional:
|
||||
|
||||
- When you update or install our VS Code extension, you'll see a simple prompt: "Help Improve Cline" with Allow or Deny options
|
||||
- When you update or install our VS Code extension, you'll see a message about our anonymous telemetry
|
||||
- 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.
|
||||
|
||||
Generated
+10118
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "docs",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"dev": "mintlify dev"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"mintlify": "^4.0.538"
|
||||
}
|
||||
}
|
||||
@@ -4,212 +4,6 @@ title: "Prompt Engineering Guide"
|
||||
|
||||
Welcome to the Cline Prompting Guide! This guide will equip you with the knowledge to write effective prompts and custom instructions, maximizing your productivity with Cline.
|
||||
|
||||
## Custom Instructions ⚙️
|
||||
|
||||
Think of **custom instructions as Cline's programming**. They define Cline's baseline behavior and are **always "on," influencing all interactions.** Instructions can be broad and abstract, or specific and explicit. You might want Cline to have a unique personality, or produce output in a particular file format, or adhere to certain architectural principles. Custom instructions can standardize Cline's output in ways you define, which is especially valuable when working with others. See the [Enterprise section](../enterprise-solutions/custom-instructions.md) for using Custom Instructions in a team context.\
|
||||
\
|
||||
|
||||
<mark style="color:yellow;">
|
||||
NOTE: Modifying the Custom Instructions field updates Cline's prompt cache, discarding accumulated context. This causes a
|
||||
temporary increase in cost while that context is replaced. Update Custom Instructions between conversations whenever possible.
|
||||
</mark>
|
||||
|
||||
To add custom instructions:
|
||||
|
||||
1. Open VSCode
|
||||
2. Click the Cline extension settings dial ⚙️
|
||||
3. Find the "Custom Instructions" field
|
||||
4. Paste your instructions
|
||||
|
||||
<Frame>
|
||||
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(1).png" alt="Cline Logo" />
|
||||
</Frame>
|
||||
|
||||
Custom instructions are powerful for:
|
||||
|
||||
- Enforcing Coding Style and Best Practices: Ensure Cline always adheres to your team's coding conventions, naming conventions, and best practices.
|
||||
- Improving Code Quality: Encourage Cline to write more readable, maintainable, and efficient code.
|
||||
- Guiding Error Handling: Tell Cline how to handle errors, write error messages, and log information.
|
||||
|
||||
---
|
||||
|
||||
## .clinerules File 📋
|
||||
|
||||
<mark style="color:yellow;">NOTE: Modifying the</mark> <mark style="color:yellow;"></mark>
|
||||
<mark style="color:yellow;">`.clinerules`</mark>
|
||||
<mark style="color:yellow;">
|
||||
file updates Cline's prompt cache, discarding accumulated context. This causes a temporary increase in cost while that context
|
||||
is replaced. Update the
|
||||
</mark> <mark style="color:yellow;"></mark>
|
||||
<mark style="color:yellow;">`.clinerules`</mark> <mark style="color:yellow;"></mark>
|
||||
<mark style="color:yellow;">file between conversations whenever possible.</mark>
|
||||
|
||||
While custom instructions are user-specific and global (applying across all projects), the `.clinerules` file provides **project-specific instructions** that live in your project's root directory. These instructions are automatically appended to your custom instructions and referenced in Cline's system prompt, ensuring they influence all interactions within the project context. This makes it an excellent tool for:
|
||||
|
||||
### General Use Cases
|
||||
|
||||
The `.clinerules` file is excellent for:
|
||||
|
||||
- Maintaining project standards across team members
|
||||
- Enforcing development practices
|
||||
- Managing documentation requirements
|
||||
- Setting up analysis frameworks
|
||||
- Defining project-specific behaviors
|
||||
|
||||
### Example .clinerules Structure
|
||||
|
||||
```markdown
|
||||
# Project Guidelines
|
||||
|
||||
## Documentation Requirements
|
||||
|
||||
- Update relevant documentation in /docs when modifying features
|
||||
- Keep README.md in sync with new capabilities
|
||||
- Maintain changelog entries in CHANGELOG.md
|
||||
|
||||
## Architecture Decision Records
|
||||
|
||||
Create ADRs in /docs/adr for:
|
||||
|
||||
- Major dependency changes
|
||||
- Architectural pattern changes
|
||||
- New integration patterns
|
||||
- Database schema changes
|
||||
Follow template in /docs/adr/template.md
|
||||
|
||||
## Code Style & Patterns
|
||||
|
||||
- Generate API clients using OpenAPI Generator
|
||||
- Use TypeScript axios template
|
||||
- Place generated code in /src/generated
|
||||
- Prefer composition over inheritance
|
||||
- Use repository pattern for data access
|
||||
- Follow error handling pattern in /src/utils/errors.ts
|
||||
|
||||
## Testing Standards
|
||||
|
||||
- Unit tests required for business logic
|
||||
- Integration tests for API endpoints
|
||||
- E2E tests for critical user flows
|
||||
```
|
||||
|
||||
### Key Benefits
|
||||
|
||||
1. **Version Controlled**: The `.clinerules` file becomes part of your project's source code
|
||||
2. **Team Consistency**: Ensures consistent behavior across all team members
|
||||
3. **Project-Specific**: Rules and standards tailored to each project's needs
|
||||
4. **Institutional Knowledge**: Maintains project standards and practices in code
|
||||
|
||||
Place the `.clinerules` file in your project's root directory:
|
||||
|
||||
```
|
||||
your-project/
|
||||
├── .clinerules
|
||||
├── src/
|
||||
├── docs/
|
||||
└── ...
|
||||
```
|
||||
|
||||
Cline's system prompt, on the other hand, is not user-editable ([here's where you can find it](https://github.com/cline/cline/blob/main/src/core/prompts/system.ts)). For a broader look at prompt engineering best practices, check out [this resource](https://docs.anthropic.com/en/docs/build-with-claude/prompt-engineering/overview).
|
||||
|
||||
### Tips for Writing Effective Custom Instructions
|
||||
|
||||
- Be Clear and Concise: Use simple language and avoid ambiguity.
|
||||
- Focus on Desired Outcomes: Describe the results you want, not the specific steps.
|
||||
- Test and Iterate: Experiment to find what works best for your workflow.
|
||||
|
||||
### .clinerules Folder System 📂
|
||||
|
||||
While a single `.clinerules` file works well for simpler projects, Cline now supports a `.clinerules` folder for more sophisticated rule organization. This modular approach brings several advantages:
|
||||
|
||||
#### How It Works
|
||||
|
||||
Instead of a single file, create a `.clinerules/` directory in your project root:
|
||||
|
||||
```
|
||||
your-project/
|
||||
├── .clinerules/ # Folder containing active rules
|
||||
│ ├── 01-coding.md # Core coding standards
|
||||
│ ├── 02-documentation.md # Documentation requirements
|
||||
│ └── current-sprint.md # Rules specific to current work
|
||||
├── src/
|
||||
└── ...
|
||||
```
|
||||
|
||||
Cline automatically processes **all Markdown files** inside the `.clinerules/` directory, combining them into a unified set of rules. The numeric prefixes (optional) help organize files in a logical sequence.
|
||||
|
||||
#### Using a Rules Bank
|
||||
|
||||
For projects with multiple contexts or teams, maintain a rules bank directory:
|
||||
|
||||
```
|
||||
your-project/
|
||||
├── .clinerules/ # Active rules - automatically applied
|
||||
│ ├── 01-coding.md
|
||||
│ └── client-a.md
|
||||
│
|
||||
├── clinerules-bank/ # Repository of available but inactive rules
|
||||
│ ├── clients/ # Client-specific rule sets
|
||||
│ │ ├── client-a.md
|
||||
│ │ └── client-b.md
|
||||
│ ├── frameworks/ # Framework-specific rules
|
||||
│ │ ├── react.md
|
||||
│ │ └── vue.md
|
||||
│ └── project-types/ # Project type standards
|
||||
│ ├── api-service.md
|
||||
│ └── frontend-app.md
|
||||
└── ...
|
||||
```
|
||||
|
||||
#### Benefits of the Folder Approach
|
||||
|
||||
1. **Contextual Activation**: Copy only relevant rules from the bank to the active folder
|
||||
2. **Easier Maintenance**: Update individual rule files without affecting others
|
||||
3. **Team Flexibility**: Different team members can activate rules specific to their current task
|
||||
4. **Reduced Noise**: Keep the active ruleset focused and relevant
|
||||
|
||||
#### Usage Examples
|
||||
|
||||
Switch between client projects:
|
||||
|
||||
```bash
|
||||
# Switch to Client B project
|
||||
rm .clinerules/client-a.md
|
||||
cp clinerules-bank/clients/client-b.md .clinerules/
|
||||
```
|
||||
|
||||
Adapt to different tech stacks:
|
||||
|
||||
```bash
|
||||
# Frontend React project
|
||||
cp clinerules-bank/frameworks/react.md .clinerules/
|
||||
```
|
||||
|
||||
#### Implementation Tips
|
||||
|
||||
- Keep individual rule files focused on specific concerns
|
||||
- Use descriptive filenames that clearly indicate the rule's purpose
|
||||
- Consider git-ignoring the active `.clinerules/` folder while tracking the `clinerules-bank/`
|
||||
- Create team scripts to quickly activate common rule combinations
|
||||
|
||||
The folder system transforms your Cline rules from a static document into a dynamic knowledge system that adapts to your team's changing contexts and requirements.
|
||||
|
||||
### Managing Rules with the Toggleable Popover
|
||||
|
||||
To make managing both single `.clinerules` files and the folder system even easier, Cline v3.13 introduces a dedicated popover UI directly accessible from the chat interface.
|
||||
|
||||
Located conveniently under the chat input field, this popover allows you to:
|
||||
|
||||
- **Instantly See Active Rules:** View which global rules (from your user settings) and workspace rules (`.clinerules` file or folder contents) are currently active.
|
||||
- **Quickly Toggle Rules:** Enable or disable specific rule files within your workspace `.clinerules/` folder with a single click. This is perfect for activating context-specific rules (like `react-rules.md` or `memory-bank.md`) only when needed.
|
||||
- **Easily Add/Manage Rules:** Quickly create a workspace `.clinerules` file or folder if one doesn't exist, or add new rule files to an existing folder.
|
||||
|
||||
This UI significantly simplifies switching contexts and managing different sets of instructions without needing to manually edit files or configurations during a conversation.
|
||||
|
||||
<Frame>
|
||||
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(1).png" alt="Cline Logo" />
|
||||
</Frame>
|
||||
|
||||
## .clineignore File Guide
|
||||
|
||||
### Overview
|
||||
|
||||
+24
-5
@@ -4,6 +4,8 @@ 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}
|
||||
@@ -85,7 +87,7 @@ const copyWasmFiles = {
|
||||
build.onEnd(() => {
|
||||
// tree sitter
|
||||
const sourceDir = path.join(__dirname, "node_modules", "web-tree-sitter")
|
||||
const targetDir = path.join(__dirname, "dist")
|
||||
const targetDir = path.join(__dirname, destDir)
|
||||
|
||||
// Copy tree-sitter.wasm
|
||||
fs.copyFileSync(path.join(sourceDir, "tree-sitter.wasm"), path.join(targetDir, "tree-sitter.wasm"))
|
||||
@@ -117,7 +119,8 @@ const copyWasmFiles = {
|
||||
},
|
||||
}
|
||||
|
||||
const extensionConfig = {
|
||||
// Base configuration shared between extension and standalone builds
|
||||
const baseConfig = {
|
||||
bundle: true,
|
||||
minify: production,
|
||||
sourcemap: !production,
|
||||
@@ -140,16 +143,32 @@ const extensionConfig = {
|
||||
},
|
||||
},
|
||||
],
|
||||
entryPoints: ["src/extension.ts"],
|
||||
format: "cjs",
|
||||
sourcesContent: false,
|
||||
platform: "node",
|
||||
outfile: "dist/extension.js",
|
||||
}
|
||||
|
||||
// Extension-specific configuration
|
||||
const extensionConfig = {
|
||||
...baseConfig,
|
||||
entryPoints: ["src/extension.ts"],
|
||||
outfile: `${destDir}/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 extensionCtx = await esbuild.context(extensionConfig)
|
||||
const config = standalone ? standaloneConfig : extensionConfig
|
||||
const extensionCtx = await esbuild.context(config)
|
||||
if (watch) {
|
||||
await extensionCtx.watch()
|
||||
} else {
|
||||
|
||||
Generated
+1048
-110
File diff suppressed because it is too large
Load Diff
+16
-76
@@ -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.1",
|
||||
"version": "3.16.1",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.84.0"
|
||||
@@ -40,6 +40,8 @@
|
||||
"llama"
|
||||
],
|
||||
"activationEvents": [
|
||||
"onLanguage",
|
||||
"onStartupFinished",
|
||||
"workspaceContains:evals.env"
|
||||
],
|
||||
"main": "./dist/extension.js",
|
||||
@@ -234,87 +236,20 @@
|
||||
},
|
||||
"configuration": {
|
||||
"title": "Cline",
|
||||
"properties": {
|
||||
"cline.vsCodeLmModelSelector": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"vendor": {
|
||||
"type": "string",
|
||||
"description": "The vendor of the language model (e.g. copilot)"
|
||||
},
|
||||
"family": {
|
||||
"type": "string",
|
||||
"description": "The family of the language model (e.g. gpt-4)"
|
||||
}
|
||||
},
|
||||
"description": "Settings for VSCode Language Model API"
|
||||
},
|
||||
"cline.enableCheckpoints": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Enables extension to save checkpoints of workspace throughout the task. Uses git under the hood which may not work well with large workspaces."
|
||||
},
|
||||
"cline.disableBrowserTool": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Disables extension from spawning browser session."
|
||||
},
|
||||
"cline.modelSettings.o3Mini.reasoningEffort": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"low",
|
||||
"medium",
|
||||
"high"
|
||||
],
|
||||
"default": "medium",
|
||||
"description": "Controls the reasoning effort when using an OpenAI reasoning model. Higher values may result in more thorough but slower responses."
|
||||
},
|
||||
"cline.chromeExecutablePath": {
|
||||
"type": "string",
|
||||
"default": null,
|
||||
"description": "Path to Chrome executable for browser use functionality. If not set, the extension will attempt to find or download it automatically."
|
||||
},
|
||||
"cline.preferredLanguage": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"English",
|
||||
"Arabic - العربية",
|
||||
"Portuguese - Português (Brasil)",
|
||||
"Czech - Čeština",
|
||||
"French - Français",
|
||||
"German - Deutsch",
|
||||
"Hindi - हिन्दी",
|
||||
"Hungarian - Magyar",
|
||||
"Italian - Italiano",
|
||||
"Japanese - 日本語",
|
||||
"Korean - 한국어",
|
||||
"Polish - Polski",
|
||||
"Portuguese - Português (Portugal)",
|
||||
"Russian - Русский",
|
||||
"Simplified Chinese - 简体中文",
|
||||
"Spanish - Español",
|
||||
"Traditional Chinese - 繁體中文",
|
||||
"Turkish - Türkçe"
|
||||
],
|
||||
"default": "English",
|
||||
"description": "The language that Cline should use for communication."
|
||||
},
|
||||
"cline.mcpMarketplace.enabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Controls whether the MCP Marketplace is enabled."
|
||||
}
|
||||
}
|
||||
"properties": {}
|
||||
}
|
||||
},
|
||||
"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 && prettier src/shared/proto src/core/controller webview-ui/src/services --write",
|
||||
"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",
|
||||
"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",
|
||||
@@ -357,8 +292,8 @@
|
||||
"@types/vscode": "^1.84.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.14.1",
|
||||
"@typescript-eslint/parser": "^7.11.0",
|
||||
"@vscode/test-cli": "^0.0.9",
|
||||
"@vscode/test-electron": "^2.4.0",
|
||||
"@vscode/test-cli": "^0.0.10",
|
||||
"@vscode/test-electron": "^2.4.1",
|
||||
"chai": "^4.3.10",
|
||||
"chalk": "^5.3.0",
|
||||
"esbuild": "^0.25.0",
|
||||
@@ -384,8 +319,9 @@
|
||||
"@aws-sdk/client-bedrock-runtime": "^3.758.0",
|
||||
"@bufbuild/protobuf": "^2.2.5",
|
||||
"@google-cloud/vertexai": "^1.9.3",
|
||||
"@google/genai": "^0.9.0",
|
||||
"@google/genai": "^0.13.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",
|
||||
@@ -396,6 +332,7 @@
|
||||
"@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",
|
||||
@@ -409,6 +346,7 @@
|
||||
"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",
|
||||
@@ -417,6 +355,7 @@
|
||||
"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",
|
||||
@@ -430,6 +369,7 @@
|
||||
"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"
|
||||
|
||||
@@ -12,4 +12,8 @@ service AccountService {
|
||||
// Generates a secure nonce for state validation, stores it in secrets,
|
||||
// and opens the authentication URL in the external browser.
|
||||
rpc accountLoginClicked(EmptyRequest) returns (String);
|
||||
|
||||
// Handles the user clicking the logout button in the UI.
|
||||
// Clears API keys and user state.
|
||||
rpc accountLogoutClicked(EmptyRequest) returns (Empty);
|
||||
}
|
||||
|
||||
@@ -40,6 +40,8 @@ message BrowserSettings {
|
||||
Viewport viewport = 1;
|
||||
optional string remote_browser_host = 2;
|
||||
optional bool remote_browser_enabled = 3;
|
||||
optional string chrome_executable_path = 4;
|
||||
optional bool disable_tool_use = 5;
|
||||
}
|
||||
|
||||
message UpdateBrowserSettingsRequest {
|
||||
@@ -47,4 +49,6 @@ message UpdateBrowserSettingsRequest {
|
||||
Viewport viewport = 2;
|
||||
optional string remote_browser_host = 3;
|
||||
optional bool remote_browser_enabled = 4;
|
||||
optional string chrome_executable_path = 5;
|
||||
optional bool disable_tool_use = 6;
|
||||
}
|
||||
|
||||
+34
-21
@@ -29,6 +29,7 @@ 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))
|
||||
@@ -54,30 +55,42 @@ 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 })
|
||||
const protoFiles = await globby("*.proto", { cwd: SCRIPT_DIR, absolute: true })
|
||||
|
||||
for (const protoFile of protoFiles) {
|
||||
console.log(chalk.cyan(`Generating TypeScript code for ${protoFile}...`))
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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 descriptorOutDir = path.join(ROOT_DIR, "dist-standalone", "proto")
|
||||
await fs.mkdir(descriptorOutDir, { recursive: true })
|
||||
|
||||
try {
|
||||
const execOptions = {
|
||||
stdio: "inherit",
|
||||
}
|
||||
execSync(protocCommand, execOptions)
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Error generating TypeScript for ${protoFile}:`), error)
|
||||
process.exit(1)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
console.log(chalk.green("Protocol Buffer code generation completed successfully."))
|
||||
|
||||
@@ -8,6 +8,9 @@ import "common.proto";
|
||||
|
||||
// Service for file-related operations
|
||||
service FileService {
|
||||
// Copies text to clipboard
|
||||
rpc copyToClipboard(StringRequest) returns (Empty);
|
||||
|
||||
// Opens a file in the editor
|
||||
rpc openFile(StringRequest) returns (Empty);
|
||||
|
||||
@@ -22,12 +25,18 @@ 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);
|
||||
|
||||
// Search for files in the workspace with fuzzy matching
|
||||
rpc searchFiles(FileSearchRequest) returns (FileSearchResults);
|
||||
|
||||
// Toggle a Cline rule (enable or disable)
|
||||
rpc toggleClineRule(ToggleClineRuleRequest) returns (ToggleClineRules);
|
||||
}
|
||||
|
||||
// Request to convert a list of URIs to relative paths
|
||||
@@ -82,6 +91,7 @@ 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
|
||||
@@ -90,3 +100,22 @@ message RuleFile {
|
||||
string display_name = 2; // Filename for display purposes
|
||||
bool already_exists = 3; // For createRuleFile, indicates if file already existed
|
||||
}
|
||||
|
||||
// Request to toggle a Cline rule
|
||||
message ToggleClineRuleRequest {
|
||||
Metadata metadata = 1;
|
||||
bool is_global = 2; // Whether this is a global rule or workspace rule
|
||||
string rule_path = 3; // Path to the rule file
|
||||
bool enabled = 4; // Whether to enable or disable the rule
|
||||
}
|
||||
|
||||
// Maps from filepath to enabled/disabled status, matching app's ClineRulesToggles type
|
||||
message ClineRulesToggles {
|
||||
map<string, bool> toggles = 1;
|
||||
}
|
||||
|
||||
// Response for toggleClineRule operation
|
||||
message ToggleClineRules {
|
||||
ClineRulesToggles global_cline_rules_toggles = 1;
|
||||
ClineRulesToggles local_cline_rules_toggles = 2;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,10 @@ service McpService {
|
||||
rpc toggleMcpServer(ToggleMcpServerRequest) returns (McpServers);
|
||||
rpc updateMcpTimeout(UpdateMcpTimeoutRequest) returns (McpServers);
|
||||
rpc addRemoteMcpServer(AddRemoteMcpServerRequest) returns (McpServers);
|
||||
rpc downloadMcp(StringRequest) returns (Empty);
|
||||
rpc restartMcpServer(StringRequest) returns (McpServers);
|
||||
rpc deleteMcpServer(StringRequest) returns (McpServers);
|
||||
rpc toggleToolAutoApprove(ToggleToolAutoApproveRequest) returns (McpServers);
|
||||
}
|
||||
|
||||
message ToggleMcpServerRequest {
|
||||
@@ -30,6 +34,13 @@ message AddRemoteMcpServerRequest {
|
||||
string server_url = 3;
|
||||
}
|
||||
|
||||
message ToggleToolAutoApproveRequest {
|
||||
Metadata metadata = 1;
|
||||
string server_name = 2;
|
||||
repeated string tool_names = 3;
|
||||
bool auto_approve = 4;
|
||||
}
|
||||
|
||||
message McpTool {
|
||||
string name = 1;
|
||||
optional string description = 2;
|
||||
|
||||
+6
-5
@@ -15,9 +15,11 @@ service ModelsService {
|
||||
// Fetches available models from VS Code LM API
|
||||
rpc getVsCodeLmModels(EmptyRequest) returns (VsCodeLmModelsArray);
|
||||
// Refreshes and returns OpenRouter models
|
||||
rpc refreshOpenRouterModels(EmptyRequest) returns (OpenRouterModels);
|
||||
rpc refreshOpenRouterModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
|
||||
// Refreshes and returns OpenAI models
|
||||
rpc refreshOpenAiModels(OpenAiModelsRequest) returns (StringArray);
|
||||
// Refreshes and returns Requesty models
|
||||
rpc refreshRequestyModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
|
||||
}
|
||||
|
||||
// List of VS Code LM models
|
||||
@@ -33,7 +35,7 @@ message VsCodeLmModel {
|
||||
string id = 4;
|
||||
}
|
||||
|
||||
// For ModelInfo structure in OpenRouterModels
|
||||
// For OpenRouterCompatibleModelInfo structure in OpenRouterModels
|
||||
message OpenRouterModelInfo {
|
||||
int32 max_tokens = 1;
|
||||
int32 context_window = 2;
|
||||
@@ -46,8 +48,8 @@ message OpenRouterModelInfo {
|
||||
string description = 9;
|
||||
}
|
||||
|
||||
// Response message for OpenRouter models
|
||||
message OpenRouterModels {
|
||||
// Shared response message for model information
|
||||
message OpenRouterCompatibleModelInfo {
|
||||
map<string, OpenRouterModelInfo> models = 1;
|
||||
}
|
||||
|
||||
@@ -57,4 +59,3 @@ message OpenAiModelsRequest {
|
||||
string baseUrl = 2;
|
||||
string apiKey = 3;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
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);
|
||||
}
|
||||
@@ -6,8 +6,33 @@ import "common.proto";
|
||||
service StateService {
|
||||
rpc getLatestState(EmptyRequest) returns (State);
|
||||
rpc subscribeToState(EmptyRequest) returns (stream State);
|
||||
rpc toggleFavoriteModel(StringRequest) returns (Empty);
|
||||
rpc resetState(EmptyRequest) returns (Empty);
|
||||
rpc togglePlanActMode(TogglePlanActModeRequest) returns (Empty);
|
||||
}
|
||||
|
||||
message State {
|
||||
string state_json = 1;
|
||||
}
|
||||
|
||||
message TogglePlanActModeRequest {
|
||||
Metadata metadata = 1;
|
||||
ChatSettings chat_settings = 2;
|
||||
optional ChatContent chat_content = 3;
|
||||
}
|
||||
|
||||
enum PlanActMode {
|
||||
PLAN = 0;
|
||||
ACT = 1;
|
||||
}
|
||||
|
||||
message ChatSettings {
|
||||
PlanActMode mode = 1;
|
||||
optional string preferred_language = 2;
|
||||
optional string open_ai_reasoning_effort = 3;
|
||||
}
|
||||
|
||||
message ChatContent {
|
||||
optional string message = 1;
|
||||
repeated string images = 2;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,12 @@ 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);
|
||||
// Records task feedback (thumbs up/down)
|
||||
rpc taskFeedback(StringRequest) returns (Empty);
|
||||
// Shows task completion changes diff in a view
|
||||
rpc taskCompletionViewChanges(Int64Request) returns (Empty);
|
||||
}
|
||||
|
||||
// Request message for creating a new task
|
||||
@@ -67,6 +73,7 @@ message GetTaskHistoryRequest {
|
||||
bool favorites_only = 2;
|
||||
string search_query = 3;
|
||||
string sort_by = 4;
|
||||
bool current_workspace_only = 5;
|
||||
}
|
||||
|
||||
// Response for task history
|
||||
@@ -88,3 +95,11 @@ 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;
|
||||
}
|
||||
|
||||
@@ -8,9 +8,19 @@ import "common.proto";
|
||||
|
||||
service WebService {
|
||||
rpc checkIsImageUrl(StringRequest) returns (IsImageUrl);
|
||||
rpc fetchOpenGraphData(StringRequest) returns (OpenGraphData);
|
||||
}
|
||||
|
||||
message IsImageUrl {
|
||||
bool is_image = 1;
|
||||
string url = 2;
|
||||
}
|
||||
|
||||
message OpenGraphData {
|
||||
string title = 1;
|
||||
string description = 2;
|
||||
string image = 3;
|
||||
string url = 4;
|
||||
string site_name = 5;
|
||||
string type = 6;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
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}.`)
|
||||
@@ -0,0 +1,96 @@
|
||||
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)
|
||||
})
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
#!/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)
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
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" })
|
||||
// Move the vscode directory into node_modules.
|
||||
// It can't be installed using npm because it will create a symlink which is not portable.
|
||||
fs.renameSync("vscode", path.join("node_modules", "vscode"))
|
||||
} catch (error) {
|
||||
console.error("Error during setup:", 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()
|
||||
+1
-1
@@ -9,7 +9,7 @@ try {
|
||||
execSync("which xvfb-run", { stdio: "ignore" })
|
||||
|
||||
console.log("xvfb-run is installed. Running tests with xvfb-run...")
|
||||
execSync("xvfb-run -a npm run test:integration", { stdio: "inherit" })
|
||||
execSync("xvfb-run -a npm run test:coverage", { stdio: "inherit" })
|
||||
} else {
|
||||
console.log("Non-Linux environment detected. Running tests normally.")
|
||||
execSync("npm run test:integration", { stdio: "inherit" })
|
||||
|
||||
@@ -19,6 +19,7 @@ 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"
|
||||
@@ -58,6 +59,8 @@ 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":
|
||||
|
||||
@@ -33,7 +33,7 @@ export class ClineHandler implements ApiHandler {
|
||||
systemPrompt,
|
||||
messages,
|
||||
this.getModel(),
|
||||
this.options.o3MiniReasoningEffort,
|
||||
this.options.reasoningEffort,
|
||||
this.options.thinkingBudgetTokens,
|
||||
this.options.openRouterProviderSorting,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
+108
-45
@@ -1,11 +1,12 @@
|
||||
import type { Anthropic } from "@anthropic-ai/sdk"
|
||||
// Restore GenerateContentConfig import and add GenerateContentResponseUsageMetadata
|
||||
import { GoogleGenAI, type Content, type GenerateContentConfig, type GenerateContentResponseUsageMetadata } from "@google/genai"
|
||||
import { GoogleGenAI, 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
|
||||
@@ -72,9 +73,13 @@ export class GeminiHandler implements ApiHandler {
|
||||
* @param messages The conversation history to include in the message
|
||||
* @returns An async generator that yields chunks of the response with accurate immediate costs
|
||||
*/
|
||||
@withRetry()
|
||||
@withRetry({
|
||||
maxRetries: 4,
|
||||
baseDelay: 2000,
|
||||
maxDelay: 15000,
|
||||
})
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const { id: model, info } = this.getModel()
|
||||
const { id: modelId, info } = this.getModel()
|
||||
const contents = messages.map(convertAnthropicMessageToGemini)
|
||||
|
||||
// Configure thinking budget if supported
|
||||
@@ -98,52 +103,110 @@ export class GeminiHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
// Generate content using the configured parameters
|
||||
const result = await this.client.models.generateContentStream({
|
||||
model,
|
||||
contents: contents,
|
||||
config: {
|
||||
...requestConfig,
|
||||
},
|
||||
})
|
||||
|
||||
// Track usage metadata
|
||||
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
|
||||
let lastUsageMetadata: GenerateContentResponseUsageMetadata | undefined
|
||||
|
||||
// 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,
|
||||
try {
|
||||
const result = await this.client.models.generateContentStream({
|
||||
model: modelId,
|
||||
contents: contents,
|
||||
config: {
|
||||
...requestConfig,
|
||||
},
|
||||
})
|
||||
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
cacheReadTokens,
|
||||
cacheWriteTokens: 0,
|
||||
totalCost,
|
||||
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.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ export class LiteLlmHandler implements ApiHandler {
|
||||
const reasoningOn = budgetTokens !== 0 ? true : false
|
||||
const thinkingConfig = reasoningOn ? { type: "enabled", budget_tokens: budgetTokens } : undefined
|
||||
|
||||
let temperature: number | undefined = 0
|
||||
let temperature: number | undefined = this.options.liteLlmModelInfo?.temperature ?? 0
|
||||
|
||||
if (isOminiModel && reasoningOn) {
|
||||
temperature = undefined // Thinking mode doesn't support temperature
|
||||
@@ -169,7 +169,7 @@ export class LiteLlmHandler implements ApiHandler {
|
||||
getModel() {
|
||||
return {
|
||||
id: this.options.liteLlmModelId || liteLlmDefaultModelId,
|
||||
info: liteLlmModelInfoSaneDefaults,
|
||||
info: this.options.liteLlmModelInfo || liteLlmModelInfoSaneDefaults,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,8 +66,9 @@ export class OpenAiNativeHandler implements ApiHandler {
|
||||
messages: [{ role: "developer", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
reasoning_effort: (this.options.o3MiniReasoningEffort as ChatCompletionReasoningEffort) || "medium",
|
||||
reasoning_effort: (this.options.reasoningEffort as ChatCompletionReasoningEffort) || "medium",
|
||||
})
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
if (delta?.content) {
|
||||
|
||||
@@ -65,7 +65,7 @@ export class OpenAiHandler implements ApiHandler {
|
||||
if (isReasoningModelFamily) {
|
||||
openAiMessages = [{ role: "developer", content: systemPrompt }, ...convertToOpenAiMessages(messages)]
|
||||
temperature = undefined // does not support temperature
|
||||
reasoningEffort = (this.options.o3MiniReasoningEffort as ChatCompletionReasoningEffort) || "medium"
|
||||
reasoningEffort = (this.options.reasoningEffort as ChatCompletionReasoningEffort) || "medium"
|
||||
}
|
||||
|
||||
const stream = await this.client.chat.completions.create({
|
||||
|
||||
@@ -35,7 +35,7 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
systemPrompt,
|
||||
messages,
|
||||
this.getModel(),
|
||||
this.options.o3MiniReasoningEffort,
|
||||
this.options.reasoningEffort,
|
||||
this.options.thinkingBudgetTokens,
|
||||
this.options.openRouterProviderSorting,
|
||||
)
|
||||
|
||||
@@ -32,7 +32,7 @@ export class RequestyHandler implements ApiHandler {
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
|
||||
const reasoningEffort = this.options.o3MiniReasoningEffort || "medium"
|
||||
const reasoningEffort = this.options.reasoningEffort || "medium"
|
||||
const reasoning = { reasoning_effort: reasoningEffort }
|
||||
const reasoningArgs = model.id.startsWith("openai/o") ? reasoning : {}
|
||||
|
||||
|
||||
@@ -54,6 +54,15 @@ export function withRetry(options: RetryOptions = {}) {
|
||||
delay = Math.min(maxDelay, baseDelay * Math.pow(2, attempt))
|
||||
}
|
||||
|
||||
const handlerInstance = this as any
|
||||
if (handlerInstance.options?.onRetryAttempt) {
|
||||
try {
|
||||
handlerInstance.options.onRetryAttempt(attempt + 1, maxRetries, delay, error)
|
||||
} catch (e) {
|
||||
console.error("Error in onRetryAttempt callback:", e)
|
||||
}
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, delay))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ export async function createOpenRouterStream(
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
model: { id: string; info: ModelInfo },
|
||||
o3MiniReasoningEffort?: string,
|
||||
reasoningEffort?: string,
|
||||
thinkingBudgetTokens?: number,
|
||||
openRouterProviderSorting?: string,
|
||||
) {
|
||||
@@ -75,74 +75,6 @@ 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
|
||||
@@ -212,7 +144,7 @@ export async function createOpenRouterStream(
|
||||
stream_options: { include_usage: true },
|
||||
transforms: shouldApplyMiddleOutTransform ? ["middle-out"] : undefined,
|
||||
include_reasoning: true,
|
||||
...(model.id.startsWith("openai/o") ? { reasoning_effort: o3MiniReasoningEffort || "medium" } : {}),
|
||||
...(model.id.startsWith("openai/o") ? { reasoning_effort: reasoningEffort || "medium" } : {}),
|
||||
...(reasoning ? { reasoning } : {}),
|
||||
...(openRouterProviderSorting ? { provider: { sort: openRouterProviderSorting } } : {}),
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export type AssistantMessageContent = TextContent | ToolUse
|
||||
|
||||
export { parseAssistantMessage } from "./parse-assistant-message"
|
||||
export { parseAssistantMessageV1, parseAssistantMessageV2 } from "./parse-assistant-message"
|
||||
|
||||
export interface TextContent {
|
||||
type: "text"
|
||||
|
||||
@@ -1,6 +1,24 @@
|
||||
import { AssistantMessageContent, TextContent, ToolUse, ToolParamName, toolParamNames, toolUseNames, ToolUseName } from "."
|
||||
import { AssistantMessageContent, TextContent, ToolUse, ToolParamName, toolParamNames, toolUseNames, ToolUseName } from "." // Assuming types are defined in index.ts or a similar file
|
||||
|
||||
export function parseAssistantMessage(assistantMessage: string) {
|
||||
/**
|
||||
* @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[] {
|
||||
const contentBlocks: AssistantMessageContent[] = []
|
||||
let currentTextContent: TextContent | undefined = undefined
|
||||
let currentTextContentStartIndex = 0
|
||||
@@ -14,46 +32,56 @@ export function parseAssistantMessage(assistantMessage: string) {
|
||||
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
|
||||
// End of param value found
|
||||
currentToolUse.params[currentParamName] = currentParamValue.slice(0, -paramClosingTag.length).trim()
|
||||
currentParamName = undefined
|
||||
continue
|
||||
currentParamName = undefined // Go back to parsing tool content or looking for next param
|
||||
continue // Move to next character
|
||||
} else {
|
||||
// partial param value is accumulating
|
||||
continue
|
||||
// Partial param value is accumulating
|
||||
continue // Move to next character
|
||||
}
|
||||
}
|
||||
|
||||
// --- 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
|
||||
// End of a tool use found
|
||||
currentToolUse.partial = false
|
||||
contentBlocks.push(currentToolUse)
|
||||
currentToolUse = undefined
|
||||
continue
|
||||
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
|
||||
} 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
|
||||
// Start of a new parameter found
|
||||
currentParamName = paramOpeningTag.slice(1, -1) as ToolParamName
|
||||
currentParamValueStartIndex = accumulator.length
|
||||
foundParamStart = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (foundParamStart) {
|
||||
continue // Move to next character
|
||||
}
|
||||
|
||||
// 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.
|
||||
// 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.
|
||||
const contentParamName: ToolParamName = "content"
|
||||
if (
|
||||
(currentToolUse.name === "write_to_file" || currentToolUse.name === "new_rule") &&
|
||||
@@ -63,73 +91,384 @@ export function parseAssistantMessage(assistantMessage: string) {
|
||||
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)
|
||||
if (contentStartIndex !== -1 && contentEndIndex !== -1 && contentEndIndex > contentStartIndex) {
|
||||
currentToolUse.params[contentParamName] = toolContent.slice(contentStartIndex, contentEndIndex).trim()
|
||||
|
||||
// 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.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// partial tool value is accumulating
|
||||
continue
|
||||
// If none of the above, partial tool value is accumulating
|
||||
continue // Move to next character
|
||||
}
|
||||
}
|
||||
|
||||
// --- 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
|
||||
// Start of a new tool use found
|
||||
const toolName = toolUseOpeningTag.slice(1, -1) as ToolUseName
|
||||
currentToolUse = {
|
||||
type: "tool_use",
|
||||
name: toolUseOpeningTag.slice(1, -1) as ToolUseName,
|
||||
name: toolName,
|
||||
params: {},
|
||||
partial: true,
|
||||
}
|
||||
currentToolUseStartIndex = accumulator.length
|
||||
// this also indicates the end of the current text content
|
||||
|
||||
// This also indicates the end of the current text content block (if any)
|
||||
if (currentTextContent) {
|
||||
currentTextContent.partial = false
|
||||
// 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)
|
||||
// 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)
|
||||
}
|
||||
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
|
||||
break // Found tool start, stop checking for others
|
||||
}
|
||||
}
|
||||
|
||||
if (!didStartToolUse) {
|
||||
// no tool use, so it must be text either at the beginning or between tools
|
||||
// No tool use started, so it must be text content accumulating
|
||||
// (or continuing after a closed tool use)
|
||||
if (currentTextContent === undefined) {
|
||||
currentTextContentStartIndex = i
|
||||
}
|
||||
currentTextContent = {
|
||||
type: "text",
|
||||
content: accumulator.slice(currentTextContentStartIndex).trim(),
|
||||
partial: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
// 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) {
|
||||
// stream did not complete tool call, add it as partial
|
||||
// If a parameter was open within that tool use
|
||||
if (currentParamName) {
|
||||
// tool call has a parameter that was not completed
|
||||
// The remaining accumulator content belongs to this partial parameter
|
||||
currentToolUse.params[currentParamName] = accumulator.slice(currentParamValueStartIndex).trim()
|
||||
}
|
||||
// Add the potentially partial tool use block
|
||||
contentBlocks.push(currentToolUse)
|
||||
}
|
||||
|
||||
// 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)
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
return contentBlocks
|
||||
|
||||
@@ -8,45 +8,6 @@ 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)) {
|
||||
@@ -80,7 +41,8 @@ export const getLocalClineRules = async (cwd: string, toggles: ClineRulesToggles
|
||||
if (await fileExistsAtPath(clineRulesFilePath)) {
|
||||
if (await isDirectory(clineRulesFilePath)) {
|
||||
try {
|
||||
const rulesFilePaths = await readDirectory(clineRulesFilePath)
|
||||
const rulesFilePaths = await readDirectory(clineRulesFilePath, [[".clinerules", "workflows"]])
|
||||
|
||||
const rulesFilesTotalContent = await getRuleFilesTotalContent(rulesFilePaths, cwd, toggles)
|
||||
if (rulesFilesTotalContent) {
|
||||
clineRulesFileInstructions = formatResponse.clineRulesLocalDirectoryInstructions(cwd, rulesFilesTotalContent)
|
||||
@@ -121,7 +83,9 @@ 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)
|
||||
const updatedLocalToggles = await synchronizeRuleToggles(localClineRulesFilePath, localClineRulesToggles, "", [
|
||||
[".clinerules", "workflows"],
|
||||
])
|
||||
await updateWorkspaceState(context, "localClineRulesToggles", updatedLocalToggles)
|
||||
|
||||
return {
|
||||
@@ -129,82 +93,3 @@ 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,14 +1,21 @@
|
||||
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): Promise<string[]> {
|
||||
export async function readDirectoryRecursive(
|
||||
directoryPath: string,
|
||||
allowedFileExtension: string,
|
||||
excludedPaths: string[][] = [],
|
||||
): Promise<string[]> {
|
||||
try {
|
||||
const entries = await readDirectory(directoryPath)
|
||||
const entries = await readDirectory(directoryPath, excludedPaths)
|
||||
let results: string[] = []
|
||||
for (const entry of entries) {
|
||||
if (allowedFileExtension !== "") {
|
||||
@@ -33,6 +40,7 @@ export async function synchronizeRuleToggles(
|
||||
rulesDirectoryPath: string,
|
||||
currentToggles: ClineRulesToggles,
|
||||
allowedFileExtension: string = "",
|
||||
excludedPaths: string[][] = [],
|
||||
): Promise<ClineRulesToggles> {
|
||||
// Create a copy of toggles to modify
|
||||
const updatedToggles = { ...currentToggles }
|
||||
@@ -45,7 +53,7 @@ export async function synchronizeRuleToggles(
|
||||
|
||||
if (isDir) {
|
||||
// DIRECTORY CASE
|
||||
const filePaths = await readDirectoryRecursive(rulesDirectoryPath, allowedFileExtension)
|
||||
const filePaths = await readDirectoryRecursive(rulesDirectoryPath, allowedFileExtension, excludedPaths)
|
||||
const existingRulePaths = new Set<string>()
|
||||
|
||||
for (const filePath of filePaths) {
|
||||
@@ -119,3 +127,155 @@ 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.`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
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
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import * as vscode from "vscode"
|
||||
import crypto from "crypto"
|
||||
import { Controller } from "../index"
|
||||
import { storeSecret } from "../../storage/state"
|
||||
import { EmptyRequest, String } from "../../../shared/proto/common"
|
||||
|
||||
/**
|
||||
* Handles the user clicking the login link in the UI.
|
||||
@@ -11,7 +12,7 @@ import { storeSecret } from "../../storage/state"
|
||||
* @param controller The controller instance.
|
||||
* @returns The login URL as a string.
|
||||
*/
|
||||
export async function accountLoginClicked(controller: Controller): Promise<String> {
|
||||
export async function accountLoginClicked(controller: Controller, unused: EmptyRequest): Promise<String> {
|
||||
// Generate nonce for state validation
|
||||
const nonce = crypto.randomBytes(32).toString("hex")
|
||||
await storeSecret(controller.context, "authNonce", nonce)
|
||||
@@ -25,6 +26,8 @@ export async function accountLoginClicked(controller: Controller): Promise<Strin
|
||||
const authUrl = vscode.Uri.parse(
|
||||
`https://app.cline.bot/auth?state=${encodeURIComponent(nonce)}&callback_url=${encodeURIComponent(`${uriScheme || "vscode"}://saoudrizwan.claude-dev/auth`)}`,
|
||||
)
|
||||
vscode.env.openExternal(authUrl)
|
||||
return authUrl.toString()
|
||||
await vscode.env.openExternal(authUrl)
|
||||
return {
|
||||
value: authUrl.toString(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { Empty } from "../../../shared/proto/common"
|
||||
import type { EmptyRequest } from "../../../shared/proto/common"
|
||||
import type { Controller } from "../index"
|
||||
|
||||
/**
|
||||
* Handles the account logout action
|
||||
* @param controller The controller instance
|
||||
* @param _request The empty request object
|
||||
* @returns Empty response
|
||||
*/
|
||||
export async function accountLogoutClicked(controller: Controller, _request: EmptyRequest): Promise<Empty> {
|
||||
await controller.handleSignOut()
|
||||
return {}
|
||||
}
|
||||
@@ -4,9 +4,11 @@
|
||||
// Import all method implementations
|
||||
import { registerMethod } from "./index"
|
||||
import { accountLoginClicked } from "./accountLoginClicked"
|
||||
import { accountLogoutClicked } from "./accountLogoutClicked"
|
||||
|
||||
// Register all account service methods
|
||||
export function registerAllMethods(): void {
|
||||
// Register each method with the registry
|
||||
registerMethod("accountLoginClicked", accountLoginClicked)
|
||||
registerMethod("accountLogoutClicked", accountLogoutClicked)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { UpdateBrowserSettingsRequest } from "../../../shared/proto/browser"
|
||||
import { Boolean } from "../../../shared/proto/common"
|
||||
import { Controller } from "../index"
|
||||
import { updateGlobalState } from "../../storage/state"
|
||||
import { BrowserSettings as SharedBrowserSettings } from "../../../shared/BrowserSettings"
|
||||
import { updateGlobalState, getGlobalState } from "../../storage/state"
|
||||
import { BrowserSettings as SharedBrowserSettings, DEFAULT_BROWSER_SETTINGS } from "../../../shared/BrowserSettings"
|
||||
|
||||
/**
|
||||
* Update browser settings
|
||||
@@ -12,23 +12,39 @@ import { BrowserSettings as SharedBrowserSettings } from "../../../shared/Browse
|
||||
*/
|
||||
export async function updateBrowserSettings(controller: Controller, request: UpdateBrowserSettingsRequest): Promise<Boolean> {
|
||||
try {
|
||||
// Convert from protobuf format to shared format
|
||||
const browserSettings: SharedBrowserSettings = {
|
||||
// Get current browser settings to preserve fields not in the request
|
||||
const currentSettings = (await getGlobalState(controller.context, "browserSettings")) as SharedBrowserSettings | undefined
|
||||
const mergedWithDefaults = { ...DEFAULT_BROWSER_SETTINGS, ...currentSettings }
|
||||
|
||||
// Convert from protobuf format to shared format, merging with existing settings
|
||||
const newBrowserSettings: SharedBrowserSettings = {
|
||||
...mergedWithDefaults, // Start with existing settings (and defaults)
|
||||
viewport: {
|
||||
width: request.viewport?.width || 900,
|
||||
height: request.viewport?.height || 600,
|
||||
// Apply updates from request
|
||||
width: request.viewport?.width || mergedWithDefaults.viewport.width,
|
||||
height: request.viewport?.height || mergedWithDefaults.viewport.height,
|
||||
},
|
||||
remoteBrowserEnabled: request.remoteBrowserEnabled || false,
|
||||
remoteBrowserHost: request.remoteBrowserHost || undefined,
|
||||
// Explicitly handle optional boolean and string fields from the request
|
||||
remoteBrowserEnabled:
|
||||
request.remoteBrowserEnabled === undefined
|
||||
? mergedWithDefaults.remoteBrowserEnabled
|
||||
: request.remoteBrowserEnabled,
|
||||
remoteBrowserHost:
|
||||
request.remoteBrowserHost === undefined ? mergedWithDefaults.remoteBrowserHost : request.remoteBrowserHost,
|
||||
chromeExecutablePath:
|
||||
// If chromeExecutablePath is explicitly in the request (even as ""), use it.
|
||||
// Otherwise, fall back to mergedWithDefaults.
|
||||
"chromeExecutablePath" in request ? request.chromeExecutablePath : mergedWithDefaults.chromeExecutablePath,
|
||||
disableToolUse: request.disableToolUse === undefined ? mergedWithDefaults.disableToolUse : request.disableToolUse,
|
||||
}
|
||||
|
||||
// Update global state with new settings
|
||||
await updateGlobalState(controller.context, "browserSettings", browserSettings)
|
||||
await updateGlobalState(controller.context, "browserSettings", newBrowserSettings)
|
||||
|
||||
// Update task browser settings if task exists
|
||||
if (controller.task) {
|
||||
controller.task.browserSettings = browserSettings
|
||||
controller.task.browserSession.browserSettings = browserSettings
|
||||
controller.task.browserSettings = newBrowserSettings
|
||||
controller.task.browserSession.browserSettings = newBrowserSettings
|
||||
}
|
||||
|
||||
// Post updated state to webview
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import * as vscode from "vscode"
|
||||
import { Controller } from ".."
|
||||
import { Empty, StringRequest } from "../../../shared/proto/common"
|
||||
|
||||
/**
|
||||
* Copies text to the system clipboard
|
||||
* @param controller The controller instance
|
||||
* @param request The request containing the text to copy
|
||||
* @returns Empty response
|
||||
*/
|
||||
export async function copyToClipboard(controller: Controller, request: StringRequest): Promise<Empty> {
|
||||
try {
|
||||
if (request.value) {
|
||||
await vscode.env.clipboard.writeText(request.value)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error copying to clipboard:", error)
|
||||
}
|
||||
return Empty.create()
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
import { Controller } from ".."
|
||||
import { RuleFileRequest, RuleFile } from "@shared/proto/file"
|
||||
import { FileMethodHandler } from "./index"
|
||||
import {
|
||||
createRuleFile as createRuleFileImpl,
|
||||
refreshClineRulesToggles,
|
||||
} from "@core/context/instructions/user-instructions/cline-rules"
|
||||
import { refreshClineRulesToggles } from "@core/context/instructions/user-instructions/cline-rules"
|
||||
import { createRuleFile as createRuleFileImpl } from "@core/context/instructions/user-instructions/rule-helpers"
|
||||
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
|
||||
@@ -18,32 +17,45 @@ import { cwd } from "@core/task"
|
||||
* @throws Error if operation fails
|
||||
*/
|
||||
export const createRuleFile: FileMethodHandler = async (controller: Controller, request: RuleFileRequest): Promise<RuleFile> => {
|
||||
if (typeof request.isGlobal !== "boolean" || typeof request.filename !== "string" || !request.filename) {
|
||||
if (
|
||||
typeof request.isGlobal !== "boolean" ||
|
||||
!request.filename ||
|
||||
typeof request.filename !== "string" ||
|
||||
!request.type ||
|
||||
typeof request.type !== "string"
|
||||
) {
|
||||
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)
|
||||
const { filePath, fileExists } = await createRuleFileImpl(request.isGlobal, request.filename, cwd, request.type)
|
||||
|
||||
if (!filePath) {
|
||||
throw new Error("Failed to create rule file.")
|
||||
throw new Error("Failed to create file.")
|
||||
}
|
||||
|
||||
const fileTypeName = request.type === "workflow" ? "workflow" : "rule"
|
||||
|
||||
if (fileExists) {
|
||||
vscode.window.showWarningMessage(`Rule file "${request.filename}" already exists.`)
|
||||
vscode.window.showWarningMessage(`${fileTypeName} file "${request.filename}" already exists.`)
|
||||
// Still open it for editing
|
||||
await handleFileServiceRequest(controller, "openFile", { value: filePath })
|
||||
} else {
|
||||
await refreshClineRulesToggles(controller.context, cwd)
|
||||
if (request.type === "workflow") {
|
||||
await refreshWorkflowToggles(controller.context, cwd)
|
||||
} else {
|
||||
await refreshClineRulesToggles(controller.context, cwd)
|
||||
}
|
||||
await controller.postStateToWebview()
|
||||
|
||||
await handleFileServiceRequest(controller, "openFile", { value: filePath })
|
||||
|
||||
vscode.window.showInformationMessage(
|
||||
`Created new ${request.isGlobal ? "global" : "workspace"} rule file: ${request.filename}`,
|
||||
`Created new ${request.isGlobal ? "global" : "workspace"} ${fileTypeName} file: ${request.filename}`,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Controller } from ".."
|
||||
import { RuleFileRequest, RuleFile } from "@shared/proto/file"
|
||||
import { FileMethodHandler } from "./index"
|
||||
import {
|
||||
deleteRuleFile as deleteRuleFileImpl,
|
||||
refreshClineRulesToggles,
|
||||
} from "@core/context/instructions/user-instructions/cline-rules"
|
||||
import { refreshClineRulesToggles } from "@core/context/instructions/user-instructions/cline-rules"
|
||||
import { deleteRuleFile as deleteRuleFileImpl } from "@core/context/instructions/user-instructions/rule-helpers"
|
||||
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"
|
||||
@@ -18,26 +17,38 @@ 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) {
|
||||
if (
|
||||
typeof request.isGlobal !== "boolean" ||
|
||||
typeof request.rulePath !== "string" ||
|
||||
!request.rulePath ||
|
||||
!request.type ||
|
||||
typeof request.type !== "string"
|
||||
) {
|
||||
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)
|
||||
const result = await deleteRuleFileImpl(controller.context, request.rulePath, request.isGlobal, request.type)
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.message || "Failed to delete rule file")
|
||||
}
|
||||
|
||||
await refreshClineRulesToggles(controller.context, cwd)
|
||||
await refreshExternalRulesToggles(controller.context, cwd)
|
||||
// we refresh inside of the deleteRuleFileImpl(..) call
|
||||
//await refreshClineRulesToggles(controller.context, cwd)
|
||||
//await refreshExternalRulesToggles(controller.context, cwd)
|
||||
//await refreshWorkflowToggles(controller.context, cwd)
|
||||
await controller.postStateToWebview()
|
||||
|
||||
const fileName = path.basename(request.rulePath)
|
||||
vscode.window.showInformationMessage(`Rule file "${fileName}" deleted successfully`)
|
||||
|
||||
const fileTypeName = request.type === "workflow" ? "workflow" : "rule"
|
||||
|
||||
vscode.window.showInformationMessage(`${fileTypeName} file "${fileName}" deleted successfully`)
|
||||
|
||||
return RuleFile.create({
|
||||
filePath: request.rulePath,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
// Import all method implementations
|
||||
import { registerMethod } from "./index"
|
||||
import { copyToClipboard } from "./copyToClipboard"
|
||||
import { createRuleFile } from "./createRuleFile"
|
||||
import { deleteRuleFile } from "./deleteRuleFile"
|
||||
import { getRelativePaths } from "./getRelativePaths"
|
||||
@@ -10,10 +11,13 @@ import { openFile } from "./openFile"
|
||||
import { openImage } from "./openImage"
|
||||
import { searchCommits } from "./searchCommits"
|
||||
import { searchFiles } from "./searchFiles"
|
||||
import { selectImages } from "./selectImages"
|
||||
import { toggleClineRule } from "./toggleClineRule"
|
||||
|
||||
// Register all file service methods
|
||||
export function registerAllMethods(): void {
|
||||
// Register each method with the registry
|
||||
registerMethod("copyToClipboard", copyToClipboard)
|
||||
registerMethod("createRuleFile", createRuleFile)
|
||||
registerMethod("deleteRuleFile", deleteRuleFile)
|
||||
registerMethod("getRelativePaths", getRelativePaths)
|
||||
@@ -21,4 +25,6 @@ export function registerAllMethods(): void {
|
||||
registerMethod("openImage", openImage)
|
||||
registerMethod("searchCommits", searchCommits)
|
||||
registerMethod("searchFiles", searchFiles)
|
||||
registerMethod("selectImages", selectImages)
|
||||
registerMethod("toggleClineRule", toggleClineRule)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
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: [] })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { ToggleClineRuleRequest, ClineRulesToggles, ToggleClineRules } from "../../../shared/proto/file"
|
||||
import type { Controller } from "../index"
|
||||
import { getGlobalState, getWorkspaceState, updateGlobalState, updateWorkspaceState } from "../../../core/storage/state"
|
||||
import { ClineRulesToggles as AppClineRulesToggles } from "@shared/cline-rules"
|
||||
|
||||
/**
|
||||
* Toggles a Cline rule (enable or disable)
|
||||
* @param controller The controller instance
|
||||
* @param request The toggle request
|
||||
* @returns The updated Cline rule toggles
|
||||
*/
|
||||
export async function toggleClineRule(controller: Controller, request: ToggleClineRuleRequest): Promise<ToggleClineRules> {
|
||||
const { isGlobal, rulePath, enabled } = request
|
||||
|
||||
if (!rulePath || typeof enabled !== "boolean" || typeof isGlobal !== "boolean") {
|
||||
console.error("toggleClineRule: Missing or invalid parameters", {
|
||||
rulePath,
|
||||
isGlobal: typeof isGlobal === "boolean" ? isGlobal : `Invalid: ${typeof isGlobal}`,
|
||||
enabled: typeof enabled === "boolean" ? enabled : `Invalid: ${typeof enabled}`,
|
||||
})
|
||||
throw new Error("Missing or invalid parameters for toggleClineRule")
|
||||
}
|
||||
|
||||
// This is the same core logic as in the original handler
|
||||
if (isGlobal) {
|
||||
const toggles = ((await getGlobalState(controller.context, "globalClineRulesToggles")) as AppClineRulesToggles) || {}
|
||||
toggles[rulePath] = enabled
|
||||
await updateGlobalState(controller.context, "globalClineRulesToggles", toggles)
|
||||
} else {
|
||||
const toggles = ((await getWorkspaceState(controller.context, "localClineRulesToggles")) as AppClineRulesToggles) || {}
|
||||
toggles[rulePath] = enabled
|
||||
await updateWorkspaceState(controller.context, "localClineRulesToggles", toggles)
|
||||
}
|
||||
|
||||
// Get the current state to return in the response
|
||||
const globalToggles = ((await getGlobalState(controller.context, "globalClineRulesToggles")) as AppClineRulesToggles) || {}
|
||||
const localToggles = ((await getWorkspaceState(controller.context, "localClineRulesToggles")) as AppClineRulesToggles) || {}
|
||||
|
||||
return {
|
||||
globalClineRulesToggles: { toggles: globalToggles },
|
||||
localClineRulesToggles: { toggles: localToggles },
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ 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
|
||||
@@ -67,4 +68,8 @@ export const serviceHandlers: Record<string, ServiceHandlerConfig> = {
|
||||
requestHandler: handleModelsServiceRequest,
|
||||
streamingHandler: handleModelsServiceStreamingRequest,
|
||||
},
|
||||
"cline.SlashService": {
|
||||
requestHandler: handleSlashServiceRequest,
|
||||
streamingHandler: handleSlashServiceStreamingRequest,
|
||||
},
|
||||
}
|
||||
|
||||
+59
-316
@@ -14,7 +14,6 @@ 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"
|
||||
@@ -51,6 +50,7 @@ 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
|
||||
@@ -66,7 +66,7 @@ export class Controller {
|
||||
workspaceTracker: WorkspaceTracker
|
||||
mcpHub: McpHub
|
||||
accountService: ClineAccountService
|
||||
private latestAnnouncementId = "may-09-2025_17:11:00" // update to some unique identifier when we add a new announcement
|
||||
private latestAnnouncementId = "may-16-2025_16:11:00" // update to some unique identifier when we add a new announcement
|
||||
|
||||
constructor(
|
||||
readonly context: vscode.ExtensionContext,
|
||||
@@ -145,8 +145,19 @@ export class Controller {
|
||||
browserSettings,
|
||||
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,
|
||||
@@ -168,6 +179,7 @@ export class Controller {
|
||||
browserSettings,
|
||||
chatSettings,
|
||||
shellIntegrationTimeout,
|
||||
enableCheckpointsSetting ?? true,
|
||||
customInstructions,
|
||||
task,
|
||||
images,
|
||||
@@ -249,7 +261,7 @@ export class Controller {
|
||||
// If user already opted in to telemetry, enable telemetry service
|
||||
this.getStateToPostToWebview().then((state) => {
|
||||
const { telemetrySetting } = state
|
||||
const isOptedIn = telemetrySetting === "enabled"
|
||||
const isOptedIn = telemetrySetting !== "disabled"
|
||||
telemetryService.updateTelemetryState(isOptedIn)
|
||||
})
|
||||
break
|
||||
@@ -271,12 +283,6 @@ 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)
|
||||
@@ -300,11 +306,6 @@ export class Controller {
|
||||
}
|
||||
}
|
||||
break
|
||||
case "togglePlanActMode":
|
||||
if (message.chatSettings) {
|
||||
await this.togglePlanActModeWithChatSettings(message.chatSettings, message.chatContent)
|
||||
}
|
||||
break
|
||||
case "optionsResponse":
|
||||
await this.postMessageToWebview({
|
||||
type: "invoke",
|
||||
@@ -317,29 +318,14 @@ 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 "refreshRequestyModels":
|
||||
await this.refreshRequestyModels()
|
||||
break
|
||||
case "refreshClineRules":
|
||||
await refreshClineRulesToggles(this.context, cwd)
|
||||
await refreshExternalRulesToggles(this.context, cwd)
|
||||
await refreshWorkflowToggles(this.context, cwd)
|
||||
await this.postStateToWebview()
|
||||
break
|
||||
case "openInBrowser":
|
||||
@@ -347,22 +333,9 @@ export class Controller {
|
||||
vscode.env.openExternal(vscode.Uri.parse(message.url))
|
||||
}
|
||||
break
|
||||
case "fetchOpenGraphData":
|
||||
this.fetchOpenGraphData(message.text!)
|
||||
break
|
||||
case "openMention":
|
||||
openMention(message.text)
|
||||
break
|
||||
case "taskCompletionViewChanges": {
|
||||
if (message.number) {
|
||||
await this.task?.presentMultifileDiff(message.number, true)
|
||||
}
|
||||
break
|
||||
}
|
||||
case "accountLogoutClicked": {
|
||||
await this.handleSignOut()
|
||||
break
|
||||
}
|
||||
case "showAccountViewClicked": {
|
||||
await this.postMessageToWebview({ type: "action", action: "accountButtonClicked" })
|
||||
break
|
||||
@@ -382,28 +355,10 @@ 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
|
||||
}
|
||||
case "taskFeedback":
|
||||
if (message.feedbackType && this.task?.taskId) {
|
||||
telemetryService.captureTaskFeedback(this.task.taskId, message.feedbackType)
|
||||
}
|
||||
break
|
||||
// case "openMcpMarketplaceServerDetails": {
|
||||
// if (message.text) {
|
||||
// const response = await fetch(`https://api.cline.bot/v1/mcp/marketplace/item?mcpId=${message.mcpId}`)
|
||||
@@ -439,45 +394,6 @@ export class Controller {
|
||||
|
||||
// break
|
||||
// }
|
||||
case "toggleToolAutoApprove": {
|
||||
try {
|
||||
await this.mcpHub?.toggleToolAutoApprove(message.serverName!, message.toolNames!, message.autoApprove!)
|
||||
} catch (error) {
|
||||
if (message.toolNames?.length === 1) {
|
||||
console.error(
|
||||
`Failed to toggle auto-approve for server ${message.serverName} with tool ${message.toolNames[0]}:`,
|
||||
error,
|
||||
)
|
||||
} else {
|
||||
console.error(`Failed to toggle auto-approve tools for server ${message.serverName}:`, error)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case "toggleClineRule": {
|
||||
const { isGlobal, rulePath, enabled } = message
|
||||
if (rulePath && typeof enabled === "boolean" && typeof isGlobal === "boolean") {
|
||||
if (isGlobal) {
|
||||
const toggles =
|
||||
((await getGlobalState(this.context, "globalClineRulesToggles")) as ClineRulesToggles) || {}
|
||||
toggles[rulePath] = enabled
|
||||
await updateGlobalState(this.context, "globalClineRulesToggles", toggles)
|
||||
} else {
|
||||
const toggles =
|
||||
((await getWorkspaceState(this.context, "localClineRulesToggles")) as ClineRulesToggles) || {}
|
||||
toggles[rulePath] = enabled
|
||||
await updateWorkspaceState(this.context, "localClineRulesToggles", toggles)
|
||||
}
|
||||
await this.postStateToWebview()
|
||||
} else {
|
||||
console.error("toggleClineRule: Missing or invalid parameters", {
|
||||
rulePath,
|
||||
isGlobal: typeof isGlobal === "boolean" ? isGlobal : `Invalid: ${typeof isGlobal}`,
|
||||
enabled: typeof enabled === "boolean" ? enabled : `Invalid: ${typeof enabled}`,
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
case "toggleWindsurfRule": {
|
||||
const { rulePath, enabled } = message
|
||||
if (rulePath && typeof enabled === "boolean") {
|
||||
@@ -504,24 +420,21 @@ 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
|
||||
}
|
||||
case "restartMcpServer": {
|
||||
try {
|
||||
await this.mcpHub?.restartConnection(message.text!)
|
||||
} catch (error) {
|
||||
console.error(`Failed to retry connection for ${message.text}:`, error)
|
||||
}
|
||||
break
|
||||
}
|
||||
case "deleteMcpServer": {
|
||||
if (message.serverName) {
|
||||
this.mcpHub?.deleteServer(message.serverName)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case "fetchLatestMcpServersFromHub": {
|
||||
this.mcpHub?.sendLatestMcpServers()
|
||||
break
|
||||
@@ -585,6 +498,22 @@ export class Controller {
|
||||
// plan act setting
|
||||
await updateGlobalState(this.context, "planActSeparateModelsSetting", message.planActSeparateModelsSetting)
|
||||
|
||||
if (typeof message.enableCheckpointsSetting === "boolean") {
|
||||
await updateGlobalState(this.context, "enableCheckpointsSetting", message.enableCheckpointsSetting)
|
||||
}
|
||||
|
||||
if (typeof message.mcpMarketplaceEnabled === "boolean") {
|
||||
await updateGlobalState(this.context, "mcpMarketplaceEnabled", message.mcpMarketplaceEnabled)
|
||||
}
|
||||
|
||||
// chat settings (including preferredLanguage and openAIReasoningEffort)
|
||||
if (message.chatSettings) {
|
||||
await updateGlobalState(this.context, "chatSettings", message.chatSettings)
|
||||
if (this.task) {
|
||||
this.task.chatSettings = message.chatSettings
|
||||
}
|
||||
}
|
||||
|
||||
// after settings are updated, post state to webview
|
||||
await this.postStateToWebview()
|
||||
|
||||
@@ -612,27 +541,6 @@ 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)
|
||||
@@ -646,14 +554,6 @@ export class Controller {
|
||||
break
|
||||
}
|
||||
|
||||
case "copyToClipboard": {
|
||||
try {
|
||||
await vscode.env.clipboard.writeText(message.text || "")
|
||||
} catch (error) {
|
||||
console.error("Error copying to clipboard:", error)
|
||||
}
|
||||
break
|
||||
}
|
||||
case "updateTerminalConnectionTimeout": {
|
||||
if (message.shellIntegrationTimeout !== undefined) {
|
||||
const timeout = message.shellIntegrationTimeout
|
||||
@@ -676,7 +576,7 @@ export class Controller {
|
||||
|
||||
async updateTelemetrySetting(telemetrySetting: TelemetrySetting) {
|
||||
await updateGlobalState(this.context, "telemetrySetting", telemetrySetting)
|
||||
const isOptedIn = telemetrySetting === "enabled"
|
||||
const isOptedIn = telemetrySetting !== "disabled"
|
||||
telemetryService.updateTelemetryState(isOptedIn)
|
||||
}
|
||||
|
||||
@@ -756,6 +656,7 @@ export class Controller {
|
||||
break
|
||||
case "litellm":
|
||||
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.liteLlmModelId)
|
||||
await updateGlobalState(this.context, "previousModeModelInfo", apiConfiguration.liteLlmModelInfo)
|
||||
break
|
||||
case "requesty":
|
||||
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.requestyModelId)
|
||||
@@ -809,7 +710,8 @@ export class Controller {
|
||||
await updateGlobalState(this.context, "lmStudioModelId", newModelId)
|
||||
break
|
||||
case "litellm":
|
||||
await updateGlobalState(this.context, "liteLlmModelId", newModelId)
|
||||
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.liteLlmModelId)
|
||||
await updateGlobalState(this.context, "previousModeModelInfo", apiConfiguration.liteLlmModelInfo)
|
||||
break
|
||||
case "requesty":
|
||||
await updateGlobalState(this.context, "requestyModelId", newModelId)
|
||||
@@ -1028,92 +930,6 @@ 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) {
|
||||
@@ -1149,6 +965,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
|
||||
return cacheDir
|
||||
}
|
||||
|
||||
// Read OpenRouter models from disk cache
|
||||
async readOpenRouterModels(): Promise<Record<string, ModelInfo> | undefined> {
|
||||
const openRouterModelsFilePath = path.join(await this.ensureCacheDirectoryExists(), GlobalFileNames.openRouterModels)
|
||||
const fileExists = await fileExistsAtPath(openRouterModelsFilePath)
|
||||
@@ -1159,51 +976,6 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
|
||||
return undefined
|
||||
}
|
||||
|
||||
async refreshRequestyModels() {
|
||||
const parsePrice = (price: any) => {
|
||||
if (price) {
|
||||
return parseFloat(price) * 1_000_000
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
let models: Record<string, ModelInfo> = {}
|
||||
try {
|
||||
const apiKey = await getSecret(this.context, "requestyApiKey")
|
||||
const headers = {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
}
|
||||
const response = await axios.get("https://router.requesty.ai/v1/models", { headers })
|
||||
if (response.data?.data) {
|
||||
for (const model of response.data.data) {
|
||||
const modelInfo: ModelInfo = {
|
||||
maxTokens: model.max_output_tokens || undefined,
|
||||
contextWindow: model.context_window,
|
||||
supportsImages: model.supports_vision || undefined,
|
||||
supportsPromptCache: model.supports_caching || undefined,
|
||||
inputPrice: parsePrice(model.input_price),
|
||||
outputPrice: parsePrice(model.output_price),
|
||||
cacheWritesPrice: parsePrice(model.caching_price),
|
||||
cacheReadsPrice: parsePrice(model.cached_price),
|
||||
description: model.description,
|
||||
}
|
||||
models[model.id] = modelInfo
|
||||
}
|
||||
console.log("Requesty models fetched", models)
|
||||
} else {
|
||||
console.error("Invalid response from Requesty API")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching Requesty models:", error)
|
||||
}
|
||||
|
||||
await this.postMessageToWebview({
|
||||
type: "requestyModels",
|
||||
requestyModels: models,
|
||||
})
|
||||
return models
|
||||
}
|
||||
|
||||
// Context menus and code actions
|
||||
|
||||
getFileMentionFromPath(filePath: string) {
|
||||
@@ -1487,7 +1259,10 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
|
||||
|
||||
async postStateToWebview() {
|
||||
const state = await this.getStateToPostToWebview()
|
||||
await sendStateUpdate(state)
|
||||
// For testing: Bypass gRPC stream and send state directly
|
||||
console.log("[Controller Test Revert] Posting full state via direct 'state' message.")
|
||||
await this.postMessageToWebview({ type: "state", state: state })
|
||||
// await sendStateUpdate(state) // Original line for the GrPC stream
|
||||
}
|
||||
|
||||
async getStateToPostToWebview(): Promise<ExtensionState> {
|
||||
@@ -1503,8 +1278,10 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
|
||||
mcpMarketplaceEnabled,
|
||||
telemetrySetting,
|
||||
planActSeparateModelsSetting,
|
||||
enableCheckpointsSetting,
|
||||
globalClineRulesToggles,
|
||||
shellIntegrationTimeout,
|
||||
isNewUser,
|
||||
} = await getAllExtensionState(this.context)
|
||||
|
||||
const localClineRulesToggles =
|
||||
@@ -1516,6 +1293,8 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
|
||||
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,
|
||||
@@ -1537,12 +1316,15 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
|
||||
mcpMarketplaceEnabled,
|
||||
telemetrySetting,
|
||||
planActSeparateModelsSetting,
|
||||
enableCheckpointsSetting: enableCheckpointsSetting ?? true,
|
||||
vscMachineId: vscode.env.machineId,
|
||||
globalClineRulesToggles: globalClineRulesToggles || {},
|
||||
localClineRulesToggles: localClineRulesToggles || {},
|
||||
localWindsurfRulesToggles: localWindsurfRulesToggles || {},
|
||||
localCursorRulesToggles: localCursorRulesToggles || {},
|
||||
workflowToggles: workflowToggles || {},
|
||||
shellIntegrationTimeout,
|
||||
isNewUser,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1618,30 +1400,6 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
|
||||
|
||||
// secrets
|
||||
|
||||
// Open Graph Data
|
||||
|
||||
async fetchOpenGraphData(url: string) {
|
||||
try {
|
||||
// Use the fetchOpenGraphData function from link-preview.ts
|
||||
const ogData = await fetchOpenGraphData(url)
|
||||
|
||||
// Send the data back to the webview
|
||||
await this.postMessageToWebview({
|
||||
type: "openGraphData",
|
||||
openGraphData: ogData,
|
||||
url: url,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(`Error fetching Open Graph data for ${url}:`, error)
|
||||
// Send an error response
|
||||
await this.postMessageToWebview({
|
||||
type: "openGraphData",
|
||||
error: `Failed to fetch Open Graph data: ${error}`,
|
||||
url: url,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Git commit message generation
|
||||
|
||||
async generateGitCommitMessage() {
|
||||
@@ -1746,19 +1504,4 @@ Commit message:`
|
||||
}
|
||||
|
||||
// dev
|
||||
|
||||
async resetState() {
|
||||
vscode.window.showInformationMessage("Resetting state...")
|
||||
await resetExtensionState(this.context)
|
||||
if (this.task) {
|
||||
this.task.abortTask()
|
||||
this.task = undefined
|
||||
}
|
||||
vscode.window.showInformationMessage("State reset")
|
||||
await this.postStateToWebview()
|
||||
await this.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "chatButtonClicked",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { Controller } from "../index"
|
||||
import type { McpServers } from "../../../shared/proto/mcp"
|
||||
import { convertMcpServersToProtoMcpServers } from "../../../shared/proto-conversions/mcp/mcp-server-conversion"
|
||||
import { StringRequest } from "@/shared/proto/common"
|
||||
|
||||
/**
|
||||
* Deletes an MCP server
|
||||
* @param controller The controller instance
|
||||
* @param request The delete server request
|
||||
* @returns The list of remaining MCP servers after deletion
|
||||
*/
|
||||
export async function deleteMcpServer(controller: Controller, request: StringRequest): Promise<McpServers> {
|
||||
try {
|
||||
// Call the RPC variant to delete the server and get updated server list
|
||||
const mcpServers = (await controller.mcpHub?.deleteServerRPC(request.value)) || []
|
||||
|
||||
// Convert application types to protobuf types
|
||||
const protoServers = convertMcpServersToProtoMcpServers(mcpServers)
|
||||
|
||||
return { mcpServers: protoServers }
|
||||
} catch (error) {
|
||||
console.error(`Failed to delete MCP server: ${error}`)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -4,13 +4,21 @@
|
||||
// Import all method implementations
|
||||
import { registerMethod } from "./index"
|
||||
import { addRemoteMcpServer } from "./addRemoteMcpServer"
|
||||
import { deleteMcpServer } from "./deleteMcpServer"
|
||||
import { downloadMcp } from "./downloadMcp"
|
||||
import { restartMcpServer } from "./restartMcpServer"
|
||||
import { toggleMcpServer } from "./toggleMcpServer"
|
||||
import { toggleToolAutoApprove } from "./toggleToolAutoApprove"
|
||||
import { updateMcpTimeout } from "./updateMcpTimeout"
|
||||
|
||||
// Register all mcp service methods
|
||||
export function registerAllMethods(): void {
|
||||
// Register each method with the registry
|
||||
registerMethod("addRemoteMcpServer", addRemoteMcpServer)
|
||||
registerMethod("deleteMcpServer", deleteMcpServer)
|
||||
registerMethod("downloadMcp", downloadMcp)
|
||||
registerMethod("restartMcpServer", restartMcpServer)
|
||||
registerMethod("toggleMcpServer", toggleMcpServer)
|
||||
registerMethod("toggleToolAutoApprove", toggleToolAutoApprove)
|
||||
registerMethod("updateMcpTimeout", updateMcpTimeout)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { McpServers } from "@shared/proto/mcp"
|
||||
import type { Controller } from "../index"
|
||||
import { convertMcpServersToProtoMcpServers } from "@shared/proto-conversions/mcp/mcp-server-conversion"
|
||||
import { StringRequest } from "@/shared/proto/common"
|
||||
|
||||
/**
|
||||
* Restarts an MCP server connection
|
||||
* @param controller The controller instance
|
||||
* @param request The request containing the server name
|
||||
* @returns The updated list of MCP servers
|
||||
*/
|
||||
export async function restartMcpServer(controller: Controller, request: StringRequest): Promise<McpServers> {
|
||||
try {
|
||||
const mcpServers = await controller.mcpHub?.restartConnectionRPC(request.value)
|
||||
|
||||
// Convert from McpServer[] to ProtoMcpServer[] ensuring all required fields are set
|
||||
const protoServers = convertMcpServersToProtoMcpServers(mcpServers)
|
||||
|
||||
return { mcpServers: protoServers }
|
||||
} catch (error) {
|
||||
console.error(`Failed to restart MCP server ${request.value}:`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { ToggleToolAutoApproveRequest, McpServers } from "@shared/proto/mcp"
|
||||
import type { Controller } from "../index"
|
||||
import { convertMcpServersToProtoMcpServers } from "@shared/proto-conversions/mcp/mcp-server-conversion"
|
||||
|
||||
/**
|
||||
* Toggles auto-approve setting for MCP server tools
|
||||
* @param controller The controller instance
|
||||
* @param request The toggle tool auto-approve request
|
||||
* @returns Updated list of MCP servers
|
||||
*/
|
||||
export async function toggleToolAutoApprove(controller: Controller, request: ToggleToolAutoApproveRequest): Promise<McpServers> {
|
||||
try {
|
||||
// Call the RPC variant that returns the servers directly
|
||||
const mcpServers =
|
||||
(await controller.mcpHub?.toggleToolAutoApproveRPC(request.serverName, request.toolNames, request.autoApprove)) || []
|
||||
|
||||
// Convert application types to proto types
|
||||
return { mcpServers: convertMcpServersToProtoMcpServers(mcpServers) }
|
||||
} catch (error) {
|
||||
console.error(`Failed to toggle tool auto-approve for ${request.serverName}:`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { getOllamaModels } from "./getOllamaModels"
|
||||
import { getVsCodeLmModels } from "./getVsCodeLmModels"
|
||||
import { refreshOpenAiModels } from "./refreshOpenAiModels"
|
||||
import { refreshOpenRouterModels } from "./refreshOpenRouterModels"
|
||||
import { refreshRequestyModels } from "./refreshRequestyModels"
|
||||
|
||||
// Register all models service methods
|
||||
export function registerAllMethods(): void {
|
||||
@@ -17,4 +18,5 @@ export function registerAllMethods(): void {
|
||||
registerMethod("getVsCodeLmModels", getVsCodeLmModels)
|
||||
registerMethod("refreshOpenAiModels", refreshOpenAiModels)
|
||||
registerMethod("refreshOpenRouterModels", refreshOpenRouterModels)
|
||||
registerMethod("refreshRequestyModels", refreshRequestyModels)
|
||||
}
|
||||
|
||||
@@ -29,9 +29,6 @@ export async function refreshOpenAiModels(controller: Controller, request: OpenA
|
||||
const modelsArray = response.data?.data?.map((model: any) => model.id) || []
|
||||
const models = [...new Set<string>(modelsArray)]
|
||||
|
||||
// Send models to webview
|
||||
controller.postMessageToWebview({ type: "openAiModels", openAiModels: models })
|
||||
|
||||
return StringArray.create({ values: models })
|
||||
} catch (error) {
|
||||
console.error("Error fetching OpenAI models:", error)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Controller } from ".."
|
||||
import { EmptyRequest } from "../../../shared/proto/common"
|
||||
import { OpenRouterModels, OpenRouterModelInfo } from "../../../shared/proto/models"
|
||||
import { OpenRouterCompatibleModelInfo, OpenRouterModelInfo } from "../../../shared/proto/models"
|
||||
import axios from "axios"
|
||||
import path from "path"
|
||||
import fs from "fs/promises"
|
||||
@@ -13,7 +13,10 @@ import { GlobalFileNames } from "@core/storage/disk"
|
||||
* @param request Empty request object
|
||||
* @returns Response containing the OpenRouter models
|
||||
*/
|
||||
export async function refreshOpenRouterModels(controller: Controller, request: EmptyRequest): Promise<OpenRouterModels> {
|
||||
export async function refreshOpenRouterModels(
|
||||
controller: Controller,
|
||||
request: EmptyRequest,
|
||||
): Promise<OpenRouterCompatibleModelInfo> {
|
||||
const openRouterModelsFilePath = path.join(await ensureCacheDirectoryExists(controller), GlobalFileNames.openRouterModels)
|
||||
|
||||
let models: Record<string, Partial<OpenRouterModelInfo>> = {}
|
||||
@@ -141,13 +144,7 @@ export async function refreshOpenRouterModels(controller: Controller, request: E
|
||||
}
|
||||
}
|
||||
|
||||
// Send models to webview
|
||||
await controller.postMessageToWebview({
|
||||
type: "openRouterModels",
|
||||
openRouterModels: typedModels,
|
||||
})
|
||||
|
||||
return OpenRouterModels.create({ models: typedModels })
|
||||
return OpenRouterCompatibleModelInfo.create({ models: typedModels })
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Controller } from ".."
|
||||
import { EmptyRequest } from "../../../shared/proto/common"
|
||||
import { OpenRouterCompatibleModelInfo, OpenRouterModelInfo } from "../../../shared/proto/models"
|
||||
import axios from "axios"
|
||||
import { getSecret } from "@core/storage/state"
|
||||
|
||||
/**
|
||||
* Refreshes the Requesty models and returns the updated model list
|
||||
* @param controller The controller instance
|
||||
* @param request Empty request object
|
||||
* @returns Response containing the Requesty models
|
||||
*/
|
||||
export async function refreshRequestyModels(
|
||||
controller: Controller,
|
||||
request: EmptyRequest,
|
||||
): Promise<OpenRouterCompatibleModelInfo> {
|
||||
const parsePrice = (price: any) => {
|
||||
if (price) {
|
||||
return parseFloat(price) * 1_000_000
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
let models: Record<string, OpenRouterModelInfo> = {}
|
||||
try {
|
||||
const apiKey = await getSecret(controller.context, "requestyApiKey")
|
||||
const headers = {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
}
|
||||
const response = await axios.get("https://router.requesty.ai/v1/models", { headers })
|
||||
if (response.data?.data) {
|
||||
for (const model of response.data.data) {
|
||||
const modelInfo: OpenRouterModelInfo = {
|
||||
maxTokens: model.max_output_tokens || undefined,
|
||||
contextWindow: model.context_window,
|
||||
supportsImages: model.supports_vision || undefined,
|
||||
supportsPromptCache: model.supports_caching || undefined,
|
||||
inputPrice: parsePrice(model.input_price) || 0,
|
||||
outputPrice: parsePrice(model.output_price) || 0,
|
||||
cacheWritesPrice: parsePrice(model.caching_price) || 0,
|
||||
cacheReadsPrice: parsePrice(model.cached_price) || 0,
|
||||
description: model.description,
|
||||
}
|
||||
models[model.id] = modelInfo
|
||||
}
|
||||
console.log("Requesty models fetched", models)
|
||||
} else {
|
||||
console.error("Invalid response from Requesty API")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching Requesty models:", error)
|
||||
}
|
||||
|
||||
return OpenRouterCompatibleModelInfo.create({ models })
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
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()
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// 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()
|
||||
@@ -0,0 +1,14 @@
|
||||
// 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)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
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()
|
||||
}
|
||||
@@ -4,7 +4,10 @@
|
||||
// Import all method implementations
|
||||
import { registerMethod } from "./index"
|
||||
import { getLatestState } from "./getLatestState"
|
||||
import { resetState } from "./resetState"
|
||||
import { subscribeToState } from "./subscribeToState"
|
||||
import { toggleFavoriteModel } from "./toggleFavoriteModel"
|
||||
import { togglePlanActMode } from "./togglePlanActMode"
|
||||
|
||||
// Streaming methods for this service
|
||||
export const streamingMethods = ["subscribeToState"]
|
||||
@@ -13,5 +16,8 @@ export const streamingMethods = ["subscribeToState"]
|
||||
export function registerAllMethods(): void {
|
||||
// Register each method with the registry
|
||||
registerMethod("getLatestState", getLatestState)
|
||||
registerMethod("resetState", resetState)
|
||||
registerMethod("subscribeToState", subscribeToState, { isStreaming: true })
|
||||
registerMethod("toggleFavoriteModel", toggleFavoriteModel)
|
||||
registerMethod("togglePlanActMode", togglePlanActMode)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Controller } from ".."
|
||||
import { Empty, EmptyRequest } from "../../../shared/proto/common"
|
||||
import { resetExtensionState } from "../../../core/storage/state"
|
||||
import * as vscode from "vscode"
|
||||
|
||||
/**
|
||||
* Resets the extension state to its defaults
|
||||
* @param controller The controller instance
|
||||
* @param request An empty request (no parameters needed)
|
||||
* @returns An empty response
|
||||
*/
|
||||
export async function resetState(controller: Controller, request: EmptyRequest): Promise<Empty> {
|
||||
try {
|
||||
vscode.window.showInformationMessage("Resetting state...")
|
||||
await resetExtensionState(controller.context)
|
||||
|
||||
if (controller.task) {
|
||||
controller.task.abortTask()
|
||||
controller.task = undefined
|
||||
}
|
||||
|
||||
vscode.window.showInformationMessage("State reset")
|
||||
await controller.postStateToWebview()
|
||||
|
||||
await controller.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "chatButtonClicked",
|
||||
})
|
||||
|
||||
return Empty.create()
|
||||
} catch (error) {
|
||||
console.error("Error resetting state:", error)
|
||||
vscode.window.showErrorMessage(`Failed to reset state: ${error instanceof Error ? error.message : String(error)}`)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Controller } from ".."
|
||||
import { Empty } from "../../../shared/proto/common"
|
||||
import { TogglePlanActModeRequest } from "../../../shared/proto/state"
|
||||
import {
|
||||
convertProtoChatContentToChatContent,
|
||||
convertProtoChatSettingsToChatSettings,
|
||||
} from "@shared/proto-conversions/state/chat-settings-conversion"
|
||||
|
||||
/**
|
||||
* Toggles between Plan and Act modes
|
||||
* @param controller The controller instance
|
||||
* @param request The request containing the chat settings and optional chat content
|
||||
* @returns An empty response
|
||||
*/
|
||||
export async function togglePlanActMode(controller: Controller, request: TogglePlanActModeRequest): Promise<Empty> {
|
||||
try {
|
||||
if (!request.chatSettings) {
|
||||
throw new Error("Chat settings are required")
|
||||
}
|
||||
|
||||
const chatSettings = convertProtoChatSettingsToChatSettings(request.chatSettings)
|
||||
const chatContent = request.chatContent ? convertProtoChatContentToChatContent(request.chatContent) : undefined
|
||||
|
||||
// Call the existing controller implementation
|
||||
await controller.togglePlanActModeWithChatSettings(chatSettings, chatContent)
|
||||
|
||||
return Empty.create()
|
||||
} catch (error) {
|
||||
console.error("Failed to toggle Plan/Act mode:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
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
|
||||
@@ -10,22 +11,49 @@ import { getGlobalState } from "../../storage/state"
|
||||
*/
|
||||
export async function getTaskHistory(controller: Controller, request: GetTaskHistoryRequest): Promise<TaskHistoryArray> {
|
||||
try {
|
||||
const { favoritesOnly, searchQuery, sortBy } = request
|
||||
const { favoritesOnly, currentWorkspaceOnly, 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
|
||||
|
||||
// Apply favorites filter if requested
|
||||
if (favoritesOnly && hasRequiredFields) {
|
||||
return item.isFavorited === true
|
||||
if (!hasRequiredFields) {
|
||||
return false
|
||||
}
|
||||
|
||||
return hasRequiredFields
|
||||
// Apply favorites filter if requested
|
||||
if (favoritesOnly && !item.isFavorited) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 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
|
||||
})
|
||||
|
||||
// Apply search if provided
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
// Import all method implementations
|
||||
import { registerMethod } from "./index"
|
||||
import { askResponse } from "./askResponse"
|
||||
import { cancelTask } from "./cancelTask"
|
||||
import { clearTask } from "./clearTask"
|
||||
import { deleteNonFavoritedTasks } from "./deleteNonFavoritedTasks"
|
||||
@@ -11,11 +12,14 @@ import { exportTaskWithId } from "./exportTaskWithId"
|
||||
import { getTaskHistory } from "./getTaskHistory"
|
||||
import { newTask } from "./newTask"
|
||||
import { showTaskWithId } from "./showTaskWithId"
|
||||
import { taskCompletionViewChanges } from "./taskCompletionViewChanges"
|
||||
import { taskFeedback } from "./taskFeedback"
|
||||
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)
|
||||
@@ -24,5 +28,7 @@ export function registerAllMethods(): void {
|
||||
registerMethod("getTaskHistory", getTaskHistory)
|
||||
registerMethod("newTask", newTask)
|
||||
registerMethod("showTaskWithId", showTaskWithId)
|
||||
registerMethod("taskCompletionViewChanges", taskCompletionViewChanges)
|
||||
registerMethod("taskFeedback", taskFeedback)
|
||||
registerMethod("toggleTaskFavorite", toggleTaskFavorite)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Controller } from ".."
|
||||
import { Empty } from "../../../shared/proto/common"
|
||||
import { Int64Request } from "../../../shared/proto/common"
|
||||
|
||||
/**
|
||||
* Shows task completion changes in a diff view
|
||||
* @param controller The controller instance
|
||||
* @param request The request containing the timestamp of the message
|
||||
* @returns Empty response
|
||||
*/
|
||||
export async function taskCompletionViewChanges(controller: Controller, request: Int64Request): Promise<Empty> {
|
||||
try {
|
||||
if (request.value && controller.task) {
|
||||
await controller.task.presentMultifileDiff(request.value, true)
|
||||
}
|
||||
return Empty.create()
|
||||
} catch (error) {
|
||||
console.error("Error in taskCompletionViewChanges handler:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user