Browse Source

Resolved merge conflicts in Cline.ts and ClineProvider.ts

ShayBC 1 year ago
parent
commit
03e0aa545b
40 changed files with 1776 additions and 268 deletions
  1. 0 5
      .changeset/fluffy-apples-attack.md
  2. 0 5
      .changeset/orange-zoos-train.md
  3. 0 5
      .changeset/tender-cycles-help.md
  4. 20 0
      CHANGELOG.md
  5. 7 7
      package-lock.json
  6. 2 2
      package.json
  7. 0 1
      src/__mocks__/fs/promises.ts
  8. 30 0
      src/__mocks__/jest.setup.ts
  9. 1 1
      src/api/providers/__tests__/anthropic.test.ts
  10. 1 1
      src/api/providers/__tests__/openai-native.test.ts
  11. 448 5
      src/api/providers/__tests__/vertex.test.ts
  12. 47 39
      src/api/providers/anthropic.ts
  13. 10 3
      src/api/providers/openrouter.ts
  14. 284 30
      src/api/providers/vertex.ts
  15. 23 13
      src/core/Cline.ts
  16. 172 0
      src/core/prompts/__tests__/custom-system-prompt.test.ts
  17. 60 0
      src/core/prompts/sections/custom-system-prompt.ts
  18. 15 0
      src/core/prompts/system.ts
  19. 88 14
      src/core/sliding-window/__tests__/sliding-window.test.ts
  20. 18 19
      src/core/sliding-window/index.ts
  21. 26 20
      src/core/webview/ClineProvider.ts
  22. 2 2
      src/core/webview/__tests__/ClineProvider.test.ts
  23. 51 0
      src/services/ripgrep/__tests__/index.test.ts
  24. 26 4
      src/services/ripgrep/index.ts
  25. 1 1
      src/shared/ExtensionMessage.ts
  26. 1 1
      src/shared/WebviewMessage.ts
  27. 1 1
      src/shared/__tests__/checkExistApiConfig.test.ts
  28. 44 16
      src/shared/api.ts
  29. 3 2
      src/shared/globalState.ts
  30. 23 23
      webview-ui/src/components/chat/Announcement.tsx
  31. 5 0
      webview-ui/src/components/chat/ChatTextArea.tsx
  32. 32 1
      webview-ui/src/components/chat/ChatView.tsx
  33. 11 2
      webview-ui/src/components/chat/TaskHeader.tsx
  34. 40 0
      webview-ui/src/components/prompts/PromptsView.tsx
  35. 3 0
      webview-ui/src/components/settings/ApiOptions.tsx
  36. 21 24
      webview-ui/src/components/settings/SettingsView.tsx
  37. 61 17
      webview-ui/src/components/settings/ThinkingBudget.tsx
  38. 51 1
      webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx
  39. 145 0
      webview-ui/src/components/settings/__tests__/ThinkingBudget.test.tsx
  40. 3 3
      webview-ui/src/context/ExtensionStateContext.tsx

+ 0 - 5
.changeset/fluffy-apples-attack.md

@@ -1,5 +0,0 @@
----
-"roo-cline": patch
----
-
-Handle really long text in the ChatRow similar to TaskHeader

+ 0 - 5
.changeset/orange-zoos-train.md

@@ -1,5 +0,0 @@
----
-"roo-cline": patch
----
-
-Support multiple files in drag-and-drop

+ 0 - 5
.changeset/tender-cycles-help.md

@@ -1,5 +0,0 @@
----
-"roo-cline": patch
----
-
-Better OpenRouter error handling

+ 20 - 0
CHANGELOG.md

@@ -1,5 +1,25 @@
 # Roo Code Changelog
 # Roo Code Changelog
 
 
+## [3.7.8]
+
+- Add Vertex AI prompt caching support for Claude models (thanks @aitoroses and @lupuletic!)
+- Add gpt-4.5-preview
+- Add an advanced feature to customize the system prompt
+
+## [3.7.7]
+
+- Graduate checkpoints out of beta
+- Fix enhance prompt button when using Thinking Sonnet
+- Add tooltips to make what buttons do more obvious
+
+## [3.7.6]
+
+- Handle really long text better in the in the ChatRow similar to TaskHeader (thanks @joemanley201!)
+- Support multiple files in drag-and-drop
+- Truncate search_file output to avoid crashing the extension
+- Better OpenRouter error handling (no more "Provider Error")
+- Add slider to control max output tokens for thinking models
+
 ## [3.7.5]
 ## [3.7.5]
 
 
 - Fix context window truncation math (see [#1173](https://github.com/RooVetGit/Roo-Code/issues/1173))
 - Fix context window truncation math (see [#1173](https://github.com/RooVetGit/Roo-Code/issues/1173))

+ 7 - 7
package-lock.json

@@ -1,16 +1,16 @@
 {
 {
 	"name": "roo-cline",
 	"name": "roo-cline",
-	"version": "3.7.5",
+	"version": "3.7.8",
 	"lockfileVersion": 3,
 	"lockfileVersion": 3,
 	"requires": true,
 	"requires": true,
 	"packages": {
 	"packages": {
 		"": {
 		"": {
 			"name": "roo-cline",
 			"name": "roo-cline",
-			"version": "3.7.5",
+			"version": "3.7.8",
 			"dependencies": {
 			"dependencies": {
 				"@anthropic-ai/bedrock-sdk": "^0.10.2",
 				"@anthropic-ai/bedrock-sdk": "^0.10.2",
 				"@anthropic-ai/sdk": "^0.37.0",
 				"@anthropic-ai/sdk": "^0.37.0",
-				"@anthropic-ai/vertex-sdk": "^0.4.1",
+				"@anthropic-ai/vertex-sdk": "^0.7.0",
 				"@aws-sdk/client-bedrock-runtime": "^3.706.0",
 				"@aws-sdk/client-bedrock-runtime": "^3.706.0",
 				"@google/generative-ai": "^0.18.0",
 				"@google/generative-ai": "^0.18.0",
 				"@mistralai/mistralai": "^1.3.6",
 				"@mistralai/mistralai": "^1.3.6",
@@ -150,11 +150,11 @@
 			"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
 			"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
 		},
 		},
 		"node_modules/@anthropic-ai/vertex-sdk": {
 		"node_modules/@anthropic-ai/vertex-sdk": {
-			"version": "0.4.3",
-			"resolved": "https://registry.npmjs.org/@anthropic-ai/vertex-sdk/-/vertex-sdk-0.4.3.tgz",
-			"integrity": "sha512-2Uef0C5P2Hx+T88RnUSRA3u4aZqmqnrRSOb2N64ozgKPiSUPTM5JlggAq2b32yWMj5d3MLYa6spJXKMmHXOcoA==",
+			"version": "0.7.0",
+			"resolved": "https://registry.npmjs.org/@anthropic-ai/vertex-sdk/-/vertex-sdk-0.7.0.tgz",
+			"integrity": "sha512-zNm3hUXgYmYDTyveIxOyxbcnh5VXFkrLo4bSnG6LAfGzW7k3k2iCNDSVKtR9qZrK2BCid7JtVu7jsEKaZ/9dSw==",
 			"dependencies": {
 			"dependencies": {
-				"@anthropic-ai/sdk": ">=0.14 <1",
+				"@anthropic-ai/sdk": ">=0.35 <1",
 				"google-auth-library": "^9.4.2"
 				"google-auth-library": "^9.4.2"
 			}
 			}
 		},
 		},

+ 2 - 2
package.json

@@ -3,7 +3,7 @@
 	"displayName": "Roo Code (prev. Roo Cline)",
 	"displayName": "Roo Code (prev. Roo Cline)",
 	"description": "A whole dev team of AI agents in your editor.",
 	"description": "A whole dev team of AI agents in your editor.",
 	"publisher": "RooVeterinaryInc",
 	"publisher": "RooVeterinaryInc",
-	"version": "3.7.5",
+	"version": "3.7.8",
 	"icon": "assets/icons/rocket.png",
 	"icon": "assets/icons/rocket.png",
 	"galleryBanner": {
 	"galleryBanner": {
 		"color": "#617A91",
 		"color": "#617A91",
@@ -305,7 +305,7 @@
 	"dependencies": {
 	"dependencies": {
 		"@anthropic-ai/bedrock-sdk": "^0.10.2",
 		"@anthropic-ai/bedrock-sdk": "^0.10.2",
 		"@anthropic-ai/sdk": "^0.37.0",
 		"@anthropic-ai/sdk": "^0.37.0",
-		"@anthropic-ai/vertex-sdk": "^0.4.1",
+		"@anthropic-ai/vertex-sdk": "^0.7.0",
 		"@aws-sdk/client-bedrock-runtime": "^3.706.0",
 		"@aws-sdk/client-bedrock-runtime": "^3.706.0",
 		"@google/generative-ai": "^0.18.0",
 		"@google/generative-ai": "^0.18.0",
 		"@mistralai/mistralai": "^1.3.6",
 		"@mistralai/mistralai": "^1.3.6",

+ 0 - 1
src/__mocks__/fs/promises.ts

@@ -140,7 +140,6 @@ const mockFs = {
 		currentPath += "/" + parts[parts.length - 1]
 		currentPath += "/" + parts[parts.length - 1]
 		mockDirectories.add(currentPath)
 		mockDirectories.add(currentPath)
 		return Promise.resolve()
 		return Promise.resolve()
-		return Promise.resolve()
 	}),
 	}),
 
 
 	access: jest.fn().mockImplementation(async (path: string) => {
 	access: jest.fn().mockImplementation(async (path: string) => {

+ 30 - 0
src/__mocks__/jest.setup.ts

@@ -15,3 +15,33 @@ jest.mock("../utils/logging", () => ({
 		}),
 		}),
 	},
 	},
 }))
 }))
+
+// Add toPosix method to String prototype for all tests, mimicking src/utils/path.ts
+// This is needed because the production code expects strings to have this method
+// Note: In production, this is added via import in the entry point (extension.ts)
+export {}
+
+declare global {
+	interface String {
+		toPosix(): string
+	}
+}
+
+// Implementation that matches src/utils/path.ts
+function toPosixPath(p: string) {
+	// Extended-Length Paths in Windows start with "\\?\" to allow longer paths
+	// and bypass usual parsing. If detected, we return the path unmodified.
+	const isExtendedLengthPath = p.startsWith("\\\\?\\")
+
+	if (isExtendedLengthPath) {
+		return p
+	}
+
+	return p.replace(/\\/g, "/")
+}
+
+if (!String.prototype.toPosix) {
+	String.prototype.toPosix = function (this: string): string {
+		return toPosixPath(this)
+	}
+}

+ 1 - 1
src/api/providers/__tests__/anthropic.test.ts

@@ -153,7 +153,7 @@ describe("AnthropicHandler", () => {
 		})
 		})
 
 
 		it("should handle API errors", async () => {
 		it("should handle API errors", async () => {
-			mockCreate.mockRejectedValueOnce(new Error("API Error"))
+			mockCreate.mockRejectedValueOnce(new Error("Anthropic completion error: API Error"))
 			await expect(handler.completePrompt("Test prompt")).rejects.toThrow("Anthropic completion error: API Error")
 			await expect(handler.completePrompt("Test prompt")).rejects.toThrow("Anthropic completion error: API Error")
 		})
 		})
 
 

+ 1 - 1
src/api/providers/__tests__/openai-native.test.ts

@@ -357,7 +357,7 @@ describe("OpenAiNativeHandler", () => {
 			const modelInfo = handler.getModel()
 			const modelInfo = handler.getModel()
 			expect(modelInfo.id).toBe(mockOptions.apiModelId)
 			expect(modelInfo.id).toBe(mockOptions.apiModelId)
 			expect(modelInfo.info).toBeDefined()
 			expect(modelInfo.info).toBeDefined()
-			expect(modelInfo.info.maxTokens).toBe(4096)
+			expect(modelInfo.info.maxTokens).toBe(16384)
 			expect(modelInfo.info.contextWindow).toBe(128_000)
 			expect(modelInfo.info.contextWindow).toBe(128_000)
 		})
 		})
 
 

+ 448 - 5
src/api/providers/__tests__/vertex.test.ts

@@ -2,8 +2,10 @@
 
 
 import { Anthropic } from "@anthropic-ai/sdk"
 import { Anthropic } from "@anthropic-ai/sdk"
 import { AnthropicVertex } from "@anthropic-ai/vertex-sdk"
 import { AnthropicVertex } from "@anthropic-ai/vertex-sdk"
+import { BetaThinkingConfigParam } from "@anthropic-ai/sdk/resources/beta"
 
 
 import { VertexHandler } from "../vertex"
 import { VertexHandler } from "../vertex"
+import { ApiStreamChunk } from "../../transform/stream"
 
 
 // Mock Vertex SDK
 // Mock Vertex SDK
 jest.mock("@anthropic-ai/vertex-sdk", () => ({
 jest.mock("@anthropic-ai/vertex-sdk", () => ({
@@ -128,7 +130,7 @@ describe("VertexHandler", () => {
 			;(handler["client"].messages as any).create = mockCreate
 			;(handler["client"].messages as any).create = mockCreate
 
 
 			const stream = handler.createMessage(systemPrompt, mockMessages)
 			const stream = handler.createMessage(systemPrompt, mockMessages)
-			const chunks = []
+			const chunks: ApiStreamChunk[] = []
 
 
 			for await (const chunk of stream) {
 			for await (const chunk of stream) {
 				chunks.push(chunk)
 				chunks.push(chunk)
@@ -158,8 +160,29 @@ describe("VertexHandler", () => {
 				model: "claude-3-5-sonnet-v2@20241022",
 				model: "claude-3-5-sonnet-v2@20241022",
 				max_tokens: 8192,
 				max_tokens: 8192,
 				temperature: 0,
 				temperature: 0,
-				system: systemPrompt,
-				messages: mockMessages,
+				system: [
+					{
+						type: "text",
+						text: "You are a helpful assistant",
+						cache_control: { type: "ephemeral" },
+					},
+				],
+				messages: [
+					{
+						role: "user",
+						content: [
+							{
+								type: "text",
+								text: "Hello",
+								cache_control: { type: "ephemeral" },
+							},
+						],
+					},
+					{
+						role: "assistant",
+						content: "Hi there!",
+					},
+				],
 				stream: true,
 				stream: true,
 			})
 			})
 		})
 		})
@@ -196,7 +219,7 @@ describe("VertexHandler", () => {
 			;(handler["client"].messages as any).create = mockCreate
 			;(handler["client"].messages as any).create = mockCreate
 
 
 			const stream = handler.createMessage(systemPrompt, mockMessages)
 			const stream = handler.createMessage(systemPrompt, mockMessages)
-			const chunks = []
+			const chunks: ApiStreamChunk[] = []
 
 
 			for await (const chunk of stream) {
 			for await (const chunk of stream) {
 				chunks.push(chunk)
 				chunks.push(chunk)
@@ -230,6 +253,315 @@ describe("VertexHandler", () => {
 				}
 				}
 			}).rejects.toThrow("Vertex API error")
 			}).rejects.toThrow("Vertex API error")
 		})
 		})
+
+		it("should handle prompt caching for supported models", async () => {
+			const mockStream = [
+				{
+					type: "message_start",
+					message: {
+						usage: {
+							input_tokens: 10,
+							output_tokens: 0,
+							cache_creation_input_tokens: 3,
+							cache_read_input_tokens: 2,
+						},
+					},
+				},
+				{
+					type: "content_block_start",
+					index: 0,
+					content_block: {
+						type: "text",
+						text: "Hello",
+					},
+				},
+				{
+					type: "content_block_delta",
+					delta: {
+						type: "text_delta",
+						text: " world!",
+					},
+				},
+				{
+					type: "message_delta",
+					usage: {
+						output_tokens: 5,
+					},
+				},
+			]
+
+			const asyncIterator = {
+				async *[Symbol.asyncIterator]() {
+					for (const chunk of mockStream) {
+						yield chunk
+					}
+				},
+			}
+
+			const mockCreate = jest.fn().mockResolvedValue(asyncIterator)
+			;(handler["client"].messages as any).create = mockCreate
+
+			const stream = handler.createMessage(systemPrompt, [
+				{
+					role: "user",
+					content: "First message",
+				},
+				{
+					role: "assistant",
+					content: "Response",
+				},
+				{
+					role: "user",
+					content: "Second message",
+				},
+			])
+
+			const chunks: ApiStreamChunk[] = []
+			for await (const chunk of stream) {
+				chunks.push(chunk)
+			}
+
+			// Verify usage information
+			const usageChunks = chunks.filter((chunk) => chunk.type === "usage")
+			expect(usageChunks).toHaveLength(2)
+			expect(usageChunks[0]).toEqual({
+				type: "usage",
+				inputTokens: 10,
+				outputTokens: 0,
+				cacheWriteTokens: 3,
+				cacheReadTokens: 2,
+			})
+			expect(usageChunks[1]).toEqual({
+				type: "usage",
+				inputTokens: 0,
+				outputTokens: 5,
+			})
+
+			// Verify text content
+			const textChunks = chunks.filter((chunk) => chunk.type === "text")
+			expect(textChunks).toHaveLength(2)
+			expect(textChunks[0].text).toBe("Hello")
+			expect(textChunks[1].text).toBe(" world!")
+
+			// Verify cache control was added correctly
+			expect(mockCreate).toHaveBeenCalledWith(
+				expect.objectContaining({
+					system: [
+						{
+							type: "text",
+							text: "You are a helpful assistant",
+							cache_control: { type: "ephemeral" },
+						},
+					],
+					messages: [
+						expect.objectContaining({
+							role: "user",
+							content: [
+								{
+									type: "text",
+									text: "First message",
+									cache_control: { type: "ephemeral" },
+								},
+							],
+						}),
+						expect.objectContaining({
+							role: "assistant",
+							content: "Response",
+						}),
+						expect.objectContaining({
+							role: "user",
+							content: [
+								{
+									type: "text",
+									text: "Second message",
+									cache_control: { type: "ephemeral" },
+								},
+							],
+						}),
+					],
+				}),
+			)
+		})
+
+		it("should handle cache-related usage metrics", async () => {
+			const mockStream = [
+				{
+					type: "message_start",
+					message: {
+						usage: {
+							input_tokens: 10,
+							output_tokens: 0,
+							cache_creation_input_tokens: 5,
+							cache_read_input_tokens: 3,
+						},
+					},
+				},
+				{
+					type: "content_block_start",
+					index: 0,
+					content_block: {
+						type: "text",
+						text: "Hello",
+					},
+				},
+			]
+
+			const asyncIterator = {
+				async *[Symbol.asyncIterator]() {
+					for (const chunk of mockStream) {
+						yield chunk
+					}
+				},
+			}
+
+			const mockCreate = jest.fn().mockResolvedValue(asyncIterator)
+			;(handler["client"].messages as any).create = mockCreate
+
+			const stream = handler.createMessage(systemPrompt, mockMessages)
+			const chunks: ApiStreamChunk[] = []
+
+			for await (const chunk of stream) {
+				chunks.push(chunk)
+			}
+
+			// Check for cache-related metrics in usage chunk
+			const usageChunks = chunks.filter((chunk) => chunk.type === "usage")
+			expect(usageChunks.length).toBeGreaterThan(0)
+			expect(usageChunks[0]).toHaveProperty("cacheWriteTokens", 5)
+			expect(usageChunks[0]).toHaveProperty("cacheReadTokens", 3)
+		})
+	})
+
+	describe("thinking functionality", () => {
+		const mockMessages: Anthropic.Messages.MessageParam[] = [
+			{
+				role: "user",
+				content: "Hello",
+			},
+		]
+
+		const systemPrompt = "You are a helpful assistant"
+
+		it("should handle thinking content blocks and deltas", async () => {
+			const mockStream = [
+				{
+					type: "message_start",
+					message: {
+						usage: {
+							input_tokens: 10,
+							output_tokens: 0,
+						},
+					},
+				},
+				{
+					type: "content_block_start",
+					index: 0,
+					content_block: {
+						type: "thinking",
+						thinking: "Let me think about this...",
+					},
+				},
+				{
+					type: "content_block_delta",
+					delta: {
+						type: "thinking_delta",
+						thinking: " I need to consider all options.",
+					},
+				},
+				{
+					type: "content_block_start",
+					index: 1,
+					content_block: {
+						type: "text",
+						text: "Here's my answer:",
+					},
+				},
+			]
+
+			// Setup async iterator for mock stream
+			const asyncIterator = {
+				async *[Symbol.asyncIterator]() {
+					for (const chunk of mockStream) {
+						yield chunk
+					}
+				},
+			}
+
+			const mockCreate = jest.fn().mockResolvedValue(asyncIterator)
+			;(handler["client"].messages as any).create = mockCreate
+
+			const stream = handler.createMessage(systemPrompt, mockMessages)
+			const chunks: ApiStreamChunk[] = []
+
+			for await (const chunk of stream) {
+				chunks.push(chunk)
+			}
+
+			// Verify thinking content is processed correctly
+			const reasoningChunks = chunks.filter((chunk) => chunk.type === "reasoning")
+			expect(reasoningChunks).toHaveLength(2)
+			expect(reasoningChunks[0].text).toBe("Let me think about this...")
+			expect(reasoningChunks[1].text).toBe(" I need to consider all options.")
+
+			// Verify text content is processed correctly
+			const textChunks = chunks.filter((chunk) => chunk.type === "text")
+			expect(textChunks).toHaveLength(2) // One for the text block, one for the newline
+			expect(textChunks[0].text).toBe("\n")
+			expect(textChunks[1].text).toBe("Here's my answer:")
+		})
+
+		it("should handle multiple thinking blocks with line breaks", async () => {
+			const mockStream = [
+				{
+					type: "content_block_start",
+					index: 0,
+					content_block: {
+						type: "thinking",
+						thinking: "First thinking block",
+					},
+				},
+				{
+					type: "content_block_start",
+					index: 1,
+					content_block: {
+						type: "thinking",
+						thinking: "Second thinking block",
+					},
+				},
+			]
+
+			const asyncIterator = {
+				async *[Symbol.asyncIterator]() {
+					for (const chunk of mockStream) {
+						yield chunk
+					}
+				},
+			}
+
+			const mockCreate = jest.fn().mockResolvedValue(asyncIterator)
+			;(handler["client"].messages as any).create = mockCreate
+
+			const stream = handler.createMessage(systemPrompt, mockMessages)
+			const chunks: ApiStreamChunk[] = []
+
+			for await (const chunk of stream) {
+				chunks.push(chunk)
+			}
+
+			expect(chunks.length).toBe(3)
+			expect(chunks[0]).toEqual({
+				type: "reasoning",
+				text: "First thinking block",
+			})
+			expect(chunks[1]).toEqual({
+				type: "reasoning",
+				text: "\n",
+			})
+			expect(chunks[2]).toEqual({
+				type: "reasoning",
+				text: "Second thinking block",
+			})
+		})
 	})
 	})
 
 
 	describe("completePrompt", () => {
 	describe("completePrompt", () => {
@@ -240,7 +572,13 @@ describe("VertexHandler", () => {
 				model: "claude-3-5-sonnet-v2@20241022",
 				model: "claude-3-5-sonnet-v2@20241022",
 				max_tokens: 8192,
 				max_tokens: 8192,
 				temperature: 0,
 				temperature: 0,
-				messages: [{ role: "user", content: "Test prompt" }],
+				system: "",
+				messages: [
+					{
+						role: "user",
+						content: [{ type: "text", text: "Test prompt", cache_control: { type: "ephemeral" } }],
+					},
+				],
 				stream: false,
 				stream: false,
 			})
 			})
 		})
 		})
