mirror of
https://github.com/cline/cline.git
synced 2026-09-11 05:47:07 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3423602151 | ||
|
|
66fb8835a4 | ||
|
|
214e157360 | ||
|
|
7a9dce4f86 | ||
|
|
af7e3a4d20 | ||
|
|
3abdc9ad0f | ||
|
|
28b15d8b9b | ||
|
|
bc6b3e54be | ||
|
|
9fad0aa4ae | ||
|
|
23fea0e16b | ||
|
|
e8aaa61494 | ||
|
|
d93080304c | ||
|
|
1ce5f72bc1 | ||
|
|
fb676add2e | ||
|
|
31cda0ce5e | ||
|
|
e173dad69c | ||
|
|
92f32522c5 |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Display account balance for all org members
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fixed issue with sap ai core client credentials storage
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix credit balance out of sync issue on account switching
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix Claude Code CLAUDE_CODE_MAX_OUTPUT_TOKENS
|
||||
@@ -24,7 +24,7 @@ body:
|
||||
2.
|
||||
3.
|
||||
validations:
|
||||
required: true
|
||||
required: false
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
@@ -54,7 +54,7 @@ body:
|
||||
description: What system information is relevant to the issue?
|
||||
placeholder: "e.g., CPU: Intel Core i7-11700K, GPU: NVIDIA GeForce RTX 3070, RAM: 32GB DDR4"
|
||||
validations:
|
||||
required: true
|
||||
required: false
|
||||
- type: input
|
||||
id: cline-version
|
||||
attributes:
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# Changelog
|
||||
|
||||
## [3.20.1]
|
||||
|
||||
- Fix for files being deleted when switching modes or closing tasks
|
||||
|
||||
## [3.20.0]
|
||||
|
||||
- Add account balance display for all organization members, allowing non-admin users to view their organization's credit balance and add credits
|
||||
|
||||
## [3.19.8]
|
||||
|
||||
- Add Claude Code support on Windows with improved system prompt handling to fix E2BIG errors (Thanks @BarreiroT!)
|
||||
|
||||
@@ -244,13 +244,60 @@ Recent macOS versions have stricter terminal permissions:
|
||||
|
||||
### Windows Issues
|
||||
|
||||
#### PowerShell Execution Policy
|
||||
If you're using Windows and still experiencing issues with shell integration after trying the previous steps, it's recommended you use Git Bash (or PowerShell).
|
||||
|
||||
If commands fail silently:
|
||||
### Git Bash
|
||||
|
||||
Git Bash is a terminal emulator that provides a Unix-like command line experience on Windows. To use Git Bash, you need to:
|
||||
|
||||
1. Download and run the Git for Windows installer from [https://git-scm.com/downloads/win](https://git-scm.com/downloads/win)
|
||||
2. Quit and re-open VSCode
|
||||
3. Press `Ctrl + Shift + P` to open the Command Palette
|
||||
4. Type "Terminal: Select Default Profile" and choose it
|
||||
5. Select "Git Bash"
|
||||
|
||||
### PowerShell
|
||||
|
||||
If you'd still like to use PowerShell, make sure you're using an updated version (at least v7+).
|
||||
- Check your current PowerShell version by running: `$PSVersionTable.PSVersion`
|
||||
- If your version is below 7, [update PowerShell](https://learn.microsoft.com/en-us/powershell/scripting/whats-new/migrating-from-windows-powershell-51-to-powershell-7?view=powershell-7.4#installing-powershell-7).
|
||||
|
||||
You may also need to adjust your PowerShell execution policy. By default, PowerShell restricts script execution for security reasons.
|
||||
|
||||
#### Understanding PowerShell Execution Policies
|
||||
|
||||
PowerShell uses execution policies to determine which scripts can run on your system. Here are the most common policies:
|
||||
|
||||
- `Restricted`: No PowerShell scripts can run. This is the default setting.
|
||||
- `AllSigned`: All scripts, including local ones, must be signed by a trusted publisher.
|
||||
- `RemoteSigned`: Scripts created locally can run, but scripts downloaded from the internet must be signed.
|
||||
- `Unrestricted`: No restrictions. Any script can run, though you will be warned before running internet-downloaded scripts.
|
||||
|
||||
For development work in VSCode, the `RemoteSigned` policy is generally recommended. It allows locally created scripts to run without restrictions while maintaining security for downloaded scripts. To learn more about PowerShell execution policies and understand the security implications of changing them, visit Microsoft's documentation: [About Execution Policies](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_execution_policies).
|
||||
|
||||
#### Steps to Change the Execution Policy
|
||||
|
||||
1. Open PowerShell as an Administrator: Press `Win + X` and select "Windows PowerShell (Administrator)" or "Windows Terminal (Administrator)".
|
||||
|
||||
2. Check Current Execution Policy by running this command:
|
||||
```powershell
|
||||
Get-ExecutionPolicy
|
||||
```
|
||||
- If the output is already `RemoteSigned`, `Unrestricted`, or `Bypass`, you likely don't need to change your execution policy. These policies should allow shell integration to work.
|
||||
- If the output is `Restricted` or `AllSigned`, you may need to change your policy to enable shell integration.
|
||||
|
||||
3. Change the Execution Policy by running the following command:
|
||||
```powershell
|
||||
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
|
||||
```
|
||||
- This sets the policy to `RemoteSigned` for the current user only, which is safer than changing it system-wide.
|
||||
|
||||
4. Confirm the Change by typing `Y` and pressing Enter when prompted.
|
||||
|
||||
5. Verify the Policy Change by running `Get-ExecutionPolicy` again to confirm the new setting.
|
||||
|
||||
6. Restart VSCode and try the shell integration again.
|
||||
|
||||
```powershell
|
||||
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
|
||||
```
|
||||
|
||||
#### WSL Integration
|
||||
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "claude-dev",
|
||||
"version": "3.19.8",
|
||||
"version": "3.20.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claude-dev",
|
||||
"version": "3.19.8",
|
||||
"version": "3.20.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.37.0",
|
||||
|
||||
+1
-1
@@ -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.19.8",
|
||||
"version": "3.20.1",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.84.0"
|
||||
|
||||
@@ -14,6 +14,7 @@ service DiffService {
|
||||
rpc getDocumentText(GetDocumentTextRequest) returns (GetDocumentTextResponse);
|
||||
// Replace a text selection in the diff.
|
||||
rpc replaceText(ReplaceTextRequest) returns (ReplaceTextResponse);
|
||||
rpc scrollDiff(ScrollDiffRequest) returns (ScrollDiffResponse);
|
||||
// Truncate the diff document.
|
||||
rpc truncateDocument(TruncateDocumentRequest) returns (TruncateDocumentResponse);
|
||||
// Save the diff document.
|
||||
@@ -54,10 +55,17 @@ message ReplaceTextRequest {
|
||||
|
||||
message ReplaceTextResponse {}
|
||||
|
||||
message ScrollDiffRequest {
|
||||
optional string diff_id = 1;
|
||||
optional int32 line = 2;
|
||||
}
|
||||
|
||||
message ScrollDiffResponse {}
|
||||
|
||||
message TruncateDocumentRequest {
|
||||
optional cline.Metadata metadata = 1;
|
||||
optional string diff_id = 2;
|
||||
optional int32 end_line = 5;
|
||||
optional int32 end_line = 3;
|
||||
}
|
||||
|
||||
message TruncateDocumentResponse {}
|
||||
|
||||
@@ -14,6 +14,8 @@ service WindowService {
|
||||
rpc showMessage(ShowMessageRequest) returns (SelectedResponse);
|
||||
rpc showInputBox(ShowInputBoxRequest) returns (ShowInputBoxResponse);
|
||||
rpc showSaveDialog(ShowSaveDialogRequest) returns (ShowSaveDialogResponse);
|
||||
rpc getOpenTabs(GetOpenTabsRequest) returns (GetOpenTabsResponse);
|
||||
rpc getVisibleTabs(GetVisibleTabsRequest) returns (GetVisibleTabsResponse);
|
||||
}
|
||||
|
||||
message ShowTextDocumentRequest {
|
||||
@@ -101,4 +103,20 @@ message ShowInputBoxRequest {
|
||||
|
||||
message ShowInputBoxResponse {
|
||||
optional string response = 1;
|
||||
}
|
||||
|
||||
message GetOpenTabsRequest {
|
||||
// empty
|
||||
}
|
||||
|
||||
message GetOpenTabsResponse {
|
||||
repeated string paths = 1;
|
||||
}
|
||||
|
||||
message GetVisibleTabsRequest {
|
||||
// empty
|
||||
}
|
||||
|
||||
message GetVisibleTabsResponse {
|
||||
repeated string paths = 1;
|
||||
}
|
||||
@@ -22,6 +22,11 @@ export async function getOrganizationCredits(
|
||||
controller.accountService.fetchOrganizationUsageTransactionsRPC(request.organizationId),
|
||||
])
|
||||
|
||||
// If balance call fails (returns undefined), throw an error
|
||||
if (!balanceData) {
|
||||
throw new Error("Failed to fetch organization credits data")
|
||||
}
|
||||
|
||||
return OrganizationCreditsData.create({
|
||||
balance: balanceData ? { currentBalance: balanceData.balance / 100 } : { currentBalance: 0 },
|
||||
organizationId: balanceData?.organizationId || "",
|
||||
|
||||
@@ -21,6 +21,11 @@ export async function getUserCredits(controller: Controller, request: EmptyReque
|
||||
controller.accountService.fetchPaymentTransactionsRPC(),
|
||||
])
|
||||
|
||||
// If either call fails (returns undefined), throw an error
|
||||
if (balance === undefined) {
|
||||
throw new Error("Failed to fetch user credits data")
|
||||
}
|
||||
|
||||
return UserCreditsData.create({
|
||||
balance: balance ? { currentBalance: balance.balance / 100 } : { currentBalance: 0 },
|
||||
usageTransactions: usageTransactions,
|
||||
|
||||
@@ -46,6 +46,7 @@ export async function refreshHuggingFaceModels(
|
||||
|
||||
// Transform HF models to OpenRouter-compatible format
|
||||
for (const rawModel of rawModels) {
|
||||
const providersList = rawModel.providers?.map((provider: { provider: string }) => provider.provider)?.join(", ")
|
||||
const modelInfo = OpenRouterModelInfo.create({
|
||||
maxTokens: 8192, // HF doesn't provide max_tokens, use default
|
||||
contextWindow: 128_000, // FIXME: HF doesn't provide context window, use default
|
||||
@@ -55,7 +56,7 @@ export async function refreshHuggingFaceModels(
|
||||
outputPrice: 0, // Will be set based on providers
|
||||
cacheWritesPrice: 0,
|
||||
cacheReadsPrice: 0,
|
||||
description: `Available on providers: ${rawModel.providers?.join(", ") || "unknown"}`,
|
||||
description: `Available on providers: ${providersList || "unknown"}`,
|
||||
})
|
||||
|
||||
// Add model-specific configurations if we have them in our static models
|
||||
|
||||
@@ -86,6 +86,7 @@ import { MessageStateHandler } from "./message-state"
|
||||
import { TaskState } from "./TaskState"
|
||||
import { ToolExecutor } from "./ToolExecutor"
|
||||
import { updateApiReqMsg } from "./utils"
|
||||
|
||||
export const USE_EXPERIMENTAL_CLAUDE4_FEATURES = false
|
||||
|
||||
export type ToolResponse = string | Array<Anthropic.TextBlockParam | Anthropic.ImageBlockParam>
|
||||
@@ -2525,10 +2526,9 @@ export class Task {
|
||||
|
||||
// It could be useful for cline to know if the user went from one or no file to another between messages, so we always include this context
|
||||
details += "\n\n# VSCode Visible Files"
|
||||
const visibleFilePaths = vscode.window.visibleTextEditors
|
||||
?.map((editor) => editor.document?.uri?.fsPath)
|
||||
.filter(Boolean)
|
||||
.map((absolutePath) => path.relative(this.cwd, absolutePath))
|
||||
const visibleFilePaths = (await HostProvider.window.getVisibleTabs({})).paths.map((absolutePath) =>
|
||||
path.relative(this.cwd, absolutePath),
|
||||
)
|
||||
|
||||
// Filter paths through clineIgnoreController
|
||||
const allowedVisibleFiles = this.clineIgnoreController
|
||||
@@ -2543,11 +2543,9 @@ export class Task {
|
||||
}
|
||||
|
||||
details += "\n\n# VSCode Open Tabs"
|
||||
const openTabPaths = vscode.window.tabGroups.all
|
||||
.flatMap((group) => group.tabs)
|
||||
.map((tab) => (tab.input as vscode.TabInputText)?.uri?.fsPath)
|
||||
.filter(Boolean)
|
||||
.map((absolutePath) => path.relative(this.cwd, absolutePath))
|
||||
const openTabPaths = (await HostProvider.window.getOpenTabs({})).paths.map((absolutePath) =>
|
||||
path.relative(this.cwd, absolutePath),
|
||||
)
|
||||
|
||||
// Filter paths through clineIgnoreController
|
||||
const allowedOpenTabs = this.clineIgnoreController
|
||||
|
||||
+9
-6
@@ -102,16 +102,19 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
try {
|
||||
if (!previousVersion || currentVersion !== previousVersion) {
|
||||
Logger.log(`Cline version changed: ${previousVersion} -> ${currentVersion}. First run or update detected.`)
|
||||
const lastShownPopupNotificationVersion = context.globalState.get<string>("clineLastPopupNotificationVersion")
|
||||
|
||||
if (currentVersion !== lastShownPopupNotificationVersion && previousVersion) {
|
||||
// Show VS Code popup notification as this version hasn't been notified yet without doing it for fresh installs
|
||||
const message = `Cline has been updated to v${currentVersion}`
|
||||
// Use the same condition as announcements: focus when there's a new announcement to show
|
||||
const lastShownAnnouncementId = context.globalState.get<string>("lastShownAnnouncementId")
|
||||
const latestAnnouncementId = context.extension?.packageJSON?.version?.split(".").slice(0, 2).join(".") ?? ""
|
||||
|
||||
if (lastShownAnnouncementId !== latestAnnouncementId) {
|
||||
// Focus Cline when there's a new announcement to show (major/minor updates or fresh installs)
|
||||
const message = previousVersion
|
||||
? `Cline has been updated to v${currentVersion}`
|
||||
: `Welcome to Cline v${currentVersion}`
|
||||
await vscode.commands.executeCommand("claude-dev.SidebarProvider.focus")
|
||||
await new Promise((resolve) => setTimeout(resolve, 200))
|
||||
HostProvider.window.showMessage({ type: ShowMessageType.INFORMATION, message })
|
||||
// Record that we've shown the popup for this version.
|
||||
await context.globalState.update("clineLastPopupNotificationVersion", currentVersion)
|
||||
}
|
||||
// Always update the main version tracker for the next launch.
|
||||
await context.globalState.update("clineVersion", currentVersion)
|
||||
|
||||
+6
-1
@@ -61,7 +61,12 @@ export class ExternalDiffViewProvider extends DiffViewProvider {
|
||||
}
|
||||
}
|
||||
|
||||
protected override async scrollEditorToLine(_line: number): Promise<void> {}
|
||||
protected override async scrollEditorToLine(line: number): Promise<void> {
|
||||
if (!this.activeDiffEditorId) {
|
||||
return
|
||||
}
|
||||
await HostProvider.diff.scrollDiff({ diffId: this.activeDiffEditorId, line: line })
|
||||
}
|
||||
|
||||
override async scrollAnimation(_startLine: number, _endLine: number): Promise<void> {}
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { ScrollDiffRequest, ScrollDiffResponse } from "@/shared/proto/index.host"
|
||||
|
||||
export async function scrollDiff(_request: ScrollDiffRequest): Promise<ScrollDiffResponse> {
|
||||
throw new Error("diffService is not supported. Use the VscodeDiffViewProvider.")
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/* eslint-disable eslint-rules/no-direct-vscode-api */
|
||||
import { describe, it, beforeEach, afterEach } from "mocha"
|
||||
import { strict as assert } from "assert"
|
||||
import * as vscode from "vscode"
|
||||
import { getOpenTabs } from "@/hosts/vscode/hostbridge/window/getOpenTabs"
|
||||
import { GetOpenTabsRequest } from "@/shared/proto/host/window"
|
||||
|
||||
describe("Hostbridge - Window - getOpenTabs", () => {
|
||||
async function createAndOpenTestDocument(fileNumber: number, column: vscode.ViewColumn): Promise<void> {
|
||||
const content = `// Test file ${fileNumber}\nconsole.log('Hello from file ${fileNumber}');`
|
||||
|
||||
// Create an untitled document with a custom name
|
||||
const uri = vscode.Uri.parse(`untitled:test-file-${fileNumber}.js`)
|
||||
|
||||
const doc = await vscode.workspace.openTextDocument(uri)
|
||||
|
||||
// Set the content
|
||||
const edit = new vscode.WorkspaceEdit()
|
||||
edit.insert(uri, new vscode.Position(0, 0), content)
|
||||
await vscode.workspace.applyEdit(edit)
|
||||
|
||||
await vscode.window.showTextDocument(doc, {
|
||||
viewColumn: column,
|
||||
preview: false,
|
||||
})
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
// Clean up any existing editors
|
||||
await vscode.commands.executeCommand("workbench.action.closeAllEditors")
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
// Clean up test documents and editors
|
||||
await vscode.commands.executeCommand("workbench.action.closeAllEditors")
|
||||
})
|
||||
|
||||
it("should return empty array when no tabs are open", async () => {
|
||||
// Ensure no tabs are open
|
||||
await vscode.commands.executeCommand("workbench.action.closeAllEditors")
|
||||
|
||||
const request = GetOpenTabsRequest.create({})
|
||||
const response = await getOpenTabs(request)
|
||||
|
||||
assert.strictEqual(
|
||||
response.paths.length,
|
||||
0,
|
||||
`Should return empty array when no tabs are open. Found: ${JSON.stringify(response.paths)}`,
|
||||
)
|
||||
})
|
||||
|
||||
it("should return paths of open text document tabs", async () => {
|
||||
// Open the documents in editors (this creates the tabs)
|
||||
await createAndOpenTestDocument(1, vscode.ViewColumn.One)
|
||||
await createAndOpenTestDocument(2, vscode.ViewColumn.Two)
|
||||
|
||||
// Wait a bit for tabs to be fully created
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
const request = GetOpenTabsRequest.create({})
|
||||
const response = await getOpenTabs(request)
|
||||
|
||||
// Should have 2 tabs open
|
||||
assert.strictEqual(
|
||||
response.paths.length,
|
||||
2,
|
||||
`Expected 2 tabs, got ${response.paths.length}. Found tabs: ${JSON.stringify(response.paths)}`,
|
||||
)
|
||||
})
|
||||
|
||||
it("should return all open tabs even when multiple files are opened in the same ViewColumn", async () => {
|
||||
// Open all documents in the same column (only the last one will be visible, but all are open as tabs)
|
||||
await createAndOpenTestDocument(1, vscode.ViewColumn.One)
|
||||
await createAndOpenTestDocument(2, vscode.ViewColumn.One)
|
||||
await createAndOpenTestDocument(3, vscode.ViewColumn.One)
|
||||
|
||||
// Wait a bit for tabs to be fully created
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
const request = GetOpenTabsRequest.create({})
|
||||
const response = await getOpenTabs(request)
|
||||
|
||||
// Should have all 3 tabs open, even though only 1 is visible
|
||||
assert.strictEqual(
|
||||
response.paths.length,
|
||||
3,
|
||||
`Expected 3 open tabs, got ${response.paths.length}. Found: ${JSON.stringify(response.paths)}`,
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,11 @@
|
||||
import { window, TabInputText } from "vscode"
|
||||
import { GetOpenTabsRequest, GetOpenTabsResponse } from "@/shared/proto/host/window"
|
||||
|
||||
export async function getOpenTabs(_: GetOpenTabsRequest): Promise<GetOpenTabsResponse> {
|
||||
const openTabPaths = window.tabGroups.all
|
||||
.flatMap((group) => group.tabs)
|
||||
.map((tab) => (tab.input as TabInputText)?.uri?.fsPath)
|
||||
.filter(Boolean)
|
||||
|
||||
return GetOpenTabsResponse.create({ paths: openTabPaths })
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
/* eslint-disable eslint-rules/no-direct-vscode-api */
|
||||
import { describe, it, beforeEach, afterEach } from "mocha"
|
||||
import { strict as assert } from "assert"
|
||||
import * as vscode from "vscode"
|
||||
import { getVisibleTabs } from "@/hosts/vscode/hostbridge/window/getVisibleTabs"
|
||||
import { GetVisibleTabsRequest } from "@/shared/proto/host/window"
|
||||
|
||||
describe("Hostbridge - Window - getVisibleTabs", () => {
|
||||
/**
|
||||
* Helper function to create and open a test document in a specific column
|
||||
*/
|
||||
async function createAndOpenTestDocument(fileNumber: number, column: vscode.ViewColumn): Promise<void> {
|
||||
const content = `// Test file ${fileNumber}\nconsole.log('Hello from file ${fileNumber}');`
|
||||
|
||||
// Create an untitled document with a custom name
|
||||
const uri = vscode.Uri.parse(`untitled:test-file-${fileNumber}.js`)
|
||||
|
||||
const doc = await vscode.workspace.openTextDocument(uri)
|
||||
|
||||
// Set the content
|
||||
const edit = new vscode.WorkspaceEdit()
|
||||
edit.insert(uri, new vscode.Position(0, 0), content)
|
||||
await vscode.workspace.applyEdit(edit)
|
||||
|
||||
await vscode.window.showTextDocument(doc, {
|
||||
viewColumn: column,
|
||||
preview: false,
|
||||
})
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
// Clean up any existing editors
|
||||
await vscode.commands.executeCommand("workbench.action.closeAllEditors")
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
// Clean up test documents and editors
|
||||
await vscode.commands.executeCommand("workbench.action.closeAllEditors")
|
||||
})
|
||||
|
||||
it("should return empty array when no visible editors are open", async () => {
|
||||
// Ensure no editors are open
|
||||
await vscode.commands.executeCommand("workbench.action.closeAllEditors")
|
||||
|
||||
const request = GetVisibleTabsRequest.create({})
|
||||
const response = await getVisibleTabs(request)
|
||||
|
||||
assert.strictEqual(
|
||||
response.paths.length,
|
||||
0,
|
||||
`Should return empty array when no visible editors are open. Found tabs: ${JSON.stringify(response.paths)}`,
|
||||
)
|
||||
})
|
||||
|
||||
it("should return paths of visible text editors", async () => {
|
||||
// Open the first document in an editor (this makes it visible)
|
||||
await createAndOpenTestDocument(1, vscode.ViewColumn.One)
|
||||
|
||||
// Wait a bit for editor to be fully created
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
const request = GetVisibleTabsRequest.create({})
|
||||
const response = await getVisibleTabs(request)
|
||||
|
||||
// Should have 1 visible editor
|
||||
assert.strictEqual(
|
||||
response.paths.length,
|
||||
1,
|
||||
`Expected 1 visible editor, got ${response.paths.length}. Found: ${JSON.stringify(response.paths)}`,
|
||||
)
|
||||
|
||||
// Open the second document in a different column (both should now be visible)
|
||||
await createAndOpenTestDocument(2, vscode.ViewColumn.Two)
|
||||
|
||||
// Wait a bit for editor to be fully created
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
const response2 = await getVisibleTabs(request)
|
||||
|
||||
// Should have 2 visible editors
|
||||
assert.strictEqual(
|
||||
response2.paths.length,
|
||||
2,
|
||||
`Expected 2 visible editors, got ${response2.paths.length}. Found: ${JSON.stringify(response2.paths)}`,
|
||||
)
|
||||
})
|
||||
|
||||
it("should only return visible editors, not all open tabs", async () => {
|
||||
// Open all documents in the same column (only the last one will be visible)
|
||||
await createAndOpenTestDocument(1, vscode.ViewColumn.One)
|
||||
await createAndOpenTestDocument(2, vscode.ViewColumn.One)
|
||||
await createAndOpenTestDocument(3, vscode.ViewColumn.One)
|
||||
|
||||
// Wait a bit for editors to be fully created
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
const request = GetVisibleTabsRequest.create({})
|
||||
const response = await getVisibleTabs(request)
|
||||
|
||||
// Should have only 1 visible editor (the last one opened in the same column)
|
||||
assert.strictEqual(
|
||||
response.paths.length,
|
||||
1,
|
||||
`Expected 1 visible editor, got ${response.paths.length}. Found: ${JSON.stringify(response.paths)}`,
|
||||
)
|
||||
|
||||
// Verify that we have the correct number of visible text editors
|
||||
const actualVisibleEditors = vscode.window.visibleTextEditors.length
|
||||
assert.strictEqual(
|
||||
response.paths.length,
|
||||
actualVisibleEditors,
|
||||
`Response should match actual visible editors count: ${actualVisibleEditors}`,
|
||||
)
|
||||
})
|
||||
|
||||
it("should return only visible editors from multiple columns with multiple files", async () => {
|
||||
// Open multiple documents in column one (only the last one will be visible in that column)
|
||||
await createAndOpenTestDocument(1, vscode.ViewColumn.One)
|
||||
await createAndOpenTestDocument(2, vscode.ViewColumn.One)
|
||||
await createAndOpenTestDocument(3, vscode.ViewColumn.One)
|
||||
|
||||
// Open multiple documents in column two (only the last one will be visible in that column)
|
||||
await createAndOpenTestDocument(4, vscode.ViewColumn.Two)
|
||||
await createAndOpenTestDocument(5, vscode.ViewColumn.Two)
|
||||
|
||||
// Wait a bit for editors to be fully created
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
const request = GetVisibleTabsRequest.create({})
|
||||
const response = await getVisibleTabs(request)
|
||||
|
||||
// Should have only 2 visible editors (one from each column, despite having 5 total open tabs)
|
||||
assert.strictEqual(
|
||||
response.paths.length,
|
||||
2,
|
||||
`Expected 2 visible editors, got ${response.paths.length}. Found: ${JSON.stringify(response.paths)}`,
|
||||
)
|
||||
|
||||
// Verify that we have the correct number of visible text editors
|
||||
const actualVisibleEditors = vscode.window.visibleTextEditors.length
|
||||
assert.strictEqual(
|
||||
response.paths.length,
|
||||
actualVisibleEditors,
|
||||
`Response should match actual visible editors count: ${actualVisibleEditors}`,
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,8 @@
|
||||
import { window, TabInputText } from "vscode"
|
||||
import { GetVisibleTabsRequest, GetVisibleTabsResponse } from "@/shared/proto/host/window"
|
||||
|
||||
export async function getVisibleTabs(_: GetVisibleTabsRequest): Promise<GetVisibleTabsResponse> {
|
||||
const visibleTabPaths = window.visibleTextEditors?.map((editor) => editor.document?.uri?.fsPath).filter(Boolean)
|
||||
|
||||
return GetVisibleTabsResponse.create({ paths: visibleTabPaths })
|
||||
}
|
||||
@@ -182,6 +182,9 @@ const CLAUDE_CODE_TIMEOUT = 600000 // 10 minutes
|
||||
// https://github.com/sindresorhus/execa/blob/main/docs/api.md#optionsmaxbuffer
|
||||
const BUFFER_SIZE = 20_000_000 // 20 MB
|
||||
|
||||
// This is the limit imposed by the CLI
|
||||
const CLAUDE_CODE_MAX_OUTPUT_TOKENS = "32000"
|
||||
|
||||
function runProcess(
|
||||
{ systemPrompt, messages, path, modelId, thinkingBudgetTokens, shouldUseFile }: ClaudeCodeOptions,
|
||||
cwd: string,
|
||||
@@ -210,8 +213,7 @@ function runProcess(
|
||||
const env: NodeJS.ProcessEnv = {
|
||||
...process.env,
|
||||
// Respect the user's environment variables but set defaults.
|
||||
// The default is 32000. However, I've gotten larger responses.
|
||||
CLAUDE_CODE_MAX_OUTPUT_TOKENS: process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS || "64000",
|
||||
CLAUDE_CODE_MAX_OUTPUT_TOKENS: process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS || CLAUDE_CODE_MAX_OUTPUT_TOKENS,
|
||||
// Disable telemetry, auto-updater and error reporting.
|
||||
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC || "1",
|
||||
DISABLE_NON_ESSENTIAL_MODEL_CALLS: process.env.DISABLE_NON_ESSENTIAL_MODEL_CALLS || "1",
|
||||
|
||||
@@ -295,7 +295,7 @@ export abstract class DiffViewProvider {
|
||||
}
|
||||
|
||||
async revertChanges(): Promise<void> {
|
||||
if (!this.absolutePath) {
|
||||
if (!this.absolutePath || !this.isEditing) {
|
||||
return
|
||||
}
|
||||
const fileExists = this.editType === "modify"
|
||||
|
||||
@@ -1156,6 +1156,22 @@ export type InternationalQwenModelId = keyof typeof internationalQwenModels
|
||||
export const internationalQwenDefaultModelId: InternationalQwenModelId = "qwen-coder-plus-latest"
|
||||
export const mainlandQwenDefaultModelId: MainlandQwenModelId = "qwen-coder-plus-latest"
|
||||
export const internationalQwenModels = {
|
||||
"qwen3-coder-plus": {
|
||||
maxTokens: 65_536,
|
||||
contextWindow: 1_000_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 1,
|
||||
outputPrice: 5,
|
||||
},
|
||||
"qwen3-coder-480b-a35b-instruct": {
|
||||
maxTokens: 65_536,
|
||||
contextWindow: 204_800,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 1.5,
|
||||
outputPrice: 7.5,
|
||||
},
|
||||
"qwen3-235b-a22b": {
|
||||
maxTokens: 16_384,
|
||||
contextWindow: 131_072,
|
||||
@@ -1970,6 +1986,14 @@ export const mistralModels = {
|
||||
inputPrice: 0.1,
|
||||
outputPrice: 0.3,
|
||||
},
|
||||
"devstral-medium-latest": {
|
||||
maxTokens: 128_000,
|
||||
contextWindow: 131_072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.4,
|
||||
outputPrice: 2.0,
|
||||
},
|
||||
} as const satisfies Record<string, ModelInfo>
|
||||
|
||||
// LiteLLM
|
||||
|
||||
@@ -7,6 +7,7 @@ const SHELL_PATHS = {
|
||||
POWERSHELL_LEGACY: "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
|
||||
CMD: "C:\\Windows\\System32\\cmd.exe",
|
||||
WSL_BASH: "/bin/bash",
|
||||
GIT_BASH: "C:\\Program Files\\Git\\bin\\bash.exe",
|
||||
// Unix paths
|
||||
MAC_DEFAULT: "/bin/zsh",
|
||||
LINUX_DEFAULT: "/bin/bash",
|
||||
@@ -221,6 +222,12 @@ export function getAvailableTerminalProfiles(): TerminalProfile[] {
|
||||
path: SHELL_PATHS.WSL_BASH,
|
||||
description: "Windows Subsystem for Linux Bash",
|
||||
},
|
||||
{
|
||||
id: "git-bash",
|
||||
name: "Git Bash",
|
||||
path: SHELL_PATHS.GIT_BASH,
|
||||
description: "Git Bash (bash.exe from Git for Windows)",
|
||||
},
|
||||
)
|
||||
} else if (process.platform === "darwin") {
|
||||
// macOS terminal profiles
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { useClineAuth } from "@/context/ClineAuthContext"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { AccountServiceClient } from "@/services/grpc-client"
|
||||
import { formatCreditsBalance } from "@/utils/format"
|
||||
import { UsageTransaction as ClineAccountUsageTransaction, PaymentTransaction } from "@shared/ClineAccount"
|
||||
import { UsageTransaction as ProtoUsageTransaction, UserOrganization, UserOrganizationUpdateRequest } from "@shared/proto/account"
|
||||
import { UsageTransaction as ProtoUsageTransaction, UserOrganization } from "@shared/proto/account"
|
||||
import { EmptyRequest } from "@shared/proto/common"
|
||||
import {
|
||||
VSCodeButton,
|
||||
@@ -13,71 +11,13 @@ import {
|
||||
VSCodeOption,
|
||||
VSCodeTag,
|
||||
} from "@vscode/webview-ui-toolkit/react"
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from "react"
|
||||
import ClineLogoWhite from "../../assets/ClineLogoWhite"
|
||||
import CreditsHistoryTable from "./CreditsHistoryTable"
|
||||
import { GetOrganizationCreditsRequest } from "@shared/proto/account"
|
||||
import debounce from "debounce"
|
||||
import deepEqual from "fast-deep-equal"
|
||||
import VSCodeButtonLink from "../common/VSCodeButtonLink"
|
||||
|
||||
// Custom hook for animated credit display with styled decimals
|
||||
const useAnimatedCredits = (targetValue: number, duration: number = 660) => {
|
||||
const [currentValue, setCurrentValue] = useState(0)
|
||||
const animationRef = useRef<number>()
|
||||
const startTimeRef = useRef<number>()
|
||||
|
||||
useEffect(() => {
|
||||
const animate = (timestamp: number) => {
|
||||
if (!startTimeRef.current) {
|
||||
startTimeRef.current = timestamp
|
||||
}
|
||||
|
||||
const elapsed = timestamp - startTimeRef.current
|
||||
const progress = Math.min(elapsed / duration, 1)
|
||||
|
||||
// Easing function (ease-out)
|
||||
const easedProgress = 1 - (1 - progress) ** 3
|
||||
const newValue = easedProgress * targetValue
|
||||
|
||||
setCurrentValue(newValue)
|
||||
|
||||
if (progress < 1) {
|
||||
animationRef.current = requestAnimationFrame(animate)
|
||||
}
|
||||
}
|
||||
|
||||
// Reset and start animation
|
||||
startTimeRef.current = undefined
|
||||
animationRef.current = requestAnimationFrame(animate)
|
||||
|
||||
return () => {
|
||||
if (animationRef.current) {
|
||||
cancelAnimationFrame(animationRef.current)
|
||||
}
|
||||
}
|
||||
}, [targetValue, duration])
|
||||
|
||||
return currentValue
|
||||
}
|
||||
|
||||
// Custom component to handle styled credit display
|
||||
const StyledCreditDisplay = ({ balance }: { balance: number }) => {
|
||||
const animatedValue = useAnimatedCredits(formatCreditsBalance(balance))
|
||||
const formatted = animatedValue.toFixed(4)
|
||||
const parts = formatted.split(".")
|
||||
const wholePart = parts[0]
|
||||
const decimalPart = parts[1] || "0000"
|
||||
const firstTwoDecimals = decimalPart.slice(0, 2)
|
||||
const lastTwoDecimals = decimalPart.slice(2)
|
||||
|
||||
return (
|
||||
<span className="font-azeret-mono font-light tabular-nums">
|
||||
{wholePart}.{firstTwoDecimals}
|
||||
<span className="text-[var(--vscode-descriptionForeground)]">{lastTwoDecimals}</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
import { StyledCreditDisplay } from "./StyledCreditDisplay"
|
||||
|
||||
type VSCodeDropdownChangeEvent = Event & {
|
||||
target: {
|
||||
@@ -118,15 +58,14 @@ const CLINE_APP_URL = "https://app.cline.bot"
|
||||
|
||||
export const ClineAccountView = () => {
|
||||
const { clineUser, handleSignIn, handleSignOut } = useClineAuth()
|
||||
const { userInfo, apiConfiguration } = useExtensionState()
|
||||
|
||||
const user = apiConfiguration?.clineAccountId ? clineUser || userInfo : undefined
|
||||
// Source of truth: Dedicated state for dropdown value that persists through failures
|
||||
// and represents that user's current selection.
|
||||
const [dropdownValue, setDropdownValue] = useState<string>("personal")
|
||||
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
||||
const [balance, setBalance] = useState<number | null>(null)
|
||||
const [userOrganizations, setUserOrganizations] = useState<UserOrganization[]>([])
|
||||
const [activeOrganization, setActiveOrganization] = useState<UserOrganization | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [isSwitchingProfile, setIsSwitchingProfile] = useState(false)
|
||||
const [usageData, setUsageData] = useState<ClineAccountUsageTransaction[]>([])
|
||||
const [paymentsData, setPaymentsData] = useState<PaymentTransaction[]>([])
|
||||
const [lastFetchTime, setLastFetchTime] = useState<number>(Date.now())
|
||||
@@ -134,7 +73,7 @@ export const ClineAccountView = () => {
|
||||
const clineUris = useMemo(() => {
|
||||
const base = new URL(clineUser?.appBaseUrl || CLINE_APP_URL)
|
||||
const dashboard = new URL("dashboard", base)
|
||||
const credits = new URL(activeOrganization ? "/organization" : "/account", dashboard)
|
||||
const credits = new URL(dropdownValue === "personal" ? "/account" : "/organization", dashboard)
|
||||
credits.searchParams.set("tab", "credits")
|
||||
credits.searchParams.set("redirect", "true")
|
||||
|
||||
@@ -142,171 +81,153 @@ export const ClineAccountView = () => {
|
||||
dashboard,
|
||||
credits,
|
||||
}
|
||||
}, [clineUser?.appBaseUrl, activeOrganization])
|
||||
}, [clineUser?.appBaseUrl, dropdownValue])
|
||||
|
||||
// Add a ref to track the intended organization during transitions
|
||||
const pendingOrganizationRef = useRef<string | null>(null)
|
||||
const [userOrganizations, setUserOrganizations] = useState<UserOrganization[]>([])
|
||||
const activeOrganization = useMemo(() => {
|
||||
return userOrganizations.find((org) => org.organizationId === dropdownValue)
|
||||
}, [userOrganizations, dropdownValue])
|
||||
|
||||
const getUserOrganizations = useCallback(async () => {
|
||||
try {
|
||||
if (!clineUser?.uid) {
|
||||
setBalance(null)
|
||||
setUserOrganizations([])
|
||||
setActiveOrganization(null)
|
||||
setIsSwitchingProfile(false)
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
const response = await AccountServiceClient.getUserOrganizations(EmptyRequest.create())
|
||||
if (response.organizations && !deepEqual(userOrganizations, response.organizations)) {
|
||||
setUserOrganizations(response.organizations)
|
||||
|
||||
// Only update activeOrganization if we're not in the middle of a switch
|
||||
// or if the server response matches our pending change
|
||||
const serverActiveOrg = response.organizations.find((org: UserOrganization) => org.active)
|
||||
const serverActiveOrgId = serverActiveOrg?.organizationId || ""
|
||||
|
||||
if (!isSwitchingProfile || pendingOrganizationRef.current === serverActiveOrgId) {
|
||||
if (serverActiveOrgId !== (activeOrganization?.organizationId || "")) {
|
||||
setActiveOrganization(serverActiveOrg || null)
|
||||
}
|
||||
// Clear pending ref if the server state matches
|
||||
if (pendingOrganizationRef.current === serverActiveOrgId) {
|
||||
pendingOrganizationRef.current = null
|
||||
}
|
||||
if (clineUser?.uid) {
|
||||
const response = await AccountServiceClient.getUserOrganizations(EmptyRequest.create())
|
||||
if (response?.organizations && !deepEqual(userOrganizations, response.organizations)) {
|
||||
setUserOrganizations(response.organizations)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch user organizations:", error)
|
||||
}
|
||||
}, [clineUser?.uid, userOrganizations, isSwitchingProfile, activeOrganization?.organizationId])
|
||||
}, [userOrganizations, clineUser?.uid, dropdownValue])
|
||||
|
||||
const fetchCreditBalance = useCallback(async () => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
const fetchCreditBalance = useCallback(
|
||||
async (orgId?: string) => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
const targetOrgId = orgId ?? dropdownValue
|
||||
const isPersonal = targetOrgId === "personal"
|
||||
|
||||
// Use the pending organization if we're switching, otherwise use current active org
|
||||
const targetOrgId =
|
||||
pendingOrganizationRef.current !== null ? pendingOrganizationRef.current : activeOrganization?.organizationId
|
||||
// Use targetOrgId consistently for all requests
|
||||
const response = isPersonal
|
||||
? await AccountServiceClient.getUserCredits(EmptyRequest.create())
|
||||
: await AccountServiceClient.getOrganizationCredits({ organizationId: targetOrgId })
|
||||
|
||||
const response = targetOrgId
|
||||
? await AccountServiceClient.getOrganizationCredits(
|
||||
GetOrganizationCreditsRequest.fromPartial({
|
||||
organizationId: targetOrgId,
|
||||
}),
|
||||
)
|
||||
: await AccountServiceClient.getUserCredits(EmptyRequest.create())
|
||||
// Update balance if changed
|
||||
const newBalance = response.balance?.currentBalance
|
||||
if (newBalance !== undefined && newBalance !== balance) {
|
||||
setBalance(newBalance)
|
||||
}
|
||||
if (response.usageTransactions && !deepEqual(usageData, response.usageTransactions)) {
|
||||
const clineUsage = convertProtoUsageTransactions(response.usageTransactions) || []
|
||||
setUsageData(clineUsage)
|
||||
}
|
||||
|
||||
// Update balance if changed
|
||||
const newBalance = response.balance?.currentBalance
|
||||
if (newBalance !== balance) {
|
||||
setBalance(newBalance ?? null)
|
||||
}
|
||||
// Organizations don't have payment transactions
|
||||
if (targetOrgId !== "personal") {
|
||||
setPaymentsData([])
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
const clineUsage = convertProtoUsageTransactions(response.usageTransactions)
|
||||
setUsageData(clineUsage || [])
|
||||
|
||||
if (activeOrganization?.organizationId) {
|
||||
setPaymentsData([]) // Organizations don't have payment transactions
|
||||
} else {
|
||||
// Check if response is UserCreditsData type
|
||||
if (typeof response !== "object" || !("paymentTransactions" in response)) {
|
||||
return
|
||||
}
|
||||
const paymentsData = response.paymentTransactions || []
|
||||
const newPaymentsData = response.paymentTransactions
|
||||
// Check if paymentTransactions is part of the response
|
||||
if (response.paymentTransactions?.length !== paymentsData?.length) {
|
||||
setPaymentsData(paymentsData)
|
||||
if (newPaymentsData?.length && !deepEqual(paymentsData, newPaymentsData)) {
|
||||
setPaymentsData(newPaymentsData)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch credit balance:", error)
|
||||
} finally {
|
||||
setLastFetchTime(Date.now())
|
||||
setIsLoading(false)
|
||||
}
|
||||
} finally {
|
||||
setLastFetchTime(Date.now())
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [activeOrganization?.organizationId])
|
||||
|
||||
const handleManualRefresh = useCallback(
|
||||
debounce(() => !isLoading && fetchCreditBalance(), 500, { immediate: true }),
|
||||
[fetchCreditBalance, isLoading],
|
||||
},
|
||||
[dropdownValue, balance],
|
||||
)
|
||||
|
||||
// Create a debounced version of fetchCreditBalance
|
||||
const debouncedFetchCreditBalance = useMemo(
|
||||
() => debounce(() => fetchCreditBalance(), 500, { immediate: true }),
|
||||
[fetchCreditBalance],
|
||||
)
|
||||
|
||||
const handleManualRefresh = useCallback(() => {
|
||||
if (!isLoading) {
|
||||
debouncedFetchCreditBalance()
|
||||
}
|
||||
}, [debouncedFetchCreditBalance, isLoading])
|
||||
|
||||
const handleOrganizationChange = useCallback(
|
||||
async (event: any) => {
|
||||
const newOrgId = (event.target as VSCodeDropdownChangeEvent["target"]).value
|
||||
const currentOrgId = activeOrganization?.organizationId || ""
|
||||
const newValue = (event.target as VSCodeDropdownChangeEvent["target"]).value || "personal"
|
||||
const organizationId = newValue === "personal" ? undefined : newValue
|
||||
|
||||
if (currentOrgId !== newOrgId) {
|
||||
setIsSwitchingProfile(true)
|
||||
if (newValue === dropdownValue) {
|
||||
return // No change, do nothing
|
||||
}
|
||||
|
||||
try {
|
||||
console.info("Changing selection to:", newValue)
|
||||
|
||||
// Send the change to the server
|
||||
AccountServiceClient.setUserOrganization({ organizationId })
|
||||
|
||||
// Update dropdownValue immediately - this persists through failures
|
||||
setDropdownValue(newValue)
|
||||
setIsLoading(true)
|
||||
setBalance(null)
|
||||
setUsageData([])
|
||||
setPaymentsData([])
|
||||
|
||||
// Set the pending organization immediately to prevent race conditions
|
||||
pendingOrganizationRef.current = newOrgId
|
||||
|
||||
try {
|
||||
// Update local state immediately for UI responsiveness
|
||||
if (newOrgId === "") {
|
||||
setActiveOrganization(null)
|
||||
} else {
|
||||
const org = userOrganizations.find((org: UserOrganization) => org.organizationId === newOrgId)
|
||||
if (org) {
|
||||
setActiveOrganization(org)
|
||||
}
|
||||
}
|
||||
|
||||
// Send the change to the server
|
||||
await AccountServiceClient.setUserOrganization(
|
||||
UserOrganizationUpdateRequest.create({ organizationId: newOrgId }),
|
||||
)
|
||||
|
||||
// Fetch fresh data for the new organization
|
||||
await fetchCreditBalance()
|
||||
|
||||
// Refresh organizations to get the updated active state from server
|
||||
await getUserOrganizations()
|
||||
} catch (error) {
|
||||
console.error("Failed to update organization:", error)
|
||||
// Reset pending ref on error
|
||||
pendingOrganizationRef.current = null
|
||||
} finally {
|
||||
setIsSwitchingProfile(false)
|
||||
}
|
||||
await fetchCreditBalance(organizationId)
|
||||
} catch (error) {
|
||||
console.error("Failed to update organization:", error)
|
||||
// Don't reset selectedOrgId on error - keep the user's selection
|
||||
// The next refresh will use the correct selectedOrgId
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
},
|
||||
[activeOrganization?.organizationId, fetchCreditBalance, getUserOrganizations, userOrganizations],
|
||||
[fetchCreditBalance, getUserOrganizations, dropdownValue],
|
||||
)
|
||||
|
||||
// Handle organization changes and initial load
|
||||
// Fetching initial data
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
await getUserOrganizations()
|
||||
await fetchCreditBalance()
|
||||
if (clineUser?.uid) {
|
||||
// Start with personal account as we do not have the user's organizations yet
|
||||
AccountServiceClient.setUserOrganization({ organizationId: undefined })
|
||||
await getUserOrganizations()
|
||||
await fetchCreditBalance()
|
||||
}
|
||||
}
|
||||
loadData()
|
||||
}, [activeOrganization?.organizationId])
|
||||
}, [clineUser?.uid])
|
||||
|
||||
// Periodic refresh
|
||||
useEffect(() => {
|
||||
const refreshData = async () => {
|
||||
try {
|
||||
if (clineUser?.uid) {
|
||||
await Promise.all([getUserOrganizations(), fetchCreditBalance()])
|
||||
await getUserOrganizations()
|
||||
await fetchCreditBalance()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error during periodic refresh:", error)
|
||||
}
|
||||
}
|
||||
|
||||
const intervalId = setInterval(refreshData, 30000)
|
||||
const intervalId = setInterval(refreshData, 60000)
|
||||
return () => clearInterval(intervalId)
|
||||
}, [clineUser?.uid, getUserOrganizations, fetchCreditBalance])
|
||||
|
||||
// Determine the current dropdown value, considering pending changes
|
||||
const dropdownValue =
|
||||
pendingOrganizationRef.current !== null ? pendingOrganizationRef.current : activeOrganization?.organizationId || ""
|
||||
}, [clineUser?.uid])
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
{user ? (
|
||||
{clineUser ? (
|
||||
<div className="flex flex-col pr-3 h-full">
|
||||
<div className="flex flex-col w-full">
|
||||
<div className="flex items-center mb-6 flex-wrap gap-y-4">
|
||||
@@ -314,37 +235,37 @@ export const ClineAccountView = () => {
|
||||
<img src={user.photoUrl} alt="Profile" className="size-16 rounded-full mr-4" />
|
||||
) : ( */}
|
||||
<div className="size-16 rounded-full bg-[var(--vscode-button-background)] flex items-center justify-center text-2xl text-[var(--vscode-button-foreground)] mr-4">
|
||||
{user.displayName?.[0] || user.email?.[0] || "?"}
|
||||
{clineUser.displayName?.[0] || clineUser.email?.[0] || "?"}
|
||||
</div>
|
||||
{/* )} */}
|
||||
|
||||
<div className="flex flex-col">
|
||||
{user.displayName && (
|
||||
{clineUser.displayName && (
|
||||
<h2 className="text-[var(--vscode-foreground)] m-0 text-lg font-medium">
|
||||
{user.displayName}
|
||||
{clineUser.displayName}
|
||||
</h2>
|
||||
)}
|
||||
|
||||
{user.email && (
|
||||
<div className="text-sm text-[var(--vscode-descriptionForeground)]">{user.email}</div>
|
||||
{clineUser.email && (
|
||||
<div className="text-sm text-[var(--vscode-descriptionForeground)]">{clineUser.email}</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2 items-center mt-1">
|
||||
{userOrganizations && (
|
||||
<VSCodeDropdown
|
||||
currentValue={dropdownValue}
|
||||
onChange={handleOrganizationChange}
|
||||
disabled={isSwitchingProfile}
|
||||
className="w-full">
|
||||
<VSCodeOption value="">Personal</VSCodeOption>
|
||||
{userOrganizations.map((org: UserOrganization) => (
|
||||
<VSCodeOption key={org.organizationId} value={org.organizationId}>
|
||||
{org.name}
|
||||
</VSCodeOption>
|
||||
))}
|
||||
</VSCodeDropdown>
|
||||
)}
|
||||
{activeOrganization?.roles && (
|
||||
<VSCodeDropdown
|
||||
currentValue={dropdownValue}
|
||||
onChange={handleOrganizationChange}
|
||||
disabled={isLoading}
|
||||
className="w-full">
|
||||
<VSCodeOption value="personal" key="personal">
|
||||
Personal
|
||||
</VSCodeOption>
|
||||
{userOrganizations?.map((org: UserOrganization) => (
|
||||
<VSCodeOption key={org.organizationId} value={org.organizationId}>
|
||||
{org.name}
|
||||
</VSCodeOption>
|
||||
))}
|
||||
</VSCodeDropdown>
|
||||
{activeOrganization && (
|
||||
<VSCodeTag className="text-xs p-2" title="Role">
|
||||
{getMainRole(activeOrganization.roles)}
|
||||
</VSCodeTag>
|
||||
@@ -376,7 +297,11 @@ export const ClineAccountView = () => {
|
||||
|
||||
<div className="text-4xl font-bold text-[var(--vscode-foreground)] mb-6 flex items-center gap-2">
|
||||
{balance === null ? <span>----</span> : <StyledCreditDisplay balance={balance} />}
|
||||
<VSCodeButton appearance="icon" className="mt-1" onClick={handleManualRefresh}>
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
className={`mt-1 ${isLoading ? "animate-spin" : ""}`}
|
||||
onClick={handleManualRefresh}
|
||||
disabled={isLoading}>
|
||||
<span className="codicon codicon-refresh"></span>
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
@@ -392,10 +317,10 @@ export const ClineAccountView = () => {
|
||||
|
||||
<div className="flex-grow flex flex-col min-h-0 pb-[0px]">
|
||||
<CreditsHistoryTable
|
||||
isLoading={isSwitchingProfile}
|
||||
isLoading={isLoading}
|
||||
usageData={usageData}
|
||||
paymentsData={paymentsData}
|
||||
showPayments={!activeOrganization?.active}
|
||||
showPayments={dropdownValue === "personal"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { formatCreditsBalance } from "@/utils/format"
|
||||
|
||||
// Custom hook for animated credit display with styled decimals
|
||||
const useAnimatedCredits = (targetValue: number, duration: number = 660) => {
|
||||
const [currentValue, setCurrentValue] = useState(0)
|
||||
const animationRef = useRef<number>()
|
||||
const startTimeRef = useRef<number>()
|
||||
|
||||
useEffect(() => {
|
||||
const animate = (timestamp: number) => {
|
||||
if (!startTimeRef.current) {
|
||||
startTimeRef.current = timestamp
|
||||
}
|
||||
|
||||
const elapsed = timestamp - startTimeRef.current
|
||||
const progress = Math.min(elapsed / duration, 1)
|
||||
|
||||
// Easing function (ease-out)
|
||||
const easedProgress = 1 - (1 - progress) ** 3
|
||||
const newValue = easedProgress * targetValue
|
||||
|
||||
setCurrentValue(newValue)
|
||||
|
||||
if (progress < 1) {
|
||||
animationRef.current = requestAnimationFrame(animate)
|
||||
}
|
||||
}
|
||||
|
||||
// Reset and start animation
|
||||
startTimeRef.current = undefined
|
||||
animationRef.current = requestAnimationFrame(animate)
|
||||
|
||||
return () => {
|
||||
if (animationRef.current) {
|
||||
cancelAnimationFrame(animationRef.current)
|
||||
}
|
||||
}
|
||||
}, [targetValue, duration])
|
||||
|
||||
return currentValue
|
||||
}
|
||||
|
||||
// Custom component to handle styled credit display
|
||||
export const StyledCreditDisplay = ({ balance }: { balance: number }) => {
|
||||
const animatedValue = useAnimatedCredits(formatCreditsBalance(balance))
|
||||
const formatted = animatedValue.toFixed(4)
|
||||
const parts = formatted.split(".")
|
||||
const wholePart = parts[0]
|
||||
const decimalPart = parts[1] || "0000"
|
||||
const firstTwoDecimals = decimalPart.slice(0, 2)
|
||||
const lastTwoDecimals = decimalPart.slice(2)
|
||||
|
||||
return (
|
||||
<span className="font-azeret-mono font-light tabular-nums">
|
||||
{wholePart}.{firstTwoDecimals}
|
||||
<span className="text-[var(--vscode-descriptionForeground)]">{lastTwoDecimals}</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -46,23 +46,23 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
|
||||
</h3>
|
||||
<ul style={ulStyle}>
|
||||
<li>
|
||||
<b>Optimized for Claude 4:</b> Cline is now optimized to work with the Claude 4 family of models, resulting in
|
||||
improved performance, reliability, and new capabilities.
|
||||
<b>Cerebras Provider Support:</b> Enhanced performance with updated model selection (Qwen and Llama 3.3 70B
|
||||
only) and increased context window for Qwen 3 32B from 16K to 64K tokens.
|
||||
</li>
|
||||
<li>
|
||||
<b>Gemini CLI Provider:</b> Added a new Gemini CLI provider that allows you to use your local Gemini CLI
|
||||
authentication to access Gemini models for free.
|
||||
<b>Claude Code for Windows:</b> Improved system prompt handling to fix E2BIG errors and better error messages
|
||||
with guidance for common setup issues.
|
||||
</li>
|
||||
<li>
|
||||
<b>WebFetch Tool:</b> Gemini 2.5 Pro and Claude 4 models now support the WebFetch tool, allowing Cline to
|
||||
retrieve and summarize web content directly in conversations.
|
||||
<b>Hugging Face Provider:</b> Added as a new API provider with support for their inference API models.
|
||||
</li>
|
||||
<li>
|
||||
<b>Self Knowledge:</b> When using frontier models, Cline is self-aware about his capabilities and featureset.
|
||||
<b>Moonshot Chinese Endpoints:</b> Added ability to choose Chinese endpoint for Moonshot provider and added
|
||||
Moonshot AI as a new provider.
|
||||
</li>
|
||||
<li>
|
||||
<b>Improved Diff Editing:</b> Improved diff editing to achieve record lows in diff edit failures for frontier
|
||||
models
|
||||
<b>Enhanced Stability:</b> Robust checkpoint timeout handling, fixed MCP servers starting when disabled, and
|
||||
improved authentication sync across multiple VSCode windows.
|
||||
</li>
|
||||
</ul>
|
||||
<Accordion isCompact className="pl-0">
|
||||
@@ -77,6 +77,26 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
|
||||
"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>Optimized for Claude 4:</b> Cline is now optimized to work with the Claude 4 family of models,
|
||||
resulting in improved performance, reliability, and new capabilities.
|
||||
</li>
|
||||
<li>
|
||||
<b>Gemini CLI Provider:</b> Added a new Gemini CLI provider that allows you to use your local Gemini
|
||||
CLI authentication to access Gemini models for free.
|
||||
</li>
|
||||
<li>
|
||||
<b>WebFetch Tool:</b> Gemini 2.5 Pro and Claude 4 models now support the WebFetch tool, allowing Cline
|
||||
to retrieve and summarize web content directly in conversations.
|
||||
</li>
|
||||
<li>
|
||||
<b>Self Knowledge:</b> When using frontier models, Cline is self-aware about his capabilities and
|
||||
featureset.
|
||||
</li>
|
||||
<li>
|
||||
<b>Improved Diff Editing:</b> Improved diff editing to achieve record lows in diff edit failures for
|
||||
frontier models.
|
||||
</li>
|
||||
<li>
|
||||
<b>Claude 4 Models:</b> Now with support for Anthropic Claude Sonnet 4 and Claude Opus 4 in both
|
||||
Anthropic and Vertex providers.
|
||||
|
||||
@@ -43,7 +43,7 @@ export const SapAiCoreProvider = ({ showModelOptions, isPopup, currentMode }: Sa
|
||||
)}
|
||||
|
||||
<DebouncedTextField
|
||||
initialValue={apiConfiguration?.sapAiCoreClientSecret ? "********" : ""}
|
||||
initialValue={apiConfiguration?.sapAiCoreClientSecret || ""}
|
||||
onChange={(value) => handleFieldChange("sapAiCoreClientSecret", value)}
|
||||
style={{ width: "100%" }}
|
||||
type="password"
|
||||
|
||||
Reference in New Issue
Block a user