Compare commits

..

7 Commits

Author SHA1 Message Date
abeatrix 85a7b7dbaf test: add comprehensive test suite for applyFileReadContextHistoryUpdates
Add extensive test coverage for the applyFileReadContextHistoryUpdates method in ContextManager. Tests cover various scenarios including:
- Early return when fileReadIndices is empty
- Handling single file occurrences
- Updating duplicate file reads (keeping only last occurrence)
- FILE_MENTION type with multiple files
- Text block replacements in API messages
- Edge cases and error conditions

This ensures the file read deduplication logic works correctly across different message types and file configurations.
2025-12-04 13:10:17 -08:00
Saoud Rizwan 852f307268 Revert "feat(prompt): add command output limiting guidance to capabilities (#…" (#7909)
This reverts commit 7a523fbaf6.
2025-12-04 11:38:10 -08:00
Bee 4e3fe004f4 feat: enable native tool calling for deepseek 3.2 [AI-27] (#7877)
* feat: enable native tool calling for deepseek 3.2

Add isDeepSeek32ModelFamily() function to identify DeepSeek 3.2 models and integrate it into the isNextGenModelFamily() check. This classifies DeepSeek 3.2 as a next-generation model family, enabling native tool calling support.

* typo
2025-12-04 09:59:27 -08:00
Zhongying Qiao 2b63eed85e feat: remove mcp enable setting for individual users (#7879) 2025-12-04 09:36:44 -08:00
Tomás Barreiro 2ffdc50ea1 Prevent simultaneous refreshes when restoring auth info (#7835)
* Prevent multiple simultaneos refreshes when retrieving auth info

* Add changeset

* refactor
2025-12-04 14:45:10 +01:00
celestial-vault 74808431e5 add litellm provider to remote config in the extension (#7775) 2025-12-04 03:01:05 -08:00
Saoud Rizwan 7a523fbaf6 feat(prompt): add command output limiting guidance to capabilities (#7884)
* feat(prompt): add command output limiting guidance to capabilities

Add guidance in the system prompt instructing the model to proactively
limit command output when anticipating large results. Includes examples
like piping to grep/head/tail or using more specific arguments.

Idea by @AraTheBoss

* chore: add changeset

* refactor: move command output limiting guidance to execute_command tool

Move the guidance from capabilities.ts to execute_command.ts where it
belongs. Extract into a shared COMMAND_BEST_PRACTICES constant to avoid
duplication across model variants (GENERIC, NATIVE_GPT_5, NATIVE_NEXT_GEN,
GEMINI_3).
2025-12-03 21:06:07 -08:00
13 changed files with 419 additions and 57 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Add DeepSeek 3.2 to native tool calling allow list
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Prevent simultaneuos refreshes when restoring auth info
-4
View File
@@ -1,9 +1,5 @@
# Changelog
## [3.40.1]
- Fix cost calculation display for Anthropic API requests
## [3.40.0]
- Fix highlighted text flashing when task header is collapsed
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "claude-dev",
"displayName": "Cline",
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
"version": "3.40.1",
"version": "3.40.0",
"icon": "assets/icons/icon.png",
"engines": {
"vscode": "^1.84.0"
@@ -197,4 +197,371 @@ describe("ContextManager", () => {
expect((content[0] as Anthropic.Messages.TextBlockParam).text).to.equal("Additional user text")
})
})
describe("applyFileReadContextHistoryUpdates", () => {
let contextManager: ContextManager
beforeEach(() => {
contextManager = new ContextManager()
})
it("should return early when fileReadIndices is empty", () => {
const fileReadIndices = new Map<string, [number, number, string, string, number][]>()
const messageFilePaths = new Map<number, string[]>()
const apiMessages: Anthropic.Messages.MessageParam[] = []
const timestamp = Date.now()
const [didUpdate, updatedIndices] = (contextManager as any).applyFileReadContextHistoryUpdates(
fileReadIndices,
messageFilePaths,
apiMessages,
timestamp,
)
expect(didUpdate).to.be.false
expect(updatedIndices.size).to.equal(0)
})
it("should not update when file has only one occurrence", () => {
const fileReadIndices = new Map<string, [number, number, string, string, number][]>()
fileReadIndices.set("test.ts", [[3, 2, "", "replacement text", 0]])
const messageFilePaths = new Map<number, string[]>()
const apiMessages: Anthropic.Messages.MessageParam[] = []
const timestamp = Date.now()
const [didUpdate, updatedIndices] = (contextManager as any).applyFileReadContextHistoryUpdates(
fileReadIndices,
messageFilePaths,
apiMessages,
timestamp,
)
expect(didUpdate).to.be.false
expect(updatedIndices.size).to.equal(0)
})
it("should update all but the last occurrence of duplicate file reads", () => {
const fileReadIndices = new Map<string, [number, number, string, string, number][]>()
// messageIndex, messageType (READ_FILE_TOOL=2), searchText, replaceText, innerIndex
fileReadIndices.set("test.ts", [
[3, 2, "", "[read_file for 'test.ts'] Result:\nDuplicate file read...", 0],
[5, 2, "", "[read_file for 'test.ts'] Result:\nDuplicate file read...", 0],
[7, 2, "", "[read_file for 'test.ts'] Result:\nKeep this one", 0],
])
const messageFilePaths = new Map<number, string[]>()
const apiMessages: Anthropic.Messages.MessageParam[] = []
const timestamp = Date.now()
const [didUpdate, updatedIndices] = (contextManager as any).applyFileReadContextHistoryUpdates(
fileReadIndices,
messageFilePaths,
apiMessages,
timestamp,
)
expect(didUpdate).to.be.true
expect(updatedIndices.size).to.equal(2)
expect(updatedIndices.has(3)).to.be.true
expect(updatedIndices.has(5)).to.be.true
expect(updatedIndices.has(7)).to.be.false // Last occurrence should not be updated
})
it("should handle FILE_MENTION type correctly with multiple files in same text", () => {
const fileReadIndices = new Map<string, [number, number, string, string, number][]>()
// FILE_MENTION = 4
fileReadIndices.set("file1.ts", [
[
3,
4,
'<file_content path="file1.ts">content1</file_content>',
'<file_content path="file1.ts">Duplicate file read...</file_content>',
0,
],
[
5,
4,
'<file_content path="file1.ts">content2</file_content>',
'<file_content path="file1.ts">Keep this</file_content>',
0,
],
])
fileReadIndices.set("file2.ts", [
[
3,
4,
'<file_content path="file2.ts">content3</file_content>',
'<file_content path="file2.ts">Duplicate file read...</file_content>',
0,
],
[
6,
4,
'<file_content path="file2.ts">content4</file_content>',
'<file_content path="file2.ts">Keep this</file_content>',
0,
],
])
const messageFilePaths = new Map<number, string[]>()
messageFilePaths.set(3, ["file1.ts", "file2.ts"])
const apiMessages: Anthropic.Messages.MessageParam[] = [
{ role: "user", content: "Initial" },
{ role: "assistant", content: "Response" },
{ role: "user", content: "Message" },
{
role: "user",
content: [
{
type: "text",
text: '<file_content path="file1.ts">content1</file_content>\n<file_content path="file2.ts">content3</file_content>',
},
],
},
]
const timestamp = Date.now()
const [didUpdate, updatedIndices] = (contextManager as any).applyFileReadContextHistoryUpdates(
fileReadIndices,
messageFilePaths,
apiMessages,
timestamp,
)
expect(didUpdate).to.be.true
expect(updatedIndices.size).to.equal(1)
expect(updatedIndices.has(3)).to.be.true
})
it("should handle ALTER_FILE_TOOL type correctly", () => {
const fileReadIndices = new Map<string, [number, number, string, string, number][]>()
// ALTER_FILE_TOOL = 3
fileReadIndices.set("test.ts", [
[3, 3, "", "replacement text 1", 0],
[5, 3, "", "replacement text 2", 0],
])
const messageFilePaths = new Map<number, string[]>()
const apiMessages: Anthropic.Messages.MessageParam[] = []
const timestamp = Date.now()
const [didUpdate, updatedIndices] = (contextManager as any).applyFileReadContextHistoryUpdates(
fileReadIndices,
messageFilePaths,
apiMessages,
timestamp,
)
expect(didUpdate).to.be.true
expect(updatedIndices.size).to.equal(1)
expect(updatedIndices.has(3)).to.be.true
expect(updatedIndices.has(5)).to.be.false
})
it("should handle native tool calling format (tool_result blocks)", () => {
const fileReadIndices = new Map<string, [number, number, string, string, number][]>()
fileReadIndices.set("test.ts", [
[3, 2, "", "[read_file for 'test.ts'] Result:\nDuplicate...", 0],
[5, 2, "", "[read_file for 'test.ts'] Result:\nKeep this", 0],
])
const messageFilePaths = new Map<number, string[]>()
const apiMessages: Anthropic.Messages.MessageParam[] = [
{ role: "user", content: "Initial" },
{ role: "assistant", content: "Response" },
{ role: "user", content: "Message" },
{
role: "user",
content: [
{
type: "tool_result",
tool_use_id: "tool_123",
content: [{ type: "text", text: "[read_file for 'test.ts'] Result:\noriginal content" }],
},
],
},
]
const timestamp = Date.now()
const [didUpdate, updatedIndices] = (contextManager as any).applyFileReadContextHistoryUpdates(
fileReadIndices,
messageFilePaths,
apiMessages,
timestamp,
)
expect(didUpdate).to.be.true
expect(updatedIndices.size).to.equal(1)
expect(updatedIndices.has(3)).to.be.true
})
})
describe("helper methods for applyFileReadContextHistoryUpdates", () => {
let contextManager: ContextManager
beforeEach(() => {
contextManager = new ContextManager()
})
it("getBaseTextForFileMention should get text from existing updates", () => {
const messageIndex = 3
const innerIndex = 0
const apiMessages: Anthropic.Messages.MessageParam[] = [
{ role: "user", content: "Initial" },
{ role: "assistant", content: "Response" },
{ role: "user", content: "Message" },
{ role: "user", content: [{ type: "text", text: "original text" }] },
]
// Manually set up context history updates
const timestamp = Date.now()
const innerMap = new Map<number, any[]>()
innerMap.set(innerIndex, [[timestamp, "text", ["updated text"], []]])
;(contextManager as any).contextHistoryUpdates.set(messageIndex, [4, innerMap])
const result = (contextManager as any).getBaseTextForFileMention(messageIndex, innerIndex, apiMessages)
expect(result).to.equal("updated text")
})
it("getBaseTextForFileMention should fallback to original message content", () => {
const messageIndex = 3
const innerIndex = 0
const apiMessages: Anthropic.Messages.MessageParam[] = [
{ role: "user", content: "Initial" },
{ role: "assistant", content: "Response" },
{ role: "user", content: "Message" },
{ role: "user", content: [{ type: "text", text: "original text" }] },
]
const result = (contextManager as any).getBaseTextForFileMention(messageIndex, innerIndex, apiMessages)
expect(result).to.equal("original text")
})
it("getBaseTextForFileMention should handle tool_result blocks", () => {
const messageIndex = 3
const innerIndex = 0
const apiMessages: Anthropic.Messages.MessageParam[] = [
{ role: "user", content: "Initial" },
{ role: "assistant", content: "Response" },
{ role: "user", content: "Message" },
{
role: "user",
content: [
{
type: "tool_result",
tool_use_id: "tool_123",
content: [{ type: "text", text: "tool result text" }],
},
],
},
]
const result = (contextManager as any).getBaseTextForFileMention(messageIndex, innerIndex, apiMessages)
expect(result).to.equal("tool result text")
})
it("getPreviouslyReplacedFiles should return empty array when no updates exist", () => {
const messageIndex = 3
const innerIndex = 0
const result = (contextManager as any).getPreviouslyReplacedFiles(messageIndex, innerIndex)
expect(result).to.deep.equal([])
})
it("getPreviouslyReplacedFiles should return previously replaced files", () => {
const messageIndex = 3
const innerIndex = 0
const timestamp = Date.now()
// Manually set up context history updates with metadata
const innerMap = new Map<number, any[]>()
innerMap.set(innerIndex, [
[
timestamp,
"text",
["updated text"],
[
["file1.ts", "file2.ts"],
["file1.ts", "file2.ts", "file3.ts"],
],
],
])
;(contextManager as any).contextHistoryUpdates.set(messageIndex, [4, innerMap])
const result = (contextManager as any).getPreviouslyReplacedFiles(messageIndex, innerIndex)
expect(result).to.deep.equal(["file1.ts", "file2.ts"])
})
it("addContextUpdate should create new entry when none exists", () => {
const messageIndex = 3
const messageType = 2 // READ_FILE_TOOL
const innerIndex = 0
const timestamp = Date.now()
const messageString = "replacement text"
;(contextManager as any).addContextUpdate(messageIndex, messageType, innerIndex, timestamp, messageString)
const contextHistory = (contextManager as any).contextHistoryUpdates
expect(contextHistory.has(messageIndex)).to.be.true
const [storedType, innerMap] = contextHistory.get(messageIndex)
expect(storedType).to.equal(messageType)
expect(innerMap.has(innerIndex)).to.be.true
const updates = innerMap.get(innerIndex)
expect(updates).to.have.lengthOf(1)
expect(updates[0]).to.deep.equal([timestamp, "text", [messageString], []])
})
it("addContextUpdate should append to existing updates", () => {
const messageIndex = 3
const messageType = 2
const innerIndex = 0
const timestamp1 = Date.now()
const timestamp2 = timestamp1 + 1000
;(contextManager as any).addContextUpdate(messageIndex, messageType, innerIndex, timestamp1, "first update")
;(contextManager as any).addContextUpdate(messageIndex, messageType, innerIndex, timestamp2, "second update")
const contextHistory = (contextManager as any).contextHistoryUpdates
const [, innerMap] = contextHistory.get(messageIndex)
const updates = innerMap.get(innerIndex)
expect(updates).to.have.lengthOf(2)
expect(updates[1]).to.deep.equal([timestamp2, "text", ["second update"], []])
})
it("getOrCreateInnerMap should return existing map", () => {
const messageIndex = 3
const messageType = 2
const innerMap = new Map<number, any[]>()
;(contextManager as any).contextHistoryUpdates.set(messageIndex, [messageType, innerMap])
const result = (contextManager as any).getOrCreateInnerMap(messageIndex, messageType)
expect(result).to.equal(innerMap)
})
it("getOrCreateInnerMap should create new map when none exists", () => {
const messageIndex = 3
const messageType = 2
const result = (contextManager as any).getOrCreateInnerMap(messageIndex, messageType)
expect(result).to.be.instanceOf(Map)
const contextHistory = (contextManager as any).contextHistoryUpdates
expect(contextHistory.has(messageIndex)).to.be.true
const [storedType, storedMap] = contextHistory.get(messageIndex)
expect(storedType).to.equal(messageType)
expect(storedMap).to.equal(result)
})
})
})
@@ -74,11 +74,6 @@ export async function updateSettings(controller: Controller, request: UpdateSett
controller.stateManager.setGlobalState("enableCheckpointsSetting", request.enableCheckpointsSetting)
}
// Update MCP marketplace setting
if (request.mcpMarketplaceEnabled !== undefined) {
controller.stateManager.setGlobalState("mcpMarketplaceEnabled", request.mcpMarketplaceEnabled)
}
// Update MCP responses collapsed setting
if (request.mcpResponsesCollapsed !== undefined) {
controller.stateManager.setGlobalState("mcpResponsesCollapsed", request.mcpResponsesCollapsed)
+12
View File
@@ -125,6 +125,18 @@ export function transformRemoteConfigToStateShape(remoteConfig: RemoteConfig): P
providers.push("cline")
}
// Map LiteLLM provider settings
const liteLlmSettings = remoteConfig.providerSettings?.LiteLLM
if (liteLlmSettings) {
transformed.planModeApiProvider = "litellm"
transformed.actModeApiProvider = "litellm"
providers.push("litellm")
if (liteLlmSettings.baseUrl !== undefined) {
transformed.liteLlmBaseUrl = liteLlmSettings.baseUrl
}
}
// This line needs to stay here, it is order dependent on the above code checking the configured providers
if (providers.length > 0) {
transformed.remoteConfiguredProviders = providers
+1 -7
View File
@@ -2716,13 +2716,7 @@ export class Task {
await this.postStateToWebview()
try {
const taskMetrics: {
cacheWriteTokens: number
cacheReadTokens: number
inputTokens: number
outputTokens: number
totalCost: number | undefined
} = { cacheWriteTokens: 0, cacheReadTokens: 0, inputTokens: 0, outputTokens: 0, totalCost: undefined }
const taskMetrics = { cacheWriteTokens: 0, cacheReadTokens: 0, inputTokens: 0, outputTokens: 0, totalCost: 0 }
const abortStream = async (cancelReason: ClineApiReqCancelReason, streamingFailedMessage?: string) => {
if (this.diffViewProvider.isEditing) {
+6
View File
@@ -351,6 +351,12 @@ export class AuthService {
throw new Error("Auth provider is not set")
}
// If a refresh is already in progress, wait for it to complete
if (this._refreshPromise) {
Logger.info("Token refresh already in progress, waiting for completion")
await this._refreshPromise
}
return this._provider.retrieveClineAuthInfo(this._controller)
}
+7 -1
View File
@@ -121,6 +121,11 @@ export function isGemini3ModelFamily(id: string): boolean {
return modelId.includes("gemini3") || modelId.includes("gemini-3")
}
function isDeepSeek32ModelFamily(id: string): boolean {
const modelId = normalize(id)
return modelId.includes("deepseek") && modelId.includes("3.2")
}
export function isNextGenModelFamily(id: string): boolean {
const modelId = normalize(id)
return (
@@ -130,7 +135,8 @@ export function isNextGenModelFamily(id: string): boolean {
isGPT5ModelFamily(modelId) ||
isMinimaxModelFamily(modelId) ||
isGemini3ModelFamily(modelId) ||
isNextGenOpenSourceModelFamily(modelId)
isNextGenOpenSourceModelFamily(modelId) ||
isDeepSeek32ModelFamily(modelId)
)
}
@@ -18,25 +18,27 @@ type McpViewProps = {
}
const McpConfigurationView = ({ onDone, initialTab }: McpViewProps) => {
const { mcpMarketplaceEnabled, setMcpServers, environment } = useExtensionState()
const [activeTab, setActiveTab] = useState<McpViewTab>(initialTab || (mcpMarketplaceEnabled ? "marketplace" : "configure"))
const { remoteConfigSettings, setMcpServers, environment } = useExtensionState()
// Show marketplace by default unless remote config explicitly disables it
const showMarketplace = remoteConfigSettings?.mcpMarketplaceEnabled !== false
const [activeTab, setActiveTab] = useState<McpViewTab>(initialTab || (showMarketplace ? "marketplace" : "configure"))
const handleTabChange = (tab: McpViewTab) => {
setActiveTab(tab)
}
useEffect(() => {
if (!mcpMarketplaceEnabled && activeTab === "marketplace") {
// If marketplace is disabled and we're on marketplace tab, switch to configure
if (!showMarketplace && activeTab === "marketplace") {
// If marketplace is disabled by remote config and we're on marketplace tab, switch to configure
setActiveTab("configure")
}
}, [mcpMarketplaceEnabled, activeTab])
}, [showMarketplace, activeTab])
// Get setter for MCP marketplace catalog from context
const { setMcpMarketplaceCatalog } = useExtensionState()
useEffect(() => {
if (mcpMarketplaceEnabled) {
if (showMarketplace) {
McpServiceClient.refreshMcpMarketplace(EmptyRequest.create({}))
.then((response) => {
setMcpMarketplaceCatalog(response)
@@ -56,7 +58,7 @@ const McpConfigurationView = ({ onDone, initialTab }: McpViewProps) => {
console.error("Failed to fetch MCP servers:", error)
})
}
}, [mcpMarketplaceEnabled])
}, [showMarketplace])
return (
<div
@@ -95,7 +97,7 @@ const McpConfigurationView = ({ onDone, initialTab }: McpViewProps) => {
padding: "0 20px 0 20px",
borderBottom: "1px solid var(--vscode-panel-border)",
}}>
{mcpMarketplaceEnabled && (
{showMarketplace && (
<TabButton isActive={activeTab === "marketplace"} onClick={() => handleTabChange("marketplace")}>
Marketplace
</TabButton>
@@ -110,7 +112,7 @@ const McpConfigurationView = ({ onDone, initialTab }: McpViewProps) => {
{/* Content container */}
<div style={{ width: "100%" }}>
{mcpMarketplaceEnabled && activeTab === "marketplace" && <McpMarketplaceView />}
{showMarketplace && activeTab === "marketplace" && <McpMarketplaceView />}
{activeTab === "addRemote" && <AddRemoteServerForm onServerAdded={() => handleTabChange("configure")} />}
{activeTab === "configure" && <ConfigureServersView />}
</div>
@@ -15,8 +15,9 @@ import McpMarketplaceCard from "./McpMarketplaceCard"
import McpSubmitCard from "./McpSubmitCard"
const McpMarketplaceView = () => {
const { mcpServers, mcpMarketplaceCatalog, setMcpMarketplaceCatalog, mcpMarketplaceEnabled, remoteConfigSettings } =
useExtensionState()
const { mcpServers, mcpMarketplaceCatalog, setMcpMarketplaceCatalog, remoteConfigSettings } = useExtensionState()
const showMarketplace = remoteConfigSettings?.mcpMarketplaceEnabled !== false
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [isRefreshing, setIsRefreshing] = useState(false)
@@ -80,7 +81,7 @@ const McpMarketplaceView = () => {
}
setError(null)
if (mcpMarketplaceEnabled) {
if (showMarketplace) {
McpServiceClient.refreshMcpMarketplace(EmptyRequest.create({}))
.then((response) => {
setMcpMarketplaceCatalog(response)
@@ -180,33 +180,6 @@ const FeatureSettingsSection = ({ renderSectionHeader }: FeatureSettingsSectionP
may not work well with large workspaces.
</p>
</div>
<div style={{ marginTop: 10 }}>
<Tooltip>
<TooltipTrigger>
<div className="flex items-center gap-2">
<VSCodeCheckbox
checked={mcpMarketplaceEnabled}
disabled={remoteConfigSettings?.mcpMarketplaceEnabled !== undefined}
onChange={(e: any) => {
const checked = e.target.checked === true
updateSetting("mcpMarketplaceEnabled", checked)
}}>
Enable MCP Marketplace
</VSCodeCheckbox>
{remoteConfigSettings?.mcpMarketplaceEnabled !== undefined && (
<i className="codicon codicon-lock text-description text-sm" />
)}
</div>
</TooltipTrigger>
<TooltipContent hidden={remoteConfigSettings?.mcpMarketplaceEnabled === undefined}>
This setting is managed by your organization's remote configuration
</TooltipContent>
</Tooltip>
<p className="text-xs text-description">
Enables the MCP Marketplace tab for discovering and installing MCP servers.
</p>
</div>
<div style={{ marginTop: 10 }}>
<label
className="block text-sm font-medium text-(--vscode-foreground) mb-1"