@@ -295,4 +633,109 @@ describe("VertexHandler", () => {
 			expect(modelInfo.id).toBe("claude-3-7-sonnet@20250219") // Default model
 			expect(modelInfo.id).toBe("claude-3-7-sonnet@20250219") // Default model
 		})
 		})
 	})
 	})
+
+	describe("thinking model configuration", () => {
+		it("should configure thinking for models with :thinking suffix", () => {
+			const thinkingHandler = new VertexHandler({
+				apiModelId: "claude-3-7-sonnet@20250219:thinking",
+				vertexProjectId: "test-project",
+				vertexRegion: "us-central1",
+				modelMaxTokens: 16384,
+				modelMaxThinkingTokens: 4096,
+			})
+
+			const modelInfo = thinkingHandler.getModel()
+
+			// Verify thinking configuration
+			expect(modelInfo.id).toBe("claude-3-7-sonnet@20250219")
+			expect(modelInfo.thinking).toBeDefined()
+			const thinkingConfig = modelInfo.thinking as { type: "enabled"; budget_tokens: number }
+			expect(thinkingConfig.type).toBe("enabled")
+			expect(thinkingConfig.budget_tokens).toBe(4096)
+			expect(modelInfo.temperature).toBe(1.0) // Thinking requires temperature 1.0
+		})
+
+		it("should calculate thinking budget correctly", () => {
+			// Test with explicit thinking budget
+			const handlerWithBudget = new VertexHandler({
+				apiModelId: "claude-3-7-sonnet@20250219:thinking",
+				vertexProjectId: "test-project",
+				vertexRegion: "us-central1",
+				modelMaxTokens: 16384,
+				modelMaxThinkingTokens: 5000,
+			})
+
+			expect((handlerWithBudget.getModel().thinking as any).budget_tokens).toBe(5000)
+
+			// Test with default thinking budget (80% of max tokens)
+			const handlerWithDefaultBudget = new VertexHandler({
+				apiModelId: "claude-3-7-sonnet@20250219:thinking",
+				vertexProjectId: "test-project",
+				vertexRegion: "us-central1",
+				modelMaxTokens: 10000,
+			})
+
+			expect((handlerWithDefaultBudget.getModel().thinking as any).budget_tokens).toBe(8000) // 80% of 10000
+
+			// Test with minimum thinking budget (should be at least 1024)
+			const handlerWithSmallMaxTokens = new VertexHandler({
+				apiModelId: "claude-3-7-sonnet@20250219:thinking",
+				vertexProjectId: "test-project",
+				vertexRegion: "us-central1",
+				modelMaxTokens: 1000, // This would result in 800 tokens for thinking, but minimum is 1024
+			})
+
+			expect((handlerWithSmallMaxTokens.getModel().thinking as any).budget_tokens).toBe(1024)
+		})
+
+		it("should pass thinking configuration to API", async () => {
+			const thinkingHandler = new VertexHandler({
+				apiModelId: "claude-3-7-sonnet@20250219:thinking",
+				vertexProjectId: "test-project",
+				vertexRegion: "us-central1",
+				modelMaxTokens: 16384,
+				modelMaxThinkingTokens: 4096,
+			})
+
+			const mockCreate = jest.fn().mockImplementation(async (options) => {
+				if (!options.stream) {
+					return {
+						id: "test-completion",
+						content: [{ type: "text", text: "Test response" }],
+						role: "assistant",
+						model: options.model,
+						usage: {
+							input_tokens: 10,
+							output_tokens: 5,
+						},
+					}
+				}
+				return {
+					async *[Symbol.asyncIterator]() {
+						yield {
+							type: "message_start",
+							message: {
+								usage: {
+									input_tokens: 10,
+									output_tokens: 5,
+								},
+							},
+						}
+					},
+				}
+			})
+			;(thinkingHandler["client"].messages as any).create = mockCreate
+
+			await thinkingHandler
+				.createMessage("You are a helpful assistant", [{ role: "user", content: "Hello" }])
+				.next()
+
+			expect(mockCreate).toHaveBeenCalledWith(
+				expect.objectContaining({
+					thinking: { type: "enabled", budget_tokens: 4096 },
+					temperature: 1.0, // Thinking requires temperature 1.0
+				}),
+			)
+		})
+	})
 })
 })

+ 47 - 39
src/api/providers/anthropic.ts

@@ -30,21 +30,7 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler {
 	async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
 	async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
 		let stream: AnthropicStream<Anthropic.Messages.RawMessageStreamEvent>
 		let stream: AnthropicStream<Anthropic.Messages.RawMessageStreamEvent>
 		const cacheControl: CacheControlEphemeral = { type: "ephemeral" }
 		const cacheControl: CacheControlEphemeral = { type: "ephemeral" }
-		let { id: modelId, info: modelInfo } = this.getModel()
-		const maxTokens = modelInfo.maxTokens || 8192
-		let temperature = this.options.modelTemperature ?? ANTHROPIC_DEFAULT_TEMPERATURE
-		let thinking: BetaThinkingConfigParam | undefined = undefined
-
-		// Anthropic "Thinking" models require a temperature of 1.0.
-		if (modelId === "claude-3-7-sonnet-20250219:thinking") {
-			// The `:thinking` variant is a virtual identifier for the
-			// `claude-3-7-sonnet-20250219` model with a thinking budget.
-			// We can handle this more elegantly in the future.
-			modelId = "claude-3-7-sonnet-20250219"
-			const budgetTokens = this.options.anthropicThinking ?? Math.max(maxTokens * 0.8, 1024)
-			thinking = { type: "enabled", budget_tokens: budgetTokens }
-			temperature = 1.0
-		}
+		let { id: modelId, temperature, maxTokens, thinking } = this.getModel()
 
 
 		switch (modelId) {
 		switch (modelId) {
 			case "claude-3-7-sonnet-20250219":
 			case "claude-3-7-sonnet-20250219":
@@ -194,40 +180,62 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler {
 		}
 		}
 	}
 	}
 
 
-	getModel(): { id: AnthropicModelId; info: ModelInfo } {
+	getModel() {
 		const modelId = this.options.apiModelId
 		const modelId = this.options.apiModelId
+		let temperature = this.options.modelTemperature ?? ANTHROPIC_DEFAULT_TEMPERATURE
+		let thinking: BetaThinkingConfigParam | undefined = undefined
 
 
 		if (modelId && modelId in anthropicModels) {
 		if (modelId && modelId in anthropicModels) {
-			const id = modelId as AnthropicModelId
-			return { id, info: anthropicModels[id] }
-		}
+			let id = modelId as AnthropicModelId
+			const info: ModelInfo = anthropicModels[id]
 
 
-		return { id: anthropicDefaultModelId, info: anthropicModels[anthropicDefaultModelId] }
-	}
+			// The `:thinking` variant is a virtual identifier for the
+			// `claude-3-7-sonnet-20250219` model with a thinking budget.
+			// We can handle this more elegantly in the future.
+			if (id === "claude-3-7-sonnet-20250219:thinking") {
+				id = "claude-3-7-sonnet-20250219"
+			}
 
 
-	async completePrompt(prompt: string): Promise<string> {
-		try {
-			const response = await this.client.messages.create({
-				model: this.getModel().id,
-				max_tokens: this.getModel().info.maxTokens || 8192,
-				temperature: this.options.modelTemperature ?? ANTHROPIC_DEFAULT_TEMPERATURE,
-				messages: [{ role: "user", content: prompt }],
-				stream: false,
-			})
+			const maxTokens = this.options.modelMaxTokens || info.maxTokens || 8192
 
 
-			const content = response.content[0]
+			if (info.thinking) {
+				// Anthropic "Thinking" models require a temperature of 1.0.
+				temperature = 1.0
 
 
-			if (content.type === "text") {
-				return content.text
-			}
+				// Clamp the thinking budget to be at most 80% of max tokens and at
+				// least 1024 tokens.
+				const maxBudgetTokens = Math.floor(maxTokens * 0.8)
+				const budgetTokens = Math.max(
+					Math.min(this.options.modelMaxThinkingTokens ?? maxBudgetTokens, maxBudgetTokens),
+					1024,
+				)
 
 
-			return ""
-		} catch (error) {
-			if (error instanceof Error) {
-				throw new Error(`Anthropic completion error: ${error.message}`)
+				thinking = { type: "enabled", budget_tokens: budgetTokens }
 			}
 			}
 
 
-			throw error
+			return { id, info, temperature, maxTokens, thinking }
 		}
 		}
+
+		const id = anthropicDefaultModelId
+		const info: ModelInfo = anthropicModels[id]
+		const maxTokens = this.options.modelMaxTokens || info.maxTokens || 8192
+
+		return { id, info, temperature, maxTokens, thinking }
+	}
+
+	async completePrompt(prompt: string) {
+		let { id: modelId, temperature, maxTokens, thinking } = this.getModel()
+
+		const message = await this.client.messages.create({
+			model: modelId,
+			max_tokens: maxTokens,
+			temperature,
+			thinking,
+			messages: [{ role: "user", content: prompt }],
+			stream: false,
+		})
+
+		const content = message.content.find(({ type }) => type === "text")
+		return content?.type === "text" ? content.text : ""
 	}
 	}
 }
 }

+ 10 - 3
src/api/providers/openrouter.ts

@@ -108,12 +108,19 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler {
 			topP = 0.95
 			topP = 0.95
 		}
 		}
 
 
+		const maxTokens = this.options.modelMaxTokens || modelInfo.maxTokens
 		let temperature = this.options.modelTemperature ?? defaultTemperature
 		let temperature = this.options.modelTemperature ?? defaultTemperature
 		let thinking: BetaThinkingConfigParam | undefined = undefined
 		let thinking: BetaThinkingConfigParam | undefined = undefined
 
 
 		if (modelInfo.thinking) {
 		if (modelInfo.thinking) {
-			const maxTokens = modelInfo.maxTokens || 8192
-			const budgetTokens = this.options.anthropicThinking ?? Math.max(maxTokens * 0.8, 1024)
+			// Clamp the thinking budget to be at most 80% of max tokens and at
+			// least 1024 tokens.
+			const maxBudgetTokens = Math.floor((maxTokens || 8192) * 0.8)
+			const budgetTokens = Math.max(
+				Math.min(this.options.modelMaxThinkingTokens ?? maxBudgetTokens, maxBudgetTokens),
+				1024,
+			)
+
 			thinking = { type: "enabled", budget_tokens: budgetTokens }
 			thinking = { type: "enabled", budget_tokens: budgetTokens }
 			temperature = 1.0
 			temperature = 1.0
 		}
 		}
@@ -271,7 +278,7 @@ export async function getOpenRouterModels() {
 					modelInfo.supportsPromptCache = true
 					modelInfo.supportsPromptCache = true
 					modelInfo.cacheWritesPrice = 3.75
 					modelInfo.cacheWritesPrice = 3.75
 					modelInfo.cacheReadsPrice = 0.3
 					modelInfo.cacheReadsPrice = 0.3
-					modelInfo.maxTokens = 16384
+					modelInfo.maxTokens = 64_000
 					break
 					break
 				case rawModel.id.startsWith("anthropic/claude-3.5-sonnet-20240620"):
 				case rawModel.id.startsWith("anthropic/claude-3.5-sonnet-20240620"):
 					modelInfo.supportsPromptCache = true
 					modelInfo.supportsPromptCache = true

+ 284 - 30
src/api/providers/vertex.ts

@@ -1,9 +1,97 @@
 import { Anthropic } from "@anthropic-ai/sdk"
 import { Anthropic } from "@anthropic-ai/sdk"
 import { AnthropicVertex } from "@anthropic-ai/vertex-sdk"
 import { AnthropicVertex } from "@anthropic-ai/vertex-sdk"
+import { Stream as AnthropicStream } from "@anthropic-ai/sdk/streaming"
 import { ApiHandler, SingleCompletionHandler } from "../"
 import { ApiHandler, SingleCompletionHandler } from "../"
+import { BetaThinkingConfigParam } from "@anthropic-ai/sdk/resources/beta"
 import { ApiHandlerOptions, ModelInfo, vertexDefaultModelId, VertexModelId, vertexModels } from "../../shared/api"
 import { ApiHandlerOptions, ModelInfo, vertexDefaultModelId, VertexModelId, vertexModels } from "../../shared/api"
 import { ApiStream } from "../transform/stream"
 import { ApiStream } from "../transform/stream"
 
 
+// Types for Vertex SDK
+
+/**
+ * Vertex API has specific limitations for prompt caching:
+ * 1. Maximum of 4 blocks can have cache_control
+ * 2. Only text blocks can be cached (images and other content types cannot)
+ * 3. Cache control can only be applied to user messages, not assistant messages
+ *
+ * Our caching strategy:
+ * - Cache the system prompt (1 block)
+ * - Cache the last text block of the second-to-last user message (1 block)
+ * - Cache the last text block of the last user message (1 block)
+ * This ensures we stay under the 4-block limit while maintaining effective caching
+ * for the most relevant context.
+ */
+
+interface VertexTextBlock {
+	type: "text"
+	text: string
+	cache_control?: { type: "ephemeral" }
+}
+
+interface VertexImageBlock {
+	type: "image"
+	source: {
+		type: "base64"
+		media_type: "image/jpeg" | "image/png" | "image/gif" | "image/webp"
+		data: string
+	}
+}
+
+type VertexContentBlock = VertexTextBlock | VertexImageBlock
+
+interface VertexUsage {
+	input_tokens?: number
+	output_tokens?: number
+	cache_creation_input_tokens?: number
+	cache_read_input_tokens?: number
+}
+
+interface VertexMessage extends Omit<Anthropic.Messages.MessageParam, "content"> {
+	content: string | VertexContentBlock[]
+}
+
+interface VertexMessageCreateParams {
+	model: string
+	max_tokens: number
+	temperature: number
+	system: string | VertexTextBlock[]
+	messages: VertexMessage[]
+	stream: boolean
+}
+
+interface VertexMessageResponse {
+	content: Array<{ type: "text"; text: string }>
+}
+
+interface VertexMessageStreamEvent {
+	type: "message_start" | "message_delta" | "content_block_start" | "content_block_delta"
+	message?: {
+		usage: VertexUsage
+	}
+	usage?: {
+		output_tokens: number
+	}
+	content_block?:
+		| {
+				type: "text"
+				text: string
+		  }
+		| {
+				type: "thinking"
+				thinking: string
+		  }
+	index?: number
+	delta?:
+		| {
+				type: "text_delta"
+				text: string
+		  }
+		| {
+				type: "thinking_delta"
+				thinking: string
+		  }
+}
+
 // https://docs.anthropic.com/en/api/claude-on-vertex-ai
 // https://docs.anthropic.com/en/api/claude-on-vertex-ai
 export class VertexHandler implements ApiHandler, SingleCompletionHandler {
 export class VertexHandler implements ApiHandler, SingleCompletionHandler {
 	private options: ApiHandlerOptions
 	private options: ApiHandlerOptions
@@ -18,37 +106,122 @@ export class VertexHandler implements ApiHandler, SingleCompletionHandler {
 		})
 		})
 	}
 	}
 
 
+	private formatMessageForCache(message: Anthropic.Messages.MessageParam, shouldCache: boolean): VertexMessage {
+		// Assistant messages are kept as-is since they can't be cached
+		if (message.role === "assistant") {
+			return message as VertexMessage
+		}
+
+		// For string content, we convert to array format with optional cache control
+		if (typeof message.content === "string") {
+			return {
+				...message,
+				content: [
+					{
+						type: "text" as const,
+						text: message.content,
+						// For string content, we only have one block so it's always the last
+						...(shouldCache && { cache_control: { type: "ephemeral" } }),
+					},
+				],
+			}
+		}
+
+		// For array content, find the last text block index once before mapping
+		const lastTextBlockIndex = message.content.reduce(
+			(lastIndex, content, index) => (content.type === "text" ? index : lastIndex),
+			-1,
+		)
+
+		// Then use this pre-calculated index in the map function
+		return {
+			...message,
+			content: message.content.map((content, contentIndex) => {
+				// Images and other non-text content are passed through unchanged
+				if (content.type === "image") {
+					return content as VertexImageBlock
+				}
+
+				// Check if this is the last text block using our pre-calculated index
+				const isLastTextBlock = contentIndex === lastTextBlockIndex
+
+				return {
+					type: "text" as const,
+					text: (content as { text: string }).text,
+					...(shouldCache && isLastTextBlock && { cache_control: { type: "ephemeral" } }),
+				}
+			}),
+		}
+	}
+
 	async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
 	async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
-		const stream = await this.client.messages.create({
-			model: this.getModel().id,
-			max_tokens: this.getModel().info.maxTokens || 8192,
-			temperature: this.options.modelTemperature ?? 0,
-			system: systemPrompt,
-			messages,
+		const model = this.getModel()
+		let { id, info, temperature, maxTokens, thinking } = model
+		const useCache = model.info.supportsPromptCache
+
+		// Find indices of user messages that we want to cache
+		// We only cache the last two user messages to stay within the 4-block limit
+		// (1 block for system + 1 block each for last two user messages = 3 total)
+		const userMsgIndices = useCache
+			? messages.reduce((acc, msg, i) => (msg.role === "user" ? [...acc, i] : acc), [] as number[])
+			: []
+		const lastUserMsgIndex = userMsgIndices[userMsgIndices.length - 1] ?? -1
+		const secondLastMsgUserIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1
+
+		// Create the stream with appropriate caching configuration
+		const params = {
+			model: id,
+			max_tokens: maxTokens,
+			temperature,
+			thinking,
+			// Cache the system prompt if caching is enabled
+			system: useCache
+				? [
+						{
+							text: systemPrompt,
+							type: "text" as const,
+							cache_control: { type: "ephemeral" },
+						},
+					]
+				: systemPrompt,
+			messages: messages.map((message, index) => {
+				// Only cache the last two user messages
+				const shouldCache = useCache && (index === lastUserMsgIndex || index === secondLastMsgUserIndex)
+				return this.formatMessageForCache(message, shouldCache)
+			}),
 			stream: true,
 			stream: true,
-		})
+		}
+
+		const stream = (await this.client.messages.create(
+			params as Anthropic.Messages.MessageCreateParamsStreaming,
+		)) as unknown as AnthropicStream<VertexMessageStreamEvent>
+
+		// Process the stream chunks
 		for await (const chunk of stream) {
 		for await (const chunk of stream) {
 			switch (chunk.type) {
 			switch (chunk.type) {
-				case "message_start":
-					const usage = chunk.message.usage
+				case "message_start": {
+					const usage = chunk.message!.usage
 					yield {
 					yield {
 						type: "usage",
 						type: "usage",
 						inputTokens: usage.input_tokens || 0,
 						inputTokens: usage.input_tokens || 0,
 						outputTokens: usage.output_tokens || 0,
 						outputTokens: usage.output_tokens || 0,
+						cacheWriteTokens: usage.cache_creation_input_tokens,
+						cacheReadTokens: usage.cache_read_input_tokens,
 					}
 					}
 					break
 					break
-				case "message_delta":
+				}
+				case "message_delta": {
 					yield {
 					yield {
 						type: "usage",
 						type: "usage",
 						inputTokens: 0,
 						inputTokens: 0,
-						outputTokens: chunk.usage.output_tokens || 0,
+						outputTokens: chunk.usage!.output_tokens || 0,
 					}
 					}
 					break
 					break
-
-				case "content_block_start":
-					switch (chunk.content_block.type) {
-						case "text":
-							if (chunk.index > 0) {
+				}
+				case "content_block_start": {
+					switch (chunk.content_block!.type) {
+						case "text": {
+							if (chunk.index! > 0) {
 								yield {
 								yield {
 									type: "text",
 									type: "text",
 									text: "\n",
 									text: "\n",
@@ -56,43 +229,124 @@ export class VertexHandler implements ApiHandler, SingleCompletionHandler {
 							}
 							}
 							yield {
 							yield {
 								type: "text",
 								type: "text",
-								text: chunk.content_block.text,
+								text: chunk.content_block!.text,
+							}
+							break
+						}
+						case "thinking": {
+							if (chunk.index! > 0) {
+								yield {
+									type: "reasoning",
+									text: "\n",
+								}
+							}
+							yield {
+								type: "reasoning",
+								text: (chunk.content_block as any).thinking,
 							}
 							}
 							break
 							break
+						}
 					}
 					}
 					break
 					break
-				case "content_block_delta":
-					switch (chunk.delta.type) {
-						case "text_delta":
+				}
+				case "content_block_delta": {
+					switch (chunk.delta!.type) {
+						case "text_delta": {
 							yield {
 							yield {
 								type: "text",
 								type: "text",
-								text: chunk.delta.text,
+								text: chunk.delta!.text,
 							}
 							}
 							break
 							break
+						}
+						case "thinking_delta": {
+							yield {
+								type: "reasoning",
+								text: (chunk.delta as any).thinking,
+							}
+							break
+						}
 					}
 					}
 					break
 					break
+				}
 			}
 			}
 		}
 		}
 	}
 	}
 
 
-	getModel(): { id: VertexModelId; info: ModelInfo } {
+	getModel(): {
+		id: VertexModelId
+		info: ModelInfo
+		temperature: number
+		maxTokens: number
+		thinking?: BetaThinkingConfigParam
+	} {
 		const modelId = this.options.apiModelId
 		const modelId = this.options.apiModelId
+		let temperature = this.options.modelTemperature ?? 0
+		let thinking: BetaThinkingConfigParam | undefined = undefined
+
 		if (modelId && modelId in vertexModels) {
 		if (modelId && modelId in vertexModels) {
 			const id = modelId as VertexModelId
 			const id = modelId as VertexModelId
-			return { id, info: vertexModels[id] }
+			const info: ModelInfo = vertexModels[id]
+
+			// The `:thinking` variant is a virtual identifier for thinking-enabled models
+			// Similar to how it's handled in the Anthropic provider
+			let actualId = id
+			if (id.endsWith(":thinking")) {
+				actualId = id.replace(":thinking", "") as VertexModelId
+			}
+
+			const maxTokens = this.options.modelMaxTokens || info.maxTokens || 8192
+
+			if (info.thinking) {
+				temperature = 1.0 // Thinking requires temperature 1.0
+				const maxBudgetTokens = Math.floor(maxTokens * 0.8)
+				const budgetTokens = Math.max(
+					Math.min(this.options.modelMaxThinkingTokens ?? maxBudgetTokens, maxBudgetTokens),
+					1024,
+				)
+				thinking = { type: "enabled", budget_tokens: budgetTokens }
+			}
+
+			return { id: actualId, info, temperature, maxTokens, thinking }
 		}
 		}
-		return { id: vertexDefaultModelId, info: vertexModels[vertexDefaultModelId] }
+
+		const id = vertexDefaultModelId
+		const info = vertexModels[id]
+		const maxTokens = this.options.modelMaxTokens || info.maxTokens || 8192
+
+		return { id, info, temperature, maxTokens, thinking }
 	}
 	}
 
 
 	async completePrompt(prompt: string): Promise<string> {
 	async completePrompt(prompt: string): Promise<string> {
 		try {
 		try {
-			const response = await this.client.messages.create({
-				model: this.getModel().id,
-				max_tokens: this.getModel().info.maxTokens || 8192,
-				temperature: this.options.modelTemperature ?? 0,
-				messages: [{ role: "user", content: prompt }],
+			let { id, info, temperature, maxTokens, thinking } = this.getModel()
+			const useCache = info.supportsPromptCache
+
+			const params = {
+				model: id,
+				max_tokens: maxTokens,
+				temperature,
+				thinking,
+				system: "", // No system prompt needed for single completions
+				messages: [
+					{
+						role: "user",
+						content: useCache
+							? [
+									{
+										type: "text" as const,
+										text: prompt,
+										cache_control: { type: "ephemeral" },
+									},
+								]
+							: prompt,
+					},
+				],
 				stream: false,
 				stream: false,
-			})
+			}
+
+			const response = (await this.client.messages.create(
+				params as Anthropic.Messages.MessageCreateParamsNonStreaming,
+			)) as unknown as VertexMessageResponse
 
 
 			const content = response.content[0]
 			const content = response.content[0]
 			if (content.type === "text") {
 			if (content.type === "text") {

+ 23 - 13
src/core/Cline.ts

@@ -93,6 +93,7 @@ export class Cline {
 	// a flag that indicated if this Cline instance is paused (waiting for provider to resume it after subtask completion)
 	// a flag that indicated if this Cline instance is paused (waiting for provider to resume it after subtask completion)
 	private isPaused: boolean = false
 	private isPaused: boolean = false
 	private pausedModeSlug: string = defaultModeSlug
 	private pausedModeSlug: string = defaultModeSlug
+	readonly apiConfiguration: ApiConfiguration
 	api: ApiHandler
 	api: ApiHandler
 	private terminalManager: TerminalManager
 	private terminalManager: TerminalManager
 	private urlContentFetcher: UrlContentFetcher
 	private urlContentFetcher: UrlContentFetcher
@@ -120,7 +121,7 @@ export class Cline {
 	isInitialized = false
 	isInitialized = false
 
 
 	// checkpoints
 	// checkpoints
-	checkpointsEnabled: boolean = false
+	enableCheckpoints: boolean = false
 	private checkpointService?: CheckpointService
 	private checkpointService?: CheckpointService
 
 
 	// streaming
 	// streaming
@@ -155,6 +156,7 @@ export class Cline {
 
 
 		this.taskId = crypto.randomUUID()
 		this.taskId = crypto.randomUUID()
 		this.taskNumber = -1
 		this.taskNumber = -1
+		this.apiConfiguration = apiConfiguration
 		this.api = buildApiHandler(apiConfiguration)
 		this.api = buildApiHandler(apiConfiguration)
 		this.terminalManager = new TerminalManager()
 		this.terminalManager = new TerminalManager()
 		this.urlContentFetcher = new UrlContentFetcher(provider.context)
 		this.urlContentFetcher = new UrlContentFetcher(provider.context)
@@ -164,7 +166,7 @@ export class Cline {
 		this.fuzzyMatchThreshold = fuzzyMatchThreshold ?? 1.0
 		this.fuzzyMatchThreshold = fuzzyMatchThreshold ?? 1.0
 		this.providerRef = new WeakRef(provider)
 		this.providerRef = new WeakRef(provider)
 		this.diffViewProvider = new DiffViewProvider(cwd)
 		this.diffViewProvider = new DiffViewProvider(cwd)
-		this.checkpointsEnabled = enableCheckpoints ?? false
+		this.enableCheckpoints = enableCheckpoints ?? false
 
 
 		if (historyItem) {
 		if (historyItem) {
 			this.taskId = historyItem.id
 			this.taskId = historyItem.id
@@ -1018,13 +1020,21 @@ export class Cline {
 				cacheWrites = 0,
 				cacheWrites = 0,
 				cacheReads = 0,
 				cacheReads = 0,
 			}: ClineApiReqInfo = JSON.parse(previousRequest)
 			}: ClineApiReqInfo = JSON.parse(previousRequest)
+
 			const totalTokens = tokensIn + tokensOut + cacheWrites + cacheReads
 			const totalTokens = tokensIn + tokensOut + cacheWrites + cacheReads
 
 
-			const trimmedMessages = truncateConversationIfNeeded(
-				this.apiConversationHistory,
+			const modelInfo = this.api.getModel().info
+			const maxTokens = modelInfo.thinking
+				? this.apiConfiguration.modelMaxTokens || modelInfo.maxTokens
+				: modelInfo.maxTokens
+			const contextWindow = modelInfo.contextWindow
+
+			const trimmedMessages = truncateConversationIfNeeded({
+				messages: this.apiConversationHistory,
 				totalTokens,
 				totalTokens,
-				this.api.getModel().info,
-			)
+				maxTokens,
+				contextWindow,
+			})
 
 
 			if (trimmedMessages !== this.apiConversationHistory) {
 			if (trimmedMessages !== this.apiConversationHistory) {
 				await this.overwriteApiConversationHistory(trimmedMessages)
 				await this.overwriteApiConversationHistory(trimmedMessages)
@@ -3436,7 +3446,7 @@ export class Cline {
 	// Checkpoints
 	// Checkpoints
 
 
 	private async getCheckpointService() {
 	private async getCheckpointService() {
-		if (!this.checkpointsEnabled) {
+		if (!this.enableCheckpoints) {
 			throw new Error("Checkpoints are disabled")
 			throw new Error("Checkpoints are disabled")
 		}
 		}
 
 
@@ -3477,7 +3487,7 @@ export class Cline {
 		commitHash: string
 		commitHash: string
 		mode: "full" | "checkpoint"
 		mode: "full" | "checkpoint"
 	}) {
 	}) {
-		if (!this.checkpointsEnabled) {
+		if (!this.enableCheckpoints) {
 			return
 			return
 		}
 		}
 
 
@@ -3516,12 +3526,12 @@ export class Cline {
 			)
 			)
 		} catch (err) {
 		} catch (err) {
 			this.providerRef.deref()?.log("[checkpointDiff] disabling checkpoints for this task")
 			this.providerRef.deref()?.log("[checkpointDiff] disabling checkpoints for this task")
-			this.checkpointsEnabled = false
+			this.enableCheckpoints = false
 		}
 		}
 	}
 	}
 
 
 	public async checkpointSave({ isFirst }: { isFirst: boolean }) {
 	public async checkpointSave({ isFirst }: { isFirst: boolean }) {
-		if (!this.checkpointsEnabled) {
+		if (!this.enableCheckpoints) {
 			return
 			return
 		}
 		}
 
 
@@ -3542,7 +3552,7 @@ export class Cline {
 			}
 			}
 		} catch (err) {
 		} catch (err) {
 			this.providerRef.deref()?.log("[checkpointSave] disabling checkpoints for this task")
 			this.providerRef.deref()?.log("[checkpointSave] disabling checkpoints for this task")
-			this.checkpointsEnabled = false
+			this.enableCheckpoints = false
 		}
 		}
 	}
 	}
 
 
@@ -3555,7 +3565,7 @@ export class Cline {
 		commitHash: string
 		commitHash: string
 		mode: "preview" | "restore"
 		mode: "preview" | "restore"
 	}) {
 	}) {
-		if (!this.checkpointsEnabled) {
+		if (!this.enableCheckpoints) {
 			return
 			return
 		}
 		}
 
 
@@ -3610,7 +3620,7 @@ export class Cline {
 			this.providerRef.deref()?.cancelTask()
 			this.providerRef.deref()?.cancelTask()
 		} catch (err) {
 		} catch (err) {
 			this.providerRef.deref()?.log("[checkpointRestore] disabling checkpoints for this task")
 			this.providerRef.deref()?.log("[checkpointRestore] disabling checkpoints for this task")
-			this.checkpointsEnabled = false
+			this.enableCheckpoints = false
 		}
 		}
 	}
 	}
 }
 }

+ 172 - 0
src/core/prompts/__tests__/custom-system-prompt.test.ts

@@ -0,0 +1,172 @@
+import { SYSTEM_PROMPT } from "../system"
+import { defaultModeSlug, modes } from "../../../shared/modes"
+import * as vscode from "vscode"
+import * as fs from "fs/promises"
+
+// Mock the fs/promises module
+jest.mock("fs/promises", () => ({
+	readFile: jest.fn(),
+	mkdir: jest.fn().mockResolvedValue(undefined),
+	access: jest.fn().mockResolvedValue(undefined),
+}))
+
+// Get the mocked fs module
+const mockedFs = fs as jest.Mocked<typeof fs>
+
+// Mock the fileExistsAtPath function
+jest.mock("../../../utils/fs", () => ({
+	fileExistsAtPath: jest.fn().mockResolvedValue(true),
+	createDirectoriesForFile: jest.fn().mockResolvedValue([]),
+}))
+
+// Create a mock ExtensionContext with relative paths instead of absolute paths
+const mockContext = {
+	extensionPath: "mock/extension/path",
+	globalStoragePath: "mock/storage/path",
+	storagePath: "mock/storage/path",
+	logPath: "mock/log/path",
+	subscriptions: [],
+	workspaceState: {
+		get: () => undefined,
+		update: () => Promise.resolve(),
+	},
+	globalState: {
+		get: () => undefined,
+		update: () => Promise.resolve(),
+		setKeysForSync: () => {},
+	},
+	extensionUri: { fsPath: "mock/extension/path" },
+	globalStorageUri: { fsPath: "mock/settings/path" },
+	asAbsolutePath: (relativePath: string) => `mock/extension/path/${relativePath}`,
+	extension: {
+		packageJSON: {
+			version: "1.0.0",
+		},
+	},
+} as unknown as vscode.ExtensionContext
+
+describe("File-Based Custom System Prompt", () => {
+	const experiments = {}
+
+	beforeEach(() => {
+		// Reset mocks before each test
+		jest.clearAllMocks()
+
+		// Default behavior: file doesn't exist
+		mockedFs.readFile.mockRejectedValue({ code: "ENOENT" })
+	})
+
+	it("should use default generation when no file-based system prompt is found", async () => {
+		const customModePrompts = {
+			[defaultModeSlug]: {
+				roleDefinition: "Test role definition",
+			},
+		}
+
+		const prompt = await SYSTEM_PROMPT(
+			mockContext,
+			"test/path", // Using a relative path without leading slash
+			false,
+			undefined,
+			undefined,
+			undefined,
+			defaultModeSlug,
+			customModePrompts,
+			undefined,
+			undefined,
+			undefined,
+			undefined,
+			experiments,
+			true,
+		)
+
+		// Should contain default sections
+		expect(prompt).toContain("TOOL USE")
+		expect(prompt).toContain("CAPABILITIES")
+		expect(prompt).toContain("MODES")
+		expect(prompt).toContain("Test role definition")
+	})
+
+	it("should use file-based custom system prompt when available", async () => {
+		// Mock the readFile to return content from a file
+		const fileCustomSystemPrompt = "Custom system prompt from file"
+		// When called with utf-8 encoding, return a string
+		mockedFs.readFile.mockImplementation((filePath, options) => {
+			if (filePath.toString().includes(`.roo/system-prompt-${defaultModeSlug}`) && options === "utf-8") {
+				return Promise.resolve(fileCustomSystemPrompt)
+			}
+			return Promise.reject({ code: "ENOENT" })
+		})
+
+		const prompt = await SYSTEM_PROMPT(
+			mockContext,
+			"test/path", // Using a relative path without leading slash
+			false,
+			undefined,
+			undefined,
+			undefined,
+			defaultModeSlug,
+			undefined,
+			undefined,
+			undefined,
+			undefined,
+			undefined,
+			experiments,
+			true,
+		)
+
+		// Should contain role definition and file-based system prompt
+		expect(prompt).toContain(modes[0].roleDefinition)
+		expect(prompt).toContain(fileCustomSystemPrompt)
+
+		// Should not contain any of the default sections
+		expect(prompt).not.toContain("TOOL USE")
+		expect(prompt).not.toContain("CAPABILITIES")
+		expect(prompt).not.toContain("MODES")
+	})
+
+	it("should combine file-based system prompt with role definition and custom instructions", async () => {
+		// Mock the readFile to return content from a file
+		const fileCustomSystemPrompt = "Custom system prompt from file"
+		mockedFs.readFile.mockImplementation((filePath, options) => {
+			if (filePath.toString().includes(`.roo/system-prompt-${defaultModeSlug}`) && options === "utf-8") {
+				return Promise.resolve(fileCustomSystemPrompt)
+			}
+			return Promise.reject({ code: "ENOENT" })
+		})
+
+		// Define custom role definition
+		const customRoleDefinition = "Custom role definition"
+		const customModePrompts = {
+			[defaultModeSlug]: {
+				roleDefinition: customRoleDefinition,
+			},
+		}
+
+		const prompt = await SYSTEM_PROMPT(
+			mockContext,
+			"test/path", // Using a relative path without leading slash
+			false,
+			undefined,
+			undefined,
+			undefined,
+			defaultModeSlug,
+			customModePrompts,
+			undefined,
+			undefined,
+			undefined,
+			undefined,
+			experiments,
+			true,
+		)
+
+		// Should contain custom role definition and file-based system prompt
+		expect(prompt).toContain(customRoleDefinition)
+		expect(prompt).toContain(fileCustomSystemPrompt)
+
+		// Should not contain any of the default sections
+		expect(prompt).not.toContain("TOOL USE")
+		expect(prompt).not.toContain("CAPABILITIES")
+		expect(prompt).not.toContain("MODES")
+	})
+})

+ 60 - 0
src/core/prompts/sections/custom-system-prompt.ts

@@ -0,0 +1,60 @@
+import fs from "fs/promises"
+import path from "path"
+import { Mode } from "../../../shared/modes"
+import { fileExistsAtPath } from "../../../utils/fs"
+
+/**
+ * Safely reads a file, returning an empty string if the file doesn't exist
+ */
+async function safeReadFile(filePath: string): Promise<string> {
+	try {
+		const content = await fs.readFile(filePath, "utf-8")
+		// When reading with "utf-8" encoding, content should be a string
+		return content.trim()
+	} catch (err) {
+		const errorCode = (err as NodeJS.ErrnoException).code
+		if (!errorCode || !["ENOENT", "EISDIR"].includes(errorCode)) {
+			throw err
+		}
+		return ""
+	}
+}
+
+/**
+ * Get the path to a system prompt file for a specific mode
+ */
+export function getSystemPromptFilePath(cwd: string, mode: Mode): string {
+	return path.join(cwd, ".roo", `system-prompt-${mode}`)
+}
+
+/**
+ * Loads custom system prompt from a file at .roo/system-prompt-[mode slug]
+ * If the file doesn't exist, returns an empty string
+ */
+export async function loadSystemPromptFile(cwd: string, mode: Mode): Promise<string> {
+	const filePath = getSystemPromptFilePath(cwd, mode)
+	return safeReadFile(filePath)
+}
+
+/**
+ * Ensures the .roo directory exists, creating it if necessary
+ */
+export async function ensureRooDirectory(cwd: string): Promise<void> {
+	const rooDir = path.join(cwd, ".roo")
+
+	// Check if directory already exists
+	if (await fileExistsAtPath(rooDir)) {
+		return
+	}
+
+	// Create the directory
+	try {
+		await fs.mkdir(rooDir, { recursive: true })
+	} catch (err) {
+		// If directory already exists (race condition), ignore the error
+		const errorCode = (err as NodeJS.ErrnoException).code
+		if (errorCode !== "EEXIST") {
+			throw err
+		}
+	}
+}

+ 15 - 0
src/core/prompts/system.ts

@@ -23,6 +23,7 @@ import {
 	getModesSection,
 	getModesSection,
 	addCustomInstructions,
 	addCustomInstructions,
 } from "./sections"
 } from "./sections"
+import { loadSystemPromptFile } from "./sections/custom-system-prompt"
 import fs from "fs/promises"
 import fs from "fs/promises"
 import path from "path"
 import path from "path"
 
 
@@ -119,11 +120,25 @@ export const SYSTEM_PROMPT = async (
 		return undefined
 		return undefined
 	}
 	}
 
 
+	// Try to load custom system prompt from file
+	const fileCustomSystemPrompt = await loadSystemPromptFile(cwd, mode)
+
 	// Check if it's a custom mode
 	// Check if it's a custom mode
 	const promptComponent = getPromptComponent(customModePrompts?.[mode])
 	const promptComponent = getPromptComponent(customModePrompts?.[mode])
+
 	// Get full mode config from custom modes or fall back to built-in modes
 	// Get full mode config from custom modes or fall back to built-in modes
 	const currentMode = getModeBySlug(mode, customModes) || modes.find((m) => m.slug === mode) || modes[0]
 	const currentMode = getModeBySlug(mode, customModes) || modes.find((m) => m.slug === mode) || modes[0]
 
 
+	// If a file-based custom system prompt exists, use it
+	if (fileCustomSystemPrompt) {
+		const roleDefinition = promptComponent?.roleDefinition || currentMode.roleDefinition
+		return `${roleDefinition}
+
+${fileCustomSystemPrompt}
+
+${await addCustomInstructions(promptComponent?.customInstructions || currentMode.customInstructions || "", globalCustomInstructions || "", cwd, mode, { preferredLanguage })}`
+	}
+
 	// If diff is disabled, don't pass the diffStrategy
 	// If diff is disabled, don't pass the diffStrategy
 	const effectiveDiffStrategy = diffEnabled ? diffStrategy : undefined
 	const effectiveDiffStrategy = diffEnabled ? diffStrategy : undefined
 
 

+ 88 - 14
src/core/sliding-window/__tests__/sliding-window.test.ts

@@ -119,11 +119,21 @@ describe("getMaxTokens", () => {
 		// Max tokens = 100000 - 50000 = 50000
 		// Max tokens = 100000 - 50000 = 50000
 
 
 		// Below max tokens - no truncation
 		// Below max tokens - no truncation
-		const result1 = truncateConversationIfNeeded(messages, 49999, modelInfo)
+		const result1 = truncateConversationIfNeeded({
+			messages,
+			totalTokens: 49999,
+			contextWindow: modelInfo.contextWindow,
+			maxTokens: modelInfo.maxTokens,
+		})
 		expect(result1).toEqual(messages)
 		expect(result1).toEqual(messages)
 
 
 		// Above max tokens - truncate
 		// Above max tokens - truncate
-		const result2 = truncateConversationIfNeeded(messages, 50001, modelInfo)
+		const result2 = truncateConversationIfNeeded({
+			messages,
+			totalTokens: 50001,
+			contextWindow: modelInfo.contextWindow,
+			maxTokens: modelInfo.maxTokens,
+		})
 		expect(result2).not.toEqual(messages)
 		expect(result2).not.toEqual(messages)
 		expect(result2.length).toBe(3) // Truncated with 0.5 fraction
 		expect(result2.length).toBe(3) // Truncated with 0.5 fraction
 	})
 	})
@@ -133,11 +143,21 @@ describe("getMaxTokens", () => {
 		// Max tokens = 100000 - (100000 * 0.2) = 80000
 		// Max tokens = 100000 - (100000 * 0.2) = 80000
 
 
 		// Below max tokens - no truncation
 		// Below max tokens - no truncation
-		const result1 = truncateConversationIfNeeded(messages, 79999, modelInfo)
+		const result1 = truncateConversationIfNeeded({
+			messages,
+			totalTokens: 79999,
+			contextWindow: modelInfo.contextWindow,
+			maxTokens: modelInfo.maxTokens,
+		})
 		expect(result1).toEqual(messages)
 		expect(result1).toEqual(messages)
 
 
 		// Above max tokens - truncate
 		// Above max tokens - truncate
-		const result2 = truncateConversationIfNeeded(messages, 80001, modelInfo)
+		const result2 = truncateConversationIfNeeded({
+			messages,
+			totalTokens: 80001,
+			contextWindow: modelInfo.contextWindow,
+			maxTokens: modelInfo.maxTokens,
+		})
 		expect(result2).not.toEqual(messages)
 		expect(result2).not.toEqual(messages)
 		expect(result2.length).toBe(3) // Truncated with 0.5 fraction
 		expect(result2.length).toBe(3) // Truncated with 0.5 fraction
 	})
 	})
@@ -147,11 +167,21 @@ describe("getMaxTokens", () => {
 		// Max tokens = 50000 - 10000 = 40000
 		// Max tokens = 50000 - 10000 = 40000
 
 
 		// Below max tokens - no truncation
 		// Below max tokens - no truncation
-		const result1 = truncateConversationIfNeeded(messages, 39999, modelInfo)
+		const result1 = truncateConversationIfNeeded({
+			messages,
+			totalTokens: 39999,
+			contextWindow: modelInfo.contextWindow,
+			maxTokens: modelInfo.maxTokens,
+		})
 		expect(result1).toEqual(messages)
 		expect(result1).toEqual(messages)
 
 
 		// Above max tokens - truncate
 		// Above max tokens - truncate
-		const result2 = truncateConversationIfNeeded(messages, 40001, modelInfo)
+		const result2 = truncateConversationIfNeeded({
+			messages,
+			totalTokens: 40001,
+			contextWindow: modelInfo.contextWindow,
+			maxTokens: modelInfo.maxTokens,
+		})
 		expect(result2).not.toEqual(messages)
 		expect(result2).not.toEqual(messages)
 		expect(result2.length).toBe(3) // Truncated with 0.5 fraction
 		expect(result2.length).toBe(3) // Truncated with 0.5 fraction
 	})
 	})
@@ -161,11 +191,21 @@ describe("getMaxTokens", () => {
 		// Max tokens = 200000 - 30000 = 170000
 		// Max tokens = 200000 - 30000 = 170000
 
 
 		// Below max tokens - no truncation
 		// Below max tokens - no truncation
-		const result1 = truncateConversationIfNeeded(messages, 169999, modelInfo)
+		const result1 = truncateConversationIfNeeded({
+			messages,
+			totalTokens: 169999,
+			contextWindow: modelInfo.contextWindow,
+			maxTokens: modelInfo.maxTokens,
+		})
 		expect(result1).toEqual(messages)
 		expect(result1).toEqual(messages)
 
 
 		// Above max tokens - truncate
 		// Above max tokens - truncate
-		const result2 = truncateConversationIfNeeded(messages, 170001, modelInfo)
+		const result2 = truncateConversationIfNeeded({
+			messages,
+			totalTokens: 170001,
+			contextWindow: modelInfo.contextWindow,
+			maxTokens: modelInfo.maxTokens,
+		})
 		expect(result2).not.toEqual(messages)
 		expect(result2).not.toEqual(messages)
 		expect(result2.length).toBe(3) // Truncated with 0.5 fraction
 		expect(result2.length).toBe(3) // Truncated with 0.5 fraction
 	})
 	})
@@ -194,7 +234,12 @@ describe("truncateConversationIfNeeded", () => {
 		const maxTokens = 100000 - 30000 // 70000
 		const maxTokens = 100000 - 30000 // 70000
 		const totalTokens = 69999 // Below threshold
 		const totalTokens = 69999 // Below threshold
 
 
-		const result = truncateConversationIfNeeded(messages, totalTokens, modelInfo)
+		const result = truncateConversationIfNeeded({
+			messages,
+			totalTokens,
+			contextWindow: modelInfo.contextWindow,
+			maxTokens: modelInfo.maxTokens,
+		})
 		expect(result).toEqual(messages) // No truncation occurs
 		expect(result).toEqual(messages) // No truncation occurs
 	})
 	})
 
 
@@ -207,7 +252,12 @@ describe("truncateConversationIfNeeded", () => {
 		// With 4 messages after the first, 0.5 fraction means remove 2 messages
 		// With 4 messages after the first, 0.5 fraction means remove 2 messages
 		const expectedResult = [messages[0], messages[3], messages[4]]
 		const expectedResult = [messages[0], messages[3], messages[4]]
 
 
-		const result = truncateConversationIfNeeded(messages, totalTokens, modelInfo)
+		const result = truncateConversationIfNeeded({
+			messages,
+			totalTokens,
+			contextWindow: modelInfo.contextWindow,
+			maxTokens: modelInfo.maxTokens,
+		})
 		expect(result).toEqual(expectedResult)
 		expect(result).toEqual(expectedResult)
 	})
 	})
 
 
@@ -218,14 +268,38 @@ describe("truncateConversationIfNeeded", () => {
 
 
 		// Test below threshold
 		// Test below threshold
 		const belowThreshold = 69999
 		const belowThreshold = 69999
-		expect(truncateConversationIfNeeded(messages, belowThreshold, modelInfo1)).toEqual(
-			truncateConversationIfNeeded(messages, belowThreshold, modelInfo2),
+		expect(
+			truncateConversationIfNeeded({
+				messages,
+				totalTokens: belowThreshold,
+				contextWindow: modelInfo1.contextWindow,
+				maxTokens: modelInfo1.maxTokens,
+			}),
+		).toEqual(
+			truncateConversationIfNeeded({
+				messages,
+				totalTokens: belowThreshold,
+				contextWindow: modelInfo2.contextWindow,
+				maxTokens: modelInfo2.maxTokens,
+			}),
 		)
 		)
 
 
 		// Test above threshold
 		// Test above threshold
 		const aboveThreshold = 70001
 		const aboveThreshold = 70001
-		expect(truncateConversationIfNeeded(messages, aboveThreshold, modelInfo1)).toEqual(
-			truncateConversationIfNeeded(messages, aboveThreshold, modelInfo2),
+		expect(
+			truncateConversationIfNeeded({
+				messages,
+				totalTokens: aboveThreshold,
+				contextWindow: modelInfo1.contextWindow,
+				maxTokens: modelInfo1.maxTokens,
+			}),
+		).toEqual(
+			truncateConversationIfNeeded({
+				messages,
+				totalTokens: aboveThreshold,
+				contextWindow: modelInfo2.contextWindow,
+				maxTokens: modelInfo2.maxTokens,
+			}),
 		)
 		)
 	})
 	})
 })
 })

+ 18 - 19
src/core/sliding-window/index.ts

@@ -1,7 +1,5 @@
 import { Anthropic } from "@anthropic-ai/sdk"
 import { Anthropic } from "@anthropic-ai/sdk"
 
 
-import { ModelInfo } from "../../shared/api"
-
 /**
 /**
  * Truncates a conversation by removing a fraction of the messages.
  * Truncates a conversation by removing a fraction of the messages.
  *
  *
@@ -26,28 +24,29 @@ export function truncateConversation(
 }
 }
 
 
 /**
 /**
- * Conditionally truncates the conversation messages if the total token count exceeds the model's limit.
+ * Conditionally truncates the conversation messages if the total token count
+ * exceeds the model's limit.
  *
  *
  * @param {Anthropic.Messages.MessageParam[]} messages - The conversation messages.
  * @param {Anthropic.Messages.MessageParam[]} messages - The conversation messages.
  * @param {number} totalTokens - The total number of tokens in the conversation.
  * @param {number} totalTokens - The total number of tokens in the conversation.
- * @param {ModelInfo} modelInfo - Model metadata including context window size.
+ * @param {number} contextWindow - The context window size.
+ * @param {number} maxTokens - The maximum number of tokens allowed.
  * @returns {Anthropic.Messages.MessageParam[]} The original or truncated conversation messages.
  * @returns {Anthropic.Messages.MessageParam[]} The original or truncated conversation messages.
  */
  */
-export function truncateConversationIfNeeded(
-	messages: Anthropic.Messages.MessageParam[],
-	totalTokens: number,
-	modelInfo: ModelInfo,
-): Anthropic.Messages.MessageParam[] {
-	return totalTokens < getMaxTokens(modelInfo) ? messages : truncateConversation(messages, 0.5)
+
+type TruncateOptions = {
+	messages: Anthropic.Messages.MessageParam[]
+	totalTokens: number
+	contextWindow: number
+	maxTokens?: number
 }
 }
 
 
-/**
- * Calculates the maximum allowed tokens
- *
- * @param {ModelInfo} modelInfo - The model information containing the context window size.
- * @returns {number} The maximum number of tokens allowed
- */
-function getMaxTokens(modelInfo: ModelInfo): number {
-	// The buffer needs to be at least as large as `modelInfo.maxTokens`, or 20% of the context window if for some reason it's not set.
-	return modelInfo.contextWindow - (modelInfo.maxTokens || modelInfo.contextWindow * 0.2)
+export function truncateConversationIfNeeded({
+	messages,
+	totalTokens,
+	contextWindow,
+	maxTokens,
+}: TruncateOptions): Anthropic.Messages.MessageParam[] {
+	const allowedTokens = contextWindow - (maxTokens || contextWindow * 0.2)
+	return totalTokens < allowedTokens ? messages : truncateConversation(messages, 0.5)
 }
 }

+ 26 - 20
src/core/webview/ClineProvider.ts

@@ -64,7 +64,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 	private clineStack: Cline[] = []
 	private clineStack: Cline[] = []
 	private workspaceTracker?: WorkspaceTracker
 	private workspaceTracker?: WorkspaceTracker
 	protected mcpHub?: McpHub // Change from private to protected
 	protected mcpHub?: McpHub // Change from private to protected
-	private latestAnnouncementId = "jan-21-2025-custom-modes" // update to some unique identifier when we add a new announcement
+	private latestAnnouncementId = "feb-27-2025-automatic-checkpoints" // update to some unique identifier when we add a new announcement
 	configManager: ConfigManager
 	configManager: ConfigManager
 	customModesManager: CustomModesManager
 	customModesManager: CustomModesManager
 	private lastTaskNumber = -1
 	private lastTaskNumber = -1
@@ -402,7 +402,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			apiConfiguration,
 			apiConfiguration,
 			customModePrompts,
 			customModePrompts,
 			diffEnabled,
 			diffEnabled,
-			checkpointsEnabled,
+			enableCheckpoints,
 			fuzzyMatchThreshold,
 			fuzzyMatchThreshold,
 			mode,
 			mode,
 			customInstructions: globalInstructions,
 			customInstructions: globalInstructions,
@@ -417,7 +417,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			apiConfiguration,
 			apiConfiguration,
 			customInstructions: effectiveInstructions,
 			customInstructions: effectiveInstructions,
 			enableDiff: diffEnabled,
 			enableDiff: diffEnabled,
-			enableCheckpoints: checkpointsEnabled,
+			enableCheckpoints,
 			fuzzyMatchThreshold,
 			fuzzyMatchThreshold,
 			task,
 			task,
 			images,
 			images,
@@ -433,7 +433,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			apiConfiguration,
 			apiConfiguration,
 			customModePrompts,
 			customModePrompts,
 			diffEnabled,
 			diffEnabled,
-			checkpointsEnabled,
+			enableCheckpoints,
 			fuzzyMatchThreshold,
 			fuzzyMatchThreshold,
 			mode,
 			mode,
 			customInstructions: globalInstructions,
 			customInstructions: globalInstructions,
@@ -448,7 +448,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			apiConfiguration,
 			apiConfiguration,
 			customInstructions: effectiveInstructions,
 			customInstructions: effectiveInstructions,
 			enableDiff: diffEnabled,
 			enableDiff: diffEnabled,
-			enableCheckpoints: checkpointsEnabled,
+			enableCheckpoints,
 			fuzzyMatchThreshold,
 			fuzzyMatchThreshold,
 			historyItem,
 			historyItem,
 			experiments,
 			experiments,
@@ -1110,9 +1110,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 						await this.updateGlobalState("diffEnabled", diffEnabled)
 						await this.updateGlobalState("diffEnabled", diffEnabled)
 						await this.postStateToWebview()
 						await this.postStateToWebview()
 						break
 						break
-					case "checkpointsEnabled":
-						const checkpointsEnabled = message.bool ?? false
-						await this.updateGlobalState("checkpointsEnabled", checkpointsEnabled)
+					case "enableCheckpoints":
+						const enableCheckpoints = message.bool ?? true
+						await this.updateGlobalState("enableCheckpoints", enableCheckpoints)
 						await this.postStateToWebview()
 						await this.postStateToWebview()
 						break
 						break
 					case "browserViewportSize":
 					case "browserViewportSize":
@@ -1751,7 +1751,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			lmStudioModelId,
 			lmStudioModelId,
 			lmStudioBaseUrl,
 			lmStudioBaseUrl,
 			anthropicBaseUrl,
 			anthropicBaseUrl,
-			anthropicThinking,
 			geminiApiKey,
 			geminiApiKey,
 			openAiNativeApiKey,
 			openAiNativeApiKey,
 			deepSeekApiKey,
 			deepSeekApiKey,
@@ -1771,6 +1770,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			requestyModelId,
 			requestyModelId,
 			requestyModelInfo,
 			requestyModelInfo,
 			modelTemperature,
 			modelTemperature,
+			modelMaxTokens,
+			modelMaxThinkingTokens,
 		} = apiConfiguration
 		} = apiConfiguration
 		await Promise.all([
 		await Promise.all([
 			this.updateGlobalState("apiProvider", apiProvider),
 			this.updateGlobalState("apiProvider", apiProvider),
@@ -1799,7 +1800,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			this.updateGlobalState("lmStudioModelId", lmStudioModelId),
 			this.updateGlobalState("lmStudioModelId", lmStudioModelId),
 			this.updateGlobalState("lmStudioBaseUrl", lmStudioBaseUrl),
 			this.updateGlobalState("lmStudioBaseUrl", lmStudioBaseUrl),
 			this.updateGlobalState("anthropicBaseUrl", anthropicBaseUrl),
 			this.updateGlobalState("anthropicBaseUrl", anthropicBaseUrl),
-			this.updateGlobalState("anthropicThinking", anthropicThinking),
 			this.storeSecret("geminiApiKey", geminiApiKey),
 			this.storeSecret("geminiApiKey", geminiApiKey),
 			this.storeSecret("openAiNativeApiKey", openAiNativeApiKey),
 			this.storeSecret("openAiNativeApiKey", openAiNativeApiKey),
 			this.storeSecret("deepSeekApiKey", deepSeekApiKey),
 			this.storeSecret("deepSeekApiKey", deepSeekApiKey),
@@ -1819,6 +1819,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			this.updateGlobalState("requestyModelId", requestyModelId),
 			this.updateGlobalState("requestyModelId", requestyModelId),
 			this.updateGlobalState("requestyModelInfo", requestyModelInfo),
 			this.updateGlobalState("requestyModelInfo", requestyModelInfo),
 			this.updateGlobalState("modelTemperature", modelTemperature),
 			this.updateGlobalState("modelTemperature", modelTemperature),
+			this.updateGlobalState("modelMaxTokens", modelMaxTokens),
+			this.updateGlobalState("anthropicThinking", modelMaxThinkingTokens),
 		])
 		])
 		if (this.getCurrentCline()) {
 		if (this.getCurrentCline()) {
 			this.getCurrentCline()!.api = buildApiHandler(apiConfiguration)
 			this.getCurrentCline()!.api = buildApiHandler(apiConfiguration)
@@ -2023,12 +2025,13 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 		await this.deleteTaskFromState(id)
 		await this.deleteTaskFromState(id)
 
 
 		// check if checkpoints are enabled
 		// check if checkpoints are enabled
-		const { checkpointsEnabled } = await this.getState()
+		const { enableCheckpoints } = await this.getState()
 		// get the base directory of the project
 		// get the base directory of the project
 		const baseDir = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
 		const baseDir = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
 
 
 		// delete checkpoints branch from project git repo
 		// delete checkpoints branch from project git repo
-		if (checkpointsEnabled && baseDir) {
+		if (enableCheckpoints && baseDir) {
+
 			const branchSummary = await simpleGit(baseDir)
 			const branchSummary = await simpleGit(baseDir)
 				.branch(["-D", `roo-code-checkpoints-${id}`])
 				.branch(["-D", `roo-code-checkpoints-${id}`])
 				.catch(() => undefined)
 				.catch(() => undefined)
@@ -2077,7 +2080,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			alwaysAllowModeSwitch,
 			alwaysAllowModeSwitch,
 			soundEnabled,
 			soundEnabled,
 			diffEnabled,
 			diffEnabled,
-			checkpointsEnabled,
+			enableCheckpoints,
 			taskHistory,
 			taskHistory,
 			soundVolume,
 			soundVolume,
 			browserViewportSize,
 			browserViewportSize,
@@ -2126,7 +2129,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 				.sort((a: HistoryItem, b: HistoryItem) => b.ts - a.ts),
 				.sort((a: HistoryItem, b: HistoryItem) => b.ts - a.ts),
 			soundEnabled: soundEnabled ?? false,
 			soundEnabled: soundEnabled ?? false,
 			diffEnabled: diffEnabled ?? true,
 			diffEnabled: diffEnabled ?? true,
-			checkpointsEnabled: checkpointsEnabled ?? false,
+			enableCheckpoints: enableCheckpoints ?? true,
 			shouldShowAnnouncement: lastShownAnnouncementId !== this.latestAnnouncementId,
 			shouldShowAnnouncement: lastShownAnnouncementId !== this.latestAnnouncementId,
 			allowedCommands,
 			allowedCommands,
 			soundVolume: soundVolume ?? 0.5,
 			soundVolume: soundVolume ?? 0.5,
@@ -2230,7 +2233,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			lmStudioModelId,
 			lmStudioModelId,
 			lmStudioBaseUrl,
 			lmStudioBaseUrl,
 			anthropicBaseUrl,
 			anthropicBaseUrl,
-			anthropicThinking,
 			geminiApiKey,
 			geminiApiKey,
 			openAiNativeApiKey,
 			openAiNativeApiKey,
 			deepSeekApiKey,
 			deepSeekApiKey,
@@ -2254,7 +2256,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			allowedCommands,
 			allowedCommands,
 			soundEnabled,
 			soundEnabled,
 			diffEnabled,
 			diffEnabled,
-			checkpointsEnabled,
+			enableCheckpoints,
 			soundVolume,
 			soundVolume,
 			browserViewportSize,
 			browserViewportSize,
 			fuzzyMatchThreshold,
 			fuzzyMatchThreshold,
@@ -2285,6 +2287,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			requestyModelId,
 			requestyModelId,
 			requestyModelInfo,
 			requestyModelInfo,
 			modelTemperature,
 			modelTemperature,
+			modelMaxTokens,
+			modelMaxThinkingTokens,
 			maxOpenTabsContext,
 			maxOpenTabsContext,
 		] = await Promise.all([
 		] = await Promise.all([
 			this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
 			this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
@@ -2313,7 +2317,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			this.getGlobalState("lmStudioModelId") as Promise<string | undefined>,
 			this.getGlobalState("lmStudioModelId") as Promise<string | undefined>,
 			this.getGlobalState("lmStudioBaseUrl") as Promise<string | undefined>,
 			this.getGlobalState("lmStudioBaseUrl") as Promise<string | undefined>,
 			this.getGlobalState("anthropicBaseUrl") as Promise<string | undefined>,
 			this.getGlobalState("anthropicBaseUrl") as Promise<string | undefined>,
-			this.getGlobalState("anthropicThinking") as Promise<number | undefined>,
 			this.getSecret("geminiApiKey") as Promise<string | undefined>,
 			this.getSecret("geminiApiKey") as Promise<string | undefined>,
 			this.getSecret("openAiNativeApiKey") as Promise<string | undefined>,
 			this.getSecret("openAiNativeApiKey") as Promise<string | undefined>,
 			this.getSecret("deepSeekApiKey") as Promise<string | undefined>,
 			this.getSecret("deepSeekApiKey") as Promise<string | undefined>,
@@ -2337,7 +2340,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			this.getGlobalState("allowedCommands") as Promise<string[] | undefined>,
 			this.getGlobalState("allowedCommands") as Promise<string[] | undefined>,
 			this.getGlobalState("soundEnabled") as Promise<boolean | undefined>,
 			this.getGlobalState("soundEnabled") as Promise<boolean | undefined>,
 			this.getGlobalState("diffEnabled") as Promise<boolean | undefined>,
 			this.getGlobalState("diffEnabled") as Promise<boolean | undefined>,
-			this.getGlobalState("checkpointsEnabled") as Promise<boolean | undefined>,
+			this.getGlobalState("enableCheckpoints") as Promise<boolean | undefined>,
 			this.getGlobalState("soundVolume") as Promise<number | undefined>,
 			this.getGlobalState("soundVolume") as Promise<number | undefined>,
 			this.getGlobalState("browserViewportSize") as Promise<string | undefined>,
 			this.getGlobalState("browserViewportSize") as Promise<string | undefined>,
 			this.getGlobalState("fuzzyMatchThreshold") as Promise<number | undefined>,
 			this.getGlobalState("fuzzyMatchThreshold") as Promise<number | undefined>,
@@ -2368,6 +2371,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			this.getGlobalState("requestyModelId") as Promise<string | undefined>,
 			this.getGlobalState("requestyModelId") as Promise<string | undefined>,
 			this.getGlobalState("requestyModelInfo") as Promise<ModelInfo | undefined>,
 			this.getGlobalState("requestyModelInfo") as Promise<ModelInfo | undefined>,
 			this.getGlobalState("modelTemperature") as Promise<number | undefined>,
 			this.getGlobalState("modelTemperature") as Promise<number | undefined>,
+			this.getGlobalState("modelMaxTokens") as Promise<number | undefined>,
+			this.getGlobalState("anthropicThinking") as Promise<number | undefined>,
 			this.getGlobalState("maxOpenTabsContext") as Promise<number | undefined>,
 			this.getGlobalState("maxOpenTabsContext") as Promise<number | undefined>,
 		])
 		])
 
 
@@ -2413,7 +2418,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 				lmStudioModelId,
 				lmStudioModelId,
 				lmStudioBaseUrl,
 				lmStudioBaseUrl,
 				anthropicBaseUrl,
 				anthropicBaseUrl,
-				anthropicThinking,
 				geminiApiKey,
 				geminiApiKey,
 				openAiNativeApiKey,
 				openAiNativeApiKey,
 				deepSeekApiKey,
 				deepSeekApiKey,
@@ -2433,6 +2437,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 				requestyModelId,
 				requestyModelId,
 				requestyModelInfo,
 				requestyModelInfo,
 				modelTemperature,
 				modelTemperature,
+				modelMaxTokens,
+				modelMaxThinkingTokens,
 			},
 			},
 			lastShownAnnouncementId,
 			lastShownAnnouncementId,
 			customInstructions,
 			customInstructions,
@@ -2446,7 +2452,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			allowedCommands,
 			allowedCommands,
 			soundEnabled: soundEnabled ?? false,
 			soundEnabled: soundEnabled ?? false,
 			diffEnabled: diffEnabled ?? true,
 			diffEnabled: diffEnabled ?? true,
-			checkpointsEnabled: checkpointsEnabled ?? false,
+			enableCheckpoints: enableCheckpoints ?? true,
 			soundVolume,
 			soundVolume,
 			browserViewportSize: browserViewportSize ?? "900x600",
 			browserViewportSize: browserViewportSize ?? "900x600",
 			screenshotQuality: screenshotQuality ?? 75,
 			screenshotQuality: screenshotQuality ?? 75,

+ 2 - 2
src/core/webview/__tests__/ClineProvider.test.ts

@@ -373,7 +373,7 @@ describe("ClineProvider", () => {
 			uriScheme: "vscode",
 			uriScheme: "vscode",
 			soundEnabled: false,
 			soundEnabled: false,
 			diffEnabled: false,
 			diffEnabled: false,
-			checkpointsEnabled: false,
+			enableCheckpoints: false,
 			writeDelayMs: 1000,
 			writeDelayMs: 1000,
 			browserViewportSize: "900x600",
 			browserViewportSize: "900x600",
 			fuzzyMatchThreshold: 1.0,
 			fuzzyMatchThreshold: 1.0,
@@ -712,7 +712,7 @@ describe("ClineProvider", () => {
 			},
 			},
 			mode: "code",
 			mode: "code",
 			diffEnabled: true,
 			diffEnabled: true,
-			checkpointsEnabled: false,
+			enableCheckpoints: false,
 			fuzzyMatchThreshold: 1.0,
 			fuzzyMatchThreshold: 1.0,
 			experiments: experimentDefault,
 			experiments: experimentDefault,
 		} as any)
 		} as any)

+ 51 - 0
src/services/ripgrep/__tests__/index.test.ts

@@ -0,0 +1,51 @@
+// npx jest src/services/ripgrep/__tests__/index.test.ts
+
+import { describe, expect, it } from "@jest/globals"
+import { truncateLine } from "../index"
+
+describe("Ripgrep line truncation", () => {
+	// The default MAX_LINE_LENGTH is 500 in the implementation
+	const MAX_LINE_LENGTH = 500
+
+	it("should truncate lines longer than MAX_LINE_LENGTH", () => {
+		const longLine = "a".repeat(600) // Line longer than MAX_LINE_LENGTH
+		const truncated = truncateLine(longLine)
+
+		expect(truncated).toContain("[truncated...]")
+		expect(truncated.length).toBeLessThan(longLine.length)
+		expect(truncated.length).toEqual(MAX_LINE_LENGTH + " [truncated...]".length)
+	})
+
+	it("should not truncate lines shorter than MAX_LINE_LENGTH", () => {
+		const shortLine = "Short line of text"
+		const truncated = truncateLine(shortLine)
+
+		expect(truncated).toEqual(shortLine)
+		expect(truncated).not.toContain("[truncated...]")
+	})
+
+	it("should correctly truncate a line at exactly MAX_LINE_LENGTH characters", () => {
+		const exactLine = "a".repeat(MAX_LINE_LENGTH)
+		const exactPlusOne = exactLine + "x"
+
+		// Should not truncate when exactly MAX_LINE_LENGTH
+		expect(truncateLine(exactLine)).toEqual(exactLine)
+
+		// Should truncate when exceeding MAX_LINE_LENGTH by even 1 character
+		expect(truncateLine(exactPlusOne)).toContain("[truncated...]")
+	})
+
+	it("should handle empty lines without errors", () => {
+		expect(truncateLine("")).toEqual("")
+	})
+
+	it("should allow custom maximum length", () => {
+		const customLength = 100
+		const line = "a".repeat(customLength + 50)
+
+		const truncated = truncateLine(line, customLength)
+
+		expect(truncated.length).toEqual(customLength + " [truncated...]".length)
+		expect(truncated).toContain("[truncated...]")
+	})
+})

+ 26 - 4
src/services/ripgrep/index.ts

@@ -58,7 +58,19 @@ interface SearchResult {
 	afterContext: string[]
 	afterContext: string[]
 }
 }
 
 
+// Constants
 const MAX_RESULTS = 300
 const MAX_RESULTS = 300
+const MAX_LINE_LENGTH = 500
+
+/**
+ * Truncates a line if it exceeds the maximum length
+ * @param line The line to truncate
+ * @param maxLength The maximum allowed length (defaults to MAX_LINE_LENGTH)
+ * @returns The truncated line, or the original line if it's shorter than maxLength
+ */
+export function truncateLine(line: string, maxLength: number = MAX_LINE_LENGTH): string {
+	return line.length > maxLength ? line.substring(0, maxLength) + " [truncated...]" : line
+}
 
 
 async function getBinPath(vscodeAppRoot: string): Promise<string | undefined> {
 async function getBinPath(vscodeAppRoot: string): Promise<string | undefined> {
 	const checkPath = async (pkgFolder: string) => {
 	const checkPath = async (pkgFolder: string) => {
@@ -140,7 +152,8 @@ export async function regexSearchFiles(
 	let output: string
 	let output: string
 	try {
 	try {
 		output = await execRipgrep(rgPath, args)
 		output = await execRipgrep(rgPath, args)
-	} catch {
+	} catch (error) {
+		console.error("Error executing ripgrep:", error)
 		return "No results found"
 		return "No results found"
 	}
 	}
 	const results: SearchResult[] = []
 	const results: SearchResult[] = []
@@ -154,19 +167,28 @@ export async function regexSearchFiles(
 					if (currentResult) {
 					if (currentResult) {
 						results.push(currentResult as SearchResult)
 						results.push(currentResult as SearchResult)
 					}
 					}
+
+					// Safety check: truncate extremely long lines to prevent excessive output
+					const matchText = parsed.data.lines.text
+					const truncatedMatch = truncateLine(matchText)
+
 					currentResult = {
 					currentResult = {
 						file: parsed.data.path.text,
 						file: parsed.data.path.text,
 						line: parsed.data.line_number,
 						line: parsed.data.line_number,
 						column: parsed.data.submatches[0].start,
 						column: parsed.data.submatches[0].start,
-						match: parsed.data.lines.text,
+						match: truncatedMatch,
 						beforeContext: [],
 						beforeContext: [],
 						afterContext: [],
 						afterContext: [],
 					}
 					}
 				} else if (parsed.type === "context" && currentResult) {
 				} else if (parsed.type === "context" && currentResult) {
+					// Apply the same truncation logic to context lines
+					const contextText = parsed.data.lines.text
+					const truncatedContext = truncateLine(contextText)
+
 					if (parsed.data.line_number < currentResult.line!) {
 					if (parsed.data.line_number < currentResult.line!) {
-						currentResult.beforeContext!.push(parsed.data.lines.text)
+						currentResult.beforeContext!.push(truncatedContext)
 					} else {
 					} else {
-						currentResult.afterContext!.push(parsed.data.lines.text)
+						currentResult.afterContext!.push(truncatedContext)
 					}
 					}
 				}
 				}
 			} catch (error) {
 			} catch (error) {

+ 1 - 1
src/shared/ExtensionMessage.ts

@@ -111,7 +111,7 @@ export interface ExtensionState {
 	soundEnabled?: boolean
 	soundEnabled?: boolean
 	soundVolume?: number
 	soundVolume?: number
 	diffEnabled?: boolean
 	diffEnabled?: boolean
-	checkpointsEnabled: boolean
+	enableCheckpoints: boolean
 	browserViewportSize?: string
 	browserViewportSize?: string
 	screenshotQuality?: number
 	screenshotQuality?: number
 	fuzzyMatchThreshold?: number
 	fuzzyMatchThreshold?: number

+ 1 - 1
src/shared/WebviewMessage.ts

@@ -52,7 +52,7 @@ export interface WebviewMessage {
 		| "soundEnabled"
 		| "soundEnabled"
 		| "soundVolume"
 		| "soundVolume"
 		| "diffEnabled"
 		| "diffEnabled"
-		| "checkpointsEnabled"
+		| "enableCheckpoints"
 		| "browserViewportSize"
 		| "browserViewportSize"
 		| "screenshotQuality"
 		| "screenshotQuality"
 		| "openMcpSettings"
 		| "openMcpSettings"

+ 1 - 1
src/shared/__tests__/checkExistApiConfig.test.ts

@@ -32,7 +32,7 @@ describe("checkExistKey", () => {
 			apiKey: "test-key",
 			apiKey: "test-key",
 			apiProvider: undefined,
 			apiProvider: undefined,
 			anthropicBaseUrl: undefined,
 			anthropicBaseUrl: undefined,
-			anthropicThinking: undefined,
+			modelMaxThinkingTokens: undefined,
 		}
 		}
 		expect(checkExistKey(config)).toBe(true)
 		expect(checkExistKey(config)).toBe(true)
 	})
 	})

+ 44 - 16
src/shared/api.ts

@@ -21,7 +21,6 @@ export interface ApiHandlerOptions {
 	apiModelId?: string
 	apiModelId?: string
 	apiKey?: string // anthropic
 	apiKey?: string // anthropic
 	anthropicBaseUrl?: string
 	anthropicBaseUrl?: string
-	anthropicThinking?: number
 	vsCodeLmModelSelector?: vscode.LanguageModelChatSelector
 	vsCodeLmModelSelector?: vscode.LanguageModelChatSelector
 	glamaModelId?: string
 	glamaModelId?: string
 	glamaModelInfo?: ModelInfo
 	glamaModelInfo?: ModelInfo
@@ -68,6 +67,8 @@ export interface ApiHandlerOptions {
 	requestyModelId?: string
 	requestyModelId?: string
 	requestyModelInfo?: ModelInfo
 	requestyModelInfo?: ModelInfo
 	modelTemperature?: number
 	modelTemperature?: number
+	modelMaxTokens?: number
+	modelMaxThinkingTokens?: number
 }
 }
 
 
 export type ApiConfiguration = ApiHandlerOptions & {
 export type ApiConfiguration = ApiHandlerOptions & {
@@ -92,19 +93,13 @@ export interface ModelInfo {
 	thinking?: boolean
 	thinking?: boolean
 }
 }
 
 
-export const THINKING_BUDGET = {
-	step: 1024,
-	min: 1024,
-	default: 8 * 1024,
-}
-
 // Anthropic
 // Anthropic
 // https://docs.anthropic.com/en/docs/about-claude/models
 // https://docs.anthropic.com/en/docs/about-claude/models
 export type AnthropicModelId = keyof typeof anthropicModels
 export type AnthropicModelId = keyof typeof anthropicModels
 export const anthropicDefaultModelId: AnthropicModelId = "claude-3-7-sonnet-20250219"
 export const anthropicDefaultModelId: AnthropicModelId = "claude-3-7-sonnet-20250219"
 export const anthropicModels = {
 export const anthropicModels = {
 	"claude-3-7-sonnet-20250219:thinking": {
 	"claude-3-7-sonnet-20250219:thinking": {
-		maxTokens: 16384,
+		maxTokens: 64_000,
 		contextWindow: 200_000,
 		contextWindow: 200_000,
 		supportsImages: true,
 		supportsImages: true,
 		supportsComputerUse: true,
 		supportsComputerUse: true,
@@ -116,7 +111,7 @@ export const anthropicModels = {
 		thinking: true,
 		thinking: true,
 	},
 	},
 	"claude-3-7-sonnet-20250219": {
 	"claude-3-7-sonnet-20250219": {
-		maxTokens: 16384,
+		maxTokens: 64_000,
 		contextWindow: 200_000,
 		contextWindow: 200_000,
 		supportsImages: true,
 		supportsImages: true,
 		supportsComputerUse: true,
 		supportsComputerUse: true,
@@ -441,55 +436,80 @@ export const openRouterDefaultModelInfo: ModelInfo = {
 export type VertexModelId = keyof typeof vertexModels
 export type VertexModelId = keyof typeof vertexModels
 export const vertexDefaultModelId: VertexModelId = "claude-3-7-sonnet@20250219"
 export const vertexDefaultModelId: VertexModelId = "claude-3-7-sonnet@20250219"
 export const vertexModels = {
 export const vertexModels = {
+	"claude-3-7-sonnet@20250219:thinking": {
+		maxTokens: 64000,
+		contextWindow: 200_000,
+		supportsImages: true,
+		supportsComputerUse: true,
+		supportsPromptCache: true,
+		inputPrice: 3.0,
+		outputPrice: 15.0,
+		cacheWritesPrice: 3.75,
+		cacheReadsPrice: 0.3,
+		thinking: true,
+	},
 	"claude-3-7-sonnet@20250219": {
 	"claude-3-7-sonnet@20250219": {
 		maxTokens: 8192,
 		maxTokens: 8192,
 		contextWindow: 200_000,
 		contextWindow: 200_000,
 		supportsImages: true,
 		supportsImages: true,
 		supportsComputerUse: true,
 		supportsComputerUse: true,
-		supportsPromptCache: false,
+		supportsPromptCache: true,
 		inputPrice: 3.0,
 		inputPrice: 3.0,
 		outputPrice: 15.0,
 		outputPrice: 15.0,
+		cacheWritesPrice: 3.75,
+		cacheReadsPrice: 0.3,
+		thinking: false,
 	},
 	},
 	"claude-3-5-sonnet-v2@20241022": {
 	"claude-3-5-sonnet-v2@20241022": {
 		maxTokens: 8192,
 		maxTokens: 8192,
 		contextWindow: 200_000,
 		contextWindow: 200_000,
 		supportsImages: true,
 		supportsImages: true,
 		supportsComputerUse: true,
 		supportsComputerUse: true,
-		supportsPromptCache: false,
+		supportsPromptCache: true,
 		inputPrice: 3.0,
 		inputPrice: 3.0,
 		outputPrice: 15.0,
 		outputPrice: 15.0,
+		cacheWritesPrice: 3.75,
+		cacheReadsPrice: 0.3,
 	},
 	},
 	"claude-3-5-sonnet@20240620": {
 	"claude-3-5-sonnet@20240620": {
 		maxTokens: 8192,
 		maxTokens: 8192,
 		contextWindow: 200_000,
 		contextWindow: 200_000,
 		supportsImages: true,
 		supportsImages: true,
-		supportsPromptCache: false,
+		supportsPromptCache: true,
 		inputPrice: 3.0,
 		inputPrice: 3.0,
 		outputPrice: 15.0,
 		outputPrice: 15.0,
+		cacheWritesPrice: 3.75,
+		cacheReadsPrice: 0.3,
 	},
 	},
 	"claude-3-5-haiku@20241022": {
 	"claude-3-5-haiku@20241022": {
 		maxTokens: 8192,
 		maxTokens: 8192,
 		contextWindow: 200_000,
 		contextWindow: 200_000,
 		supportsImages: false,
 		supportsImages: false,
-		supportsPromptCache: false,
+		supportsPromptCache: true,
 		inputPrice: 1.0,
 		inputPrice: 1.0,
 		outputPrice: 5.0,
 		outputPrice: 5.0,
+		cacheWritesPrice: 1.25,
+		cacheReadsPrice: 0.1,
 	},
 	},
 	"claude-3-opus@20240229": {
 	"claude-3-opus@20240229": {
 		maxTokens: 4096,
 		maxTokens: 4096,
 		contextWindow: 200_000,
 		contextWindow: 200_000,
 		supportsImages: true,
 		supportsImages: true,
-		supportsPromptCache: false,
+		supportsPromptCache: true,
 		inputPrice: 15.0,
 		inputPrice: 15.0,
 		outputPrice: 75.0,
 		outputPrice: 75.0,
+		cacheWritesPrice: 18.75,
+		cacheReadsPrice: 1.5,
 	},
 	},
 	"claude-3-haiku@20240307": {
 	"claude-3-haiku@20240307": {
 		maxTokens: 4096,
 		maxTokens: 4096,
 		contextWindow: 200_000,
 		contextWindow: 200_000,
 		supportsImages: true,
 		supportsImages: true,
-		supportsPromptCache: false,
+		supportsPromptCache: true,
 		inputPrice: 0.25,
 		inputPrice: 0.25,
 		outputPrice: 1.25,
 		outputPrice: 1.25,
+		cacheWritesPrice: 0.3,
+		cacheReadsPrice: 0.03,
 	},
 	},
 } as const satisfies Record<string, ModelInfo>
 } as const satisfies Record<string, ModelInfo>
 
 
@@ -671,8 +691,16 @@ export const openAiNativeModels = {
 		inputPrice: 1.1,
 		inputPrice: 1.1,
 		outputPrice: 4.4,
 		outputPrice: 4.4,
 	},
 	},
+	"gpt-4.5-preview": {
+		maxTokens: 16_384,
+		contextWindow: 128_000,
+		supportsImages: true,
+		supportsPromptCache: false,
+		inputPrice: 75,
+		outputPrice: 150,
+	},
 	"gpt-4o": {
 	"gpt-4o": {
-		maxTokens: 4_096,
+		maxTokens: 16_384,
 		contextWindow: 128_000,
 		contextWindow: 128_000,
 		supportsImages: true,
 		supportsImages: true,
 		supportsPromptCache: false,
 		supportsPromptCache: false,

+ 3 - 2
src/shared/globalState.ts

@@ -42,7 +42,6 @@ export type GlobalStateKey =
 	| "lmStudioModelId"
 	| "lmStudioModelId"
 	| "lmStudioBaseUrl"
 	| "lmStudioBaseUrl"
 	| "anthropicBaseUrl"
 	| "anthropicBaseUrl"
-	| "anthropicThinking"
 	| "azureApiVersion"
 	| "azureApiVersion"
 	| "openAiStreamingEnabled"
 	| "openAiStreamingEnabled"
 	| "openRouterModelId"
 	| "openRouterModelId"
@@ -53,7 +52,7 @@ export type GlobalStateKey =
 	| "soundEnabled"
 	| "soundEnabled"
 	| "soundVolume"
 	| "soundVolume"
 	| "diffEnabled"
 	| "diffEnabled"
-	| "checkpointsEnabled"
+	| "enableCheckpoints"
 	| "browserViewportSize"
 	| "browserViewportSize"
 	| "screenshotQuality"
 	| "screenshotQuality"
 	| "fuzzyMatchThreshold"
 	| "fuzzyMatchThreshold"
@@ -81,5 +80,7 @@ export type GlobalStateKey =
 	| "requestyModelInfo"
 	| "requestyModelInfo"
 	| "unboundModelInfo"
 	| "unboundModelInfo"
 	| "modelTemperature"
 	| "modelTemperature"
+	| "modelMaxTokens"
+	| "anthropicThinking" // TODO: Rename to `modelMaxThinkingTokens`.
 	| "mistralCodestralUrl"
 	| "mistralCodestralUrl"
 	| "maxOpenTabsContext"
 	| "maxOpenTabsContext"

+ 23 - 23
webview-ui/src/components/chat/Announcement.tsx

@@ -1,8 +1,5 @@
 import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
 import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
 import { memo } from "react"
 import { memo } from "react"
-// import VSCodeButtonLink from "./VSCodeButtonLink"
-// import { getOpenRouterAuthUrl } from "./ApiOptions"
-// import { vscode } from "../utils/vscode"
 
 
 interface AnnouncementProps {
 interface AnnouncementProps {
 	version: string
 	version: string
@@ -25,39 +22,42 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
 			<VSCodeButton
 			<VSCodeButton
 				appearance="icon"
 				appearance="icon"
 				onClick={hideAnnouncement}
 				onClick={hideAnnouncement}
+				title="Hide announcement"
 				style={{ position: "absolute", top: "8px", right: "8px" }}>
 				style={{ position: "absolute", top: "8px", right: "8px" }}>
 				<span className="codicon codicon-close"></span>
 				<span className="codicon codicon-close"></span>
 			</VSCodeButton>
 			</VSCodeButton>
-			<h2 style={{ margin: "0 0 8px" }}>🎉{"  "}Introducing Roo Code 3.2</h2>
+			<h2 style={{ margin: "0 0 8px" }}>🎉{"  "}Automatic Checkpoints Now Enabled</h2>
 
 
 			<p style={{ margin: "5px 0px" }}>
 			<p style={{ margin: "5px 0px" }}>
-				Our biggest update yet is here - we're officially changing our name from Roo Cline to Roo Code! After
-				growing beyond 50,000 installations, we're ready to chart our own course. Our heartfelt thanks to
-				everyone in the Cline community who helped us reach this milestone.
+				We're thrilled to announce that our experimental Checkpoints feature is now enabled by default for all
+				users. This powerful feature automatically tracks your project changes during a task, allowing you to
+				quickly review or revert to earlier states if needed.
 			</p>
 			</p>
 
 
-			<h3 style={{ margin: "12px 0 8px" }}>Custom Modes: Celebrating Our New Identity</h3>
+			<h3 style={{ margin: "12px 0 8px" }}>What's New</h3>
 			<p style={{ margin: "5px 0px" }}>
 			<p style={{ margin: "5px 0px" }}>
-				To mark this new chapter, we're introducing the power to shape Roo Code into any role you need! Create
-				specialized personas and create an entire team of agents with deeply customized prompts:
+				Automatic Checkpoints provide you with:
 				<ul style={{ margin: "4px 0 6px 20px", padding: 0 }}>
 				<ul style={{ margin: "4px 0 6px 20px", padding: 0 }}>
-					<li>QA Engineers who write thorough test cases and catch edge cases</li>
-					<li>Product Managers who excel at user stories and feature prioritization</li>
-					<li>UI/UX Designers who craft beautiful, accessible interfaces</li>
-					<li>Code Reviewers who ensure quality and maintainability</li>
+					<li>Peace of mind when making significant changes</li>
+					<li>Ability to visually inspect changes between steps</li>
+					<li>Easy rollback if you're not satisfied with certain code modifications</li>
+					<li>Improved navigation through complex task execution</li>
 				</ul>
 				</ul>
-				Just click the <span className="codicon codicon-notebook" style={{ fontSize: "10px" }}></span> icon to
-				get started with Custom Modes!
 			</p>
 			</p>
 
 
-			<h3 style={{ margin: "12px 0 8px" }}>Join Us for the Next Chapter</h3>
+			<h3 style={{ margin: "12px 0 8px" }}>Customize Your Experience</h3>
 			<p style={{ margin: "5px 0px" }}>
 			<p style={{ margin: "5px 0px" }}>
-				We can't wait to see how you'll push Roo Code's potential even further! Share your custom modes and join
-				the discussion at{" "}
-				<VSCodeLink href="https://www.reddit.com/r/RooCode" style={{ display: "inline" }}>
-					reddit.com/r/RooCode
-				</VSCodeLink>
-				.
+				While we recommend keeping this feature enabled, you can disable it if needed.{" "}
+				<VSCodeLink
+					href="#"
+					onClick={(e) => {
+						e.preventDefault()
+						window.postMessage({ type: "action", action: "settingsButtonClicked" }, "*")
+					}}
+					style={{ display: "inline", padding: "0 2px" }}>
+					Open Settings
+				</VSCodeLink>{" "}
+				and look for the "Enable automatic checkpoints" option in the Advanced Settings section.
 			</p>
 			</p>
 		</div>
 		</div>
 	)
 	)

+ 5 - 0
webview-ui/src/components/chat/ChatTextArea.tsx

@@ -798,6 +798,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 							<select
 							<select
 								value={mode}
 								value={mode}
 								disabled={textAreaDisabled}
 								disabled={textAreaDisabled}
+								title="Select mode for interaction"
 								onChange={(e) => {
 								onChange={(e) => {
 									const value = e.target.value
 									const value = e.target.value
 									if (value === "prompts-action") {
 									if (value === "prompts-action") {
@@ -849,6 +850,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 							<select
 							<select
 								value={currentApiConfigName || ""}
 								value={currentApiConfigName || ""}
 								disabled={textAreaDisabled}
 								disabled={textAreaDisabled}
+								title="Select API configuration"
 								onChange={(e) => {
 								onChange={(e) => {
 									const value = e.target.value
 									const value = e.target.value
 									if (value === "settings-action") {
 									if (value === "settings-action") {
@@ -915,6 +917,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 									role="button"
 									role="button"
 									aria-label="enhance prompt"
 									aria-label="enhance prompt"
 									data-testid="enhance-prompt-button"
 									data-testid="enhance-prompt-button"
+									title="Enhance prompt with additional context"
 									className={`input-icon-button ${
 									className={`input-icon-button ${
 										textAreaDisabled ? "disabled" : ""
 										textAreaDisabled ? "disabled" : ""
 									} codicon codicon-sparkle`}
 									} codicon codicon-sparkle`}
@@ -927,11 +930,13 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 							className={`input-icon-button ${
 							className={`input-icon-button ${
 								shouldDisableImages ? "disabled" : ""
 								shouldDisableImages ? "disabled" : ""
 							} codicon codicon-device-camera`}
 							} codicon codicon-device-camera`}
+							title="Add images to message"
 							onClick={() => !shouldDisableImages && onSelectImages()}
 							onClick={() => !shouldDisableImages && onSelectImages()}
 							style={{ fontSize: 16.5 }}
 							style={{ fontSize: 16.5 }}
 						/>
 						/>
 						<span
 						<span
 							className={`input-icon-button ${textAreaDisabled ? "disabled" : ""} codicon codicon-send`}
 							className={`input-icon-button ${textAreaDisabled ? "disabled" : ""} codicon codicon-send`}
+							title="Send message"
 							onClick={() => !textAreaDisabled && onSend()}
 							onClick={() => !textAreaDisabled && onSend()}
 							style={{ fontSize: 15 }}
 							style={{ fontSize: 15 }}
 						/>
 						/>

+ 32 - 1
webview-ui/src/components/chat/ChatView.tsx

@@ -1077,7 +1077,8 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
 								onClick={() => {
 								onClick={() => {
 									scrollToBottomSmooth()
 									scrollToBottomSmooth()
 									disableAutoScrollRef.current = false
 									disableAutoScrollRef.current = false
-								}}>
+								}}
+								title="Scroll to bottom of chat">
 								<span className="codicon codicon-chevron-down" style={{ fontSize: "18px" }}></span>
 								<span className="codicon codicon-chevron-down" style={{ fontSize: "18px" }}></span>
 							</ScrollToBottomButton>
 							</ScrollToBottomButton>
 						</div>
 						</div>
@@ -1101,6 +1102,25 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
 										flex: secondaryButtonText ? 1 : 2,
 										flex: secondaryButtonText ? 1 : 2,
 										marginRight: secondaryButtonText ? "6px" : "0",
 										marginRight: secondaryButtonText ? "6px" : "0",
 									}}
 									}}
+									title={
+										primaryButtonText === "Retry"
+											? "Try the operation again"
+											: primaryButtonText === "Save"
+												? "Save the file changes"
+												: primaryButtonText === "Approve"
+													? "Approve this action"
+													: primaryButtonText === "Run Command"
+														? "Execute this command"
+														: primaryButtonText === "Start New Task"
+															? "Begin a new task"
+															: primaryButtonText === "Resume Task"
+																? "Continue the current task"
+																: primaryButtonText === "Proceed Anyways"
+																	? "Continue despite warnings"
+																	: primaryButtonText === "Proceed While Running"
+																		? "Continue while command executes"
+																		: undefined
+									}
 									onClick={(e) => handlePrimaryButtonClick(inputValue, selectedImages)}>
 									onClick={(e) => handlePrimaryButtonClick(inputValue, selectedImages)}>
 									{primaryButtonText}
 									{primaryButtonText}
 								</VSCodeButton>
 								</VSCodeButton>
@@ -1113,6 +1133,17 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
 										flex: isStreaming ? 2 : 1,
 										flex: isStreaming ? 2 : 1,
 										marginLeft: isStreaming ? 0 : "6px",
 										marginLeft: isStreaming ? 0 : "6px",
 									}}
 									}}
+									title={
+										isStreaming
+											? "Cancel the current operation"
+											: secondaryButtonText === "Start New Task"
+												? "Begin a new task"
+												: secondaryButtonText === "Reject"
+													? "Reject this action"
+													: secondaryButtonText === "Terminate"
+														? "End the current task"
+														: undefined
+									}
 									onClick={(e) => handleSecondaryButtonClick(inputValue, selectedImages)}>
 									onClick={(e) => handleSecondaryButtonClick(inputValue, selectedImages)}>
 									{isStreaming ? "Cancel" : secondaryButtonText}
 									{isStreaming ? "Cancel" : secondaryButtonText}
 								</VSCodeButton>
 								</VSCodeButton>

+ 11 - 2
webview-ui/src/components/chat/TaskHeader.tsx

@@ -183,7 +183,11 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
 							${totalCost?.toFixed(4)}
 							${totalCost?.toFixed(4)}
 						</div>
 						</div>
 					)}
 					)}
-					<VSCodeButton appearance="icon" onClick={onClose} style={{ marginLeft: 6, flexShrink: 0 }}>
+					<VSCodeButton
+						appearance="icon"
+						onClick={onClose}
+						style={{ marginLeft: 6, flexShrink: 0 }}
+						title="Close task and start a new one">
 						<span className="codicon codicon-close"></span>
 						<span className="codicon codicon-close"></span>
 					</VSCodeButton>
 					</VSCodeButton>
 				</div>
 				</div>
@@ -351,13 +355,18 @@ export const highlightMentions = (text?: string, withShadow = true) => {
 
 
 const TaskActions = ({ item }: { item: HistoryItem | undefined }) => (
 const TaskActions = ({ item }: { item: HistoryItem | undefined }) => (
 	<div className="flex flex-row gap-1">
 	<div className="flex flex-row gap-1">
-		<Button variant="ghost" size="sm" onClick={() => vscode.postMessage({ type: "exportCurrentTask" })}>
+		<Button
+			variant="ghost"
+			size="sm"
+			title="Export task history"
+			onClick={() => vscode.postMessage({ type: "exportCurrentTask" })}>
 			<span className="codicon codicon-cloud-download" />
 			<span className="codicon codicon-cloud-download" />
 		</Button>
 		</Button>
 		{!!item?.size && item.size > 0 && (
 		{!!item?.size && item.size > 0 && (
 			<Button
 			<Button
 				variant="ghost"
 				variant="ghost"
 				size="sm"
 				size="sm"
+				title="Delete task from history"
 				onClick={() => vscode.postMessage({ type: "deleteTaskWithId", text: item.id })}>
 				onClick={() => vscode.postMessage({ type: "deleteTaskWithId", text: item.id })}>
 				<span className="codicon codicon-trash" />
 				<span className="codicon codicon-trash" />
 				{prettyBytes(item.size)}
 				{prettyBytes(item.size)}

+ 40 - 0
webview-ui/src/components/prompts/PromptsView.tsx

@@ -88,6 +88,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
 	const [showConfigMenu, setShowConfigMenu] = useState(false)
 	const [showConfigMenu, setShowConfigMenu] = useState(false)
 	const [isCreateModeDialogOpen, setIsCreateModeDialogOpen] = useState(false)
 	const [isCreateModeDialogOpen, setIsCreateModeDialogOpen] = useState(false)
 	const [activeSupportTab, setActiveSupportTab] = useState<SupportPromptType>("ENHANCE")
 	const [activeSupportTab, setActiveSupportTab] = useState<SupportPromptType>("ENHANCE")
+	const [isSystemPromptDisclosureOpen, setIsSystemPromptDisclosureOpen] = useState(false)
 
 
 	// Direct update functions
 	// Direct update functions
 	const updateAgentPrompt = useCallback(
 	const updateAgentPrompt = useCallback(
@@ -971,6 +972,45 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
 							<span className="codicon codicon-copy"></span>
 							<span className="codicon codicon-copy"></span>
 						</VSCodeButton>
 						</VSCodeButton>
 					</div>
 					</div>
+
+					{/* Custom System Prompt Disclosure */}
+					<div className="mb-3 mt-12">
+						<button
+							onClick={() => setIsSystemPromptDisclosureOpen(!isSystemPromptDisclosureOpen)}
+							className="flex items-center text-xs text-vscode-foreground hover:text-vscode-textLink-foreground focus:outline-none"
+							aria-expanded={isSystemPromptDisclosureOpen}>
+							<span
+								className={`codicon codicon-${isSystemPromptDisclosureOpen ? "chevron-down" : "chevron-right"} mr-1`}></span>
+							<span>Advanced: Override System Prompt</span>
+						</button>
+
+						{isSystemPromptDisclosureOpen && (
+							<div className="text-xs text-vscode-descriptionForeground mt-2 ml-5">
+								You can completely replace the system prompt for this mode (aside from the role
+								definition and custom instructions) by creating a file at{" "}
+								<span
+									className="text-vscode-textLink-foreground cursor-pointer underline"
+									onClick={() => {
+										const currentMode = getCurrentMode()
+										if (!currentMode) return
+
+										// Open or create an empty file
+										vscode.postMessage({
+											type: "openFile",
+											text: `./.roo/system-prompt-${currentMode.slug}`,
+											values: {
+												create: true,
+												content: "",
+											},
+										})
+									}}>
+									.roo/system-prompt-{getCurrentMode()?.slug || "code"}
+								</span>{" "}
+								in your workspace. This is a very advanced feature that bypasses built-in safeguards and
+								consistency checks (especially around tool usage), so be careful!
+							</div>
+						)}
+					</div>
 				</div>
 				</div>
 
 
 				<div
 				<div

+ 3 - 0
webview-ui/src/components/settings/ApiOptions.tsx

@@ -7,6 +7,7 @@ import * as vscodemodels from "vscode"
 import {
 import {
 	ApiConfiguration,
 	ApiConfiguration,
 	ModelInfo,
 	ModelInfo,
+	ApiProvider,
 	anthropicDefaultModelId,
 	anthropicDefaultModelId,
 	anthropicModels,
 	anthropicModels,
 	azureOpenAiDefaultApiVersion,
 	azureOpenAiDefaultApiVersion,
@@ -1380,9 +1381,11 @@ const ApiOptions = ({
 						/>
 						/>
 					</div>
 					</div>
 					<ThinkingBudget
 					<ThinkingBudget
+						key={`${selectedProvider}-${selectedModelId}`}
 						apiConfiguration={apiConfiguration}
 						apiConfiguration={apiConfiguration}
 						setApiConfigurationField={setApiConfigurationField}
 						setApiConfigurationField={setApiConfigurationField}
 						modelInfo={selectedModelInfo}
 						modelInfo={selectedModelInfo}
+						provider={selectedProvider as ApiProvider}
 					/>
 					/>
 					<ModelInfoView
 					<ModelInfoView
 						selectedModelId={selectedModelId}
 						selectedModelId={selectedModelId}

+ 21 - 24
webview-ui/src/components/settings/SettingsView.tsx

@@ -52,7 +52,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
 		alwaysAllowWrite,
 		alwaysAllowWrite,
 		alwaysApproveResubmit,
 		alwaysApproveResubmit,
 		browserViewportSize,
 		browserViewportSize,
-		checkpointsEnabled,
+		enableCheckpoints,
 		diffEnabled,
 		diffEnabled,
 		experiments,
 		experiments,
 		fuzzyMatchThreshold,
 		fuzzyMatchThreshold,
@@ -143,7 +143,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
 			vscode.postMessage({ type: "soundEnabled", bool: soundEnabled })
 			vscode.postMessage({ type: "soundEnabled", bool: soundEnabled })
 			vscode.postMessage({ type: "soundVolume", value: soundVolume })
 			vscode.postMessage({ type: "soundVolume", value: soundVolume })
 			vscode.postMessage({ type: "diffEnabled", bool: diffEnabled })
 			vscode.postMessage({ type: "diffEnabled", bool: diffEnabled })
-			vscode.postMessage({ type: "checkpointsEnabled", bool: checkpointsEnabled })
+			vscode.postMessage({ type: "enableCheckpoints", bool: enableCheckpoints })
 			vscode.postMessage({ type: "browserViewportSize", text: browserViewportSize })
 			vscode.postMessage({ type: "browserViewportSize", text: browserViewportSize })
 			vscode.postMessage({ type: "fuzzyMatchThreshold", value: fuzzyMatchThreshold ?? 1.0 })
 			vscode.postMessage({ type: "fuzzyMatchThreshold", value: fuzzyMatchThreshold ?? 1.0 })
 			vscode.postMessage({ type: "writeDelayMs", value: writeDelayMs })
 			vscode.postMessage({ type: "writeDelayMs", value: writeDelayMs })
@@ -706,6 +706,25 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
 						</p>
 						</p>
 					</div>
 					</div>
 
 
+					<div style={{ marginBottom: 15 }}>
+						<VSCodeCheckbox
+							checked={enableCheckpoints}
+							onChange={(e: any) => {
+								setCachedStateField("enableCheckpoints", e.target.checked)
+							}}>
+							<span style={{ fontWeight: "500" }}>Enable automatic checkpoints</span>
+						</VSCodeCheckbox>
+						<p
+							style={{
+								fontSize: "12px",
+								marginTop: "5px",
+								color: "var(--vscode-descriptionForeground)",
+							}}>
+							When enabled, Roo will automatically create checkpoints during task execution, making it
+							easy to review changes or revert to earlier states.
+						</p>
+					</div>
+
 					<div style={{ marginBottom: 15 }}>
 					<div style={{ marginBottom: 15 }}>
 						<VSCodeCheckbox
 						<VSCodeCheckbox
 							checked={diffEnabled}
 							checked={diffEnabled}
@@ -779,28 +798,6 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
 							</div>
 							</div>
 						)}
 						)}
 
 
-						<div style={{ marginBottom: 15 }}>
-							<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
-								<span style={{ color: "var(--vscode-errorForeground)" }}>⚠️</span>
-								<VSCodeCheckbox
-									checked={checkpointsEnabled}
-									onChange={(e: any) => {
-										setCachedStateField("checkpointsEnabled", e.target.checked)
-									}}>
-									<span style={{ fontWeight: "500" }}>Enable experimental checkpoints</span>
-								</VSCodeCheckbox>
-							</div>
-							<p
-								style={{
-									fontSize: "12px",
-									marginTop: "5px",
-									color: "var(--vscode-descriptionForeground)",
-								}}>
-								When enabled, Roo will save a checkpoint whenever a file in the workspace is modified,
-								added or deleted, letting you easily revert to a previous state.
-							</p>
-						</div>
-
 						{Object.entries(experimentConfigsMap)
 						{Object.entries(experimentConfigsMap)
 							.filter((config) => config[0] !== "DIFF_STRATEGY")
 							.filter((config) => config[0] !== "DIFF_STRATEGY")
 							.map((config) => (
 							.map((config) => (

+ 61 - 17
webview-ui/src/components/settings/ThinkingBudget.tsx

@@ -1,29 +1,73 @@
+import { useEffect, useMemo } from "react"
+import { ApiProvider } from "../../../../src/shared/api"
 import { Slider } from "@/components/ui"
 import { Slider } from "@/components/ui"
 
 
-import { ApiConfiguration, ModelInfo, THINKING_BUDGET } from "../../../../src/shared/api"
+import { ApiConfiguration, ModelInfo } from "../../../../src/shared/api"
 
 
 interface ThinkingBudgetProps {
 interface ThinkingBudgetProps {
 	apiConfiguration: ApiConfiguration
 	apiConfiguration: ApiConfiguration
 	setApiConfigurationField: <K extends keyof ApiConfiguration>(field: K, value: ApiConfiguration[K]) => void
 	setApiConfigurationField: <K extends keyof ApiConfiguration>(field: K, value: ApiConfiguration[K]) => void
 	modelInfo?: ModelInfo
 	modelInfo?: ModelInfo
+	provider?: ApiProvider
 }
 }
 
 
-export const ThinkingBudget = ({ apiConfiguration, setApiConfigurationField, modelInfo }: ThinkingBudgetProps) => {
-	const budget = apiConfiguration?.anthropicThinking ?? THINKING_BUDGET.default
-
-	return modelInfo && modelInfo.thinking ? (
-		<div className="flex flex-col gap-1 mt-2">
-			<div className="font-medium">Thinking Budget</div>
-			<div className="flex items-center gap-1">
-				<Slider
-					min={THINKING_BUDGET.min}
-					max={(modelInfo.maxTokens ?? THINKING_BUDGET.default) - 1}
-					step={THINKING_BUDGET.step}
-					value={[budget]}
-					onValueChange={(value) => setApiConfigurationField("anthropicThinking", value[0])}
-				/>
-				<div className="w-12 text-sm text-center">{budget}</div>
+export const ThinkingBudget = ({
+	apiConfiguration,
+	setApiConfigurationField,
+	modelInfo,
+	provider,
+}: ThinkingBudgetProps) => {
+	const tokens = apiConfiguration?.modelMaxTokens || modelInfo?.maxTokens || 64_000
+	const tokensMin = 8192
+	const tokensMax = modelInfo?.maxTokens || 64_000
+
+	// Get the appropriate thinking tokens based on provider
+	const thinkingTokens = useMemo(() => {
+		const value = apiConfiguration?.modelMaxThinkingTokens
+		return value || Math.min(Math.floor(0.8 * tokens), 8192)
+	}, [apiConfiguration, tokens])
+
+	const thinkingTokensMin = 1024
+	const thinkingTokensMax = Math.floor(0.8 * tokens)
+
+	useEffect(() => {
+		if (thinkingTokens > thinkingTokensMax) {
+			setApiConfigurationField("modelMaxThinkingTokens", thinkingTokensMax)
+		}
+	}, [thinkingTokens, thinkingTokensMax, setApiConfigurationField])
+
+	if (!modelInfo?.thinking) {
+		return null
+	}
+
+	return (
+		<div className="flex flex-col gap-2">
+			<div className="flex flex-col gap-1 mt-2">
+				<div className="font-medium">Max Tokens</div>
+				<div className="flex items-center gap-1">
+					<Slider
+						min={tokensMin}
+						max={tokensMax}
+						step={1024}
+						value={[tokens]}
+						onValueChange={([value]) => setApiConfigurationField("modelMaxTokens", value)}
+					/>
+					<div className="w-12 text-sm text-center">{tokens}</div>
+				</div>
+			</div>
+			<div className="flex flex-col gap-1 mt-2">
+				<div className="font-medium">Max Thinking Tokens</div>
+				<div className="flex items-center gap-1">
+					<Slider
+						min={thinkingTokensMin}
+						max={thinkingTokensMax}
+						step={1024}
+						value={[thinkingTokens]}
+						onValueChange={([value]) => setApiConfigurationField("modelMaxThinkingTokens", value)}
+					/>
+					<div className="w-12 text-sm text-center">{thinkingTokens}</div>
+				</div>
 			</div>
 			</div>
 		</div>
 		</div>
-	) : null
+	)
 }
 }

+ 51 - 1
webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx

@@ -46,6 +46,16 @@ jest.mock("../TemperatureControl", () => ({
 	),
 	),
 }))
 }))
 
 
+// Mock ThinkingBudget component
+jest.mock("../ThinkingBudget", () => ({
+	ThinkingBudget: ({ apiConfiguration, setApiConfigurationField, modelInfo, provider }: any) =>
+		modelInfo?.thinking ? (
+			<div data-testid="thinking-budget" data-provider={provider}>
+				<input data-testid="thinking-tokens" value={apiConfiguration?.modelMaxThinkingTokens} />
+			</div>
+		) : null,
+}))
+
 describe("ApiOptions", () => {
 describe("ApiOptions", () => {
 	const renderApiOptions = (props = {}) => {
 	const renderApiOptions = (props = {}) => {
 		render(
 		render(
@@ -72,5 +82,45 @@ describe("ApiOptions", () => {
 		expect(screen.queryByTestId("temperature-control")).not.toBeInTheDocument()
 		expect(screen.queryByTestId("temperature-control")).not.toBeInTheDocument()
 	})
 	})
 
 
-	//TODO: More test cases needed
+	describe("thinking functionality", () => {
+		it("should show ThinkingBudget for Anthropic models that support thinking", () => {
+			renderApiOptions({
+				apiConfiguration: {
+					apiProvider: "anthropic",
+					apiModelId: "claude-3-7-sonnet-20250219:thinking",
+				},
+			})
+
+			expect(screen.getByTestId("thinking-budget")).toBeInTheDocument()
+			expect(screen.getByTestId("thinking-budget")).toHaveAttribute("data-provider", "anthropic")
+		})
+
+		it("should show ThinkingBudget for Vertex models that support thinking", () => {
+			renderApiOptions({
+				apiConfiguration: {
+					apiProvider: "vertex",
+					apiModelId: "claude-3-7-sonnet@20250219:thinking",
+				},
+			})
+
+			expect(screen.getByTestId("thinking-budget")).toBeInTheDocument()
+			expect(screen.getByTestId("thinking-budget")).toHaveAttribute("data-provider", "vertex")
+		})
+
+		it("should not show ThinkingBudget for models that don't support thinking", () => {
+			renderApiOptions({
+				apiConfiguration: {
+					apiProvider: "anthropic",
+					apiModelId: "claude-3-opus-20240229",
+					modelInfo: { thinking: false }, // Non-thinking model
+				},
+			})
+
+			expect(screen.queryByTestId("thinking-budget")).not.toBeInTheDocument()
+		})
+
+		// Note: We don't need to test the actual ThinkingBudget component functionality here
+		// since we have separate tests for that component. We just need to verify that
+		// it's included in the ApiOptions component when appropriate.
+	})
 })
 })

+ 145 - 0
webview-ui/src/components/settings/__tests__/ThinkingBudget.test.tsx

@@ -0,0 +1,145 @@
+import React from "react"
+import { render, screen, fireEvent } from "@testing-library/react"
+import { ThinkingBudget } from "../ThinkingBudget"
+import { ApiProvider, ModelInfo } from "../../../../../src/shared/api"
+
+// Mock Slider component
+jest.mock("@/components/ui", () => ({
+	Slider: ({ value, onValueChange, min, max }: any) => (
+		<input
+			type="range"
+			data-testid="slider"
+			min={min}
+			max={max}
+			value={value[0]}
+			onChange={(e) => onValueChange([parseInt(e.target.value)])}
+		/>
+	),
+}))
+
+describe("ThinkingBudget", () => {
+	const mockModelInfo: ModelInfo = {
+		thinking: true,
+		maxTokens: 16384,
+		contextWindow: 200000,
+		supportsPromptCache: true,
+		supportsImages: true,
+	}
+	const defaultProps = {
+		apiConfiguration: {},
+		setApiConfigurationField: jest.fn(),
+		modelInfo: mockModelInfo,
+		provider: "anthropic" as ApiProvider,
+	}
+
+	beforeEach(() => {
+		jest.clearAllMocks()
+	})
+
+	it("should render nothing when model doesn't support thinking", () => {
+		const { container } = render(
+			<ThinkingBudget
+				{...defaultProps}
+				modelInfo={{
+					...mockModelInfo,
+					thinking: false,
+					maxTokens: 16384,
+					contextWindow: 200000,
+					supportsPromptCache: true,
+					supportsImages: true,
+				}}
+			/>,
+		)
+
+		expect(container.firstChild).toBeNull()
+	})
+
+	it("should render sliders when model supports thinking", () => {
+		render(<ThinkingBudget {...defaultProps} />)
+
+		expect(screen.getAllByTestId("slider")).toHaveLength(2)
+	})
+
+	it("should use modelMaxThinkingTokens field for Anthropic provider", () => {
+		const setApiConfigurationField = jest.fn()
+
+		render(
+			<ThinkingBudget
+				{...defaultProps}
+				apiConfiguration={{ modelMaxThinkingTokens: 4096 }}
+				setApiConfigurationField={setApiConfigurationField}
+				provider="anthropic"
+			/>,
+		)
+
+		const sliders = screen.getAllByTestId("slider")
+		fireEvent.change(sliders[1], { target: { value: "5000" } })
+
+		expect(setApiConfigurationField).toHaveBeenCalledWith("modelMaxThinkingTokens", 5000)
+	})
+
+	it("should use modelMaxThinkingTokens field for Vertex provider", () => {
+		const setApiConfigurationField = jest.fn()
+
+		render(
+			<ThinkingBudget
+				{...defaultProps}
+				apiConfiguration={{ modelMaxThinkingTokens: 4096 }}
+				setApiConfigurationField={setApiConfigurationField}
+				provider="vertex"
+			/>,
+		)
+
+		const sliders = screen.getAllByTestId("slider")
+		fireEvent.change(sliders[1], { target: { value: "5000" } })
+
+		expect(setApiConfigurationField).toHaveBeenCalledWith("modelMaxThinkingTokens", 5000)
+	})
+
+	it("should cap thinking tokens at 80% of max tokens", () => {
+		const setApiConfigurationField = jest.fn()
+
+		render(
+			<ThinkingBudget
+				{...defaultProps}
+				apiConfiguration={{ modelMaxTokens: 10000, modelMaxThinkingTokens: 9000 }}
+				setApiConfigurationField={setApiConfigurationField}
+			/>,
+		)
+
+		// Effect should trigger and cap the value
+		expect(setApiConfigurationField).toHaveBeenCalledWith("modelMaxThinkingTokens", 8000) // 80% of 10000
+	})
+
+	it("should use default thinking tokens if not provided", () => {
+		render(<ThinkingBudget {...defaultProps} apiConfiguration={{ modelMaxTokens: 10000 }} />)
+
+		// Default is 80% of max tokens, capped at 8192
+		const sliders = screen.getAllByTestId("slider")
+		expect(sliders[1]).toHaveValue("8000") // 80% of 10000
+	})
+
+	it("should use min thinking tokens of 1024", () => {
+		render(<ThinkingBudget {...defaultProps} apiConfiguration={{ modelMaxTokens: 1000 }} />)
+
+		const sliders = screen.getAllByTestId("slider")
+		expect(sliders[1].getAttribute("min")).toBe("1024")
+	})
+
+	it("should update max tokens when slider changes", () => {
+		const setApiConfigurationField = jest.fn()
+
+		render(
+			<ThinkingBudget
+				{...defaultProps}
+				apiConfiguration={{ modelMaxTokens: 10000 }}
+				setApiConfigurationField={setApiConfigurationField}
+			/>,
+		)
+
+		const sliders = screen.getAllByTestId("slider")
+		fireEvent.change(sliders[0], { target: { value: "12000" } })
+
+		expect(setApiConfigurationField).toHaveBeenCalledWith("modelMaxTokens", 12000)
+	})
+})

+ 3 - 3
webview-ui/src/context/ExtensionStateContext.tsx

@@ -32,7 +32,7 @@ export interface ExtensionStateContextType extends ExtensionState {
 	setSoundEnabled: (value: boolean) => void
 	setSoundEnabled: (value: boolean) => void
 	setSoundVolume: (value: number) => void
 	setSoundVolume: (value: number) => void
 	setDiffEnabled: (value: boolean) => void
 	setDiffEnabled: (value: boolean) => void
-	setCheckpointsEnabled: (value: boolean) => void
+	setEnableCheckpoints: (value: boolean) => void
 	setBrowserViewportSize: (value: string) => void
 	setBrowserViewportSize: (value: string) => void
 	setFuzzyMatchThreshold: (value: number) => void
 	setFuzzyMatchThreshold: (value: number) => void
 	preferredLanguage: string
 	preferredLanguage: string
@@ -79,7 +79,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
 		soundEnabled: false,
 		soundEnabled: false,
 		soundVolume: 0.5,
 		soundVolume: 0.5,
 		diffEnabled: false,
 		diffEnabled: false,
-		checkpointsEnabled: false,
+		enableCheckpoints: true,
 		fuzzyMatchThreshold: 1.0,
 		fuzzyMatchThreshold: 1.0,
 		preferredLanguage: "English",
 		preferredLanguage: "English",
 		writeDelayMs: 1000,
 		writeDelayMs: 1000,
@@ -219,7 +219,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
 		setSoundEnabled: (value) => setState((prevState) => ({ ...prevState, soundEnabled: value })),
 		setSoundEnabled: (value) => setState((prevState) => ({ ...prevState, soundEnabled: value })),
 		setSoundVolume: (value) => setState((prevState) => ({ ...prevState, soundVolume: value })),
 		setSoundVolume: (value) => setState((prevState) => ({ ...prevState, soundVolume: value })),
 		setDiffEnabled: (value) => setState((prevState) => ({ ...prevState, diffEnabled: value })),
 		setDiffEnabled: (value) => setState((prevState) => ({ ...prevState, diffEnabled: value })),
-		setCheckpointsEnabled: (value) => setState((prevState) => ({ ...prevState, checkpointsEnabled: value })),
+		setEnableCheckpoints: (value) => setState((prevState) => ({ ...prevState, enableCheckpoints: value })),
 		setBrowserViewportSize: (value: string) =>
 		setBrowserViewportSize: (value: string) =>
 			setState((prevState) => ({ ...prevState, browserViewportSize: value })),
 			setState((prevState) => ({ ...prevState, browserViewportSize: value })),
 		setFuzzyMatchThreshold: (value) => setState((prevState) => ({ ...prevState, fuzzyMatchThreshold: value })),
 		setFuzzyMatchThreshold: (value) => setState((prevState) => ({ ...prevState, fuzzyMatchThreshold: value })),