From 9b267e9afc8cc4308e5decfa0a9c312caaafc4bf Mon Sep 17 00:00:00 2001
From: Aitor Oses
Date: Tue, 25 Feb 2025 15:30:10 +0100
Subject: [PATCH 01/27] Add Vertex AI prompt caching support and enhance
streaming handling
- Implemented comprehensive prompt caching strategy for Vertex AI models
- Added support for caching system prompts and user message text blocks
- Enhanced stream processing to handle cache-related usage metrics
- Updated model configurations to enable prompt caching
- Improved type definitions for Vertex AI message handling
---
src/api/providers/__tests__/vertex.test.ts | 215 ++++++++++++++++++-
src/api/providers/vertex.ts | 235 ++++++++++++++++++---
src/shared/api.ts | 20 +-
3 files changed, 435 insertions(+), 35 deletions(-)
diff --git a/src/api/providers/__tests__/vertex.test.ts b/src/api/providers/__tests__/vertex.test.ts
index ebe60ba0c6..6e81fd771b 100644
--- a/src/api/providers/__tests__/vertex.test.ts
+++ b/src/api/providers/__tests__/vertex.test.ts
@@ -4,6 +4,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
import { AnthropicVertex } from "@anthropic-ai/vertex-sdk"
import { VertexHandler } from "../vertex"
+import { ApiStreamChunk } from "../../transform/stream"
// Mock Vertex SDK
jest.mock("@anthropic-ai/vertex-sdk", () => ({
@@ -128,7 +129,7 @@ describe("VertexHandler", () => {
;(handler["client"].messages as any).create = mockCreate
const stream = handler.createMessage(systemPrompt, mockMessages)
- const chunks = []
+ const chunks: ApiStreamChunk[] = []
for await (const chunk of stream) {
chunks.push(chunk)
@@ -158,8 +159,29 @@ describe("VertexHandler", () => {
model: "claude-3-5-sonnet-v2@20241022",
max_tokens: 8192,
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,
})
})
@@ -196,7 +218,7 @@ describe("VertexHandler", () => {
;(handler["client"].messages as any).create = mockCreate
const stream = handler.createMessage(systemPrompt, mockMessages)
- const chunks = []
+ const chunks: ApiStreamChunk[] = []
for await (const chunk of stream) {
chunks.push(chunk)
@@ -230,6 +252,183 @@ describe("VertexHandler", () => {
}
}).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("completePrompt", () => {
@@ -240,7 +439,13 @@ describe("VertexHandler", () => {
model: "claude-3-5-sonnet-v2@20241022",
max_tokens: 8192,
temperature: 0,
- messages: [{ role: "user", content: "Test prompt" }],
+ system: "",
+ messages: [
+ {
+ role: "user",
+ content: [{ type: "text", text: "Test prompt", cache_control: { type: "ephemeral" } }],
+ },
+ ],
stream: false,
})
})
diff --git a/src/api/providers/vertex.ts b/src/api/providers/vertex.ts
index 0ee22e5893..70562766c3 100644
--- a/src/api/providers/vertex.ts
+++ b/src/api/providers/vertex.ts
@@ -1,9 +1,86 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { AnthropicVertex } from "@anthropic-ai/vertex-sdk"
+import { Stream as AnthropicStream } from "@anthropic-ai/sdk/streaming"
import { ApiHandler, SingleCompletionHandler } from "../"
import { ApiHandlerOptions, ModelInfo, vertexDefaultModelId, VertexModelId, vertexModels } from "../../shared/api"
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 {
+ 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
+ }
+ index?: number
+ delta?: {
+ type: "text_delta"
+ text: string
+ }
+}
+
// https://docs.anthropic.com/en/api/claude-on-vertex-ai
export class VertexHandler implements ApiHandler, SingleCompletionHandler {
private options: ApiHandlerOptions
@@ -18,37 +95,120 @@ 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 {
- const stream = await this.client.messages.create({
- model: this.getModel().id,
- max_tokens: this.getModel().info.maxTokens || 8192,
+ const model = this.getModel()
+ 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: model.id,
+ max_tokens: model.info.maxTokens || 8192,
temperature: this.options.modelTemperature ?? 0,
- system: systemPrompt,
- messages,
+ // 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,
- })
+ }
+
+ const stream = (await this.client.messages.create(
+ params as Anthropic.Messages.MessageCreateParamsStreaming,
+ )) as unknown as AnthropicStream
+
+ // Process the stream chunks
for await (const chunk of stream) {
switch (chunk.type) {
- case "message_start":
- const usage = chunk.message.usage
+ case "message_start": {
+ const usage = chunk.message!.usage
yield {
type: "usage",
inputTokens: usage.input_tokens || 0,
outputTokens: usage.output_tokens || 0,
+ cacheWriteTokens: usage.cache_creation_input_tokens,
+ cacheReadTokens: usage.cache_read_input_tokens,
}
break
- case "message_delta":
+ }
+ case "message_delta": {
yield {
type: "usage",
inputTokens: 0,
- outputTokens: chunk.usage.output_tokens || 0,
+ outputTokens: chunk.usage!.output_tokens || 0,
}
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 {
type: "text",
text: "\n",
@@ -56,21 +216,25 @@ export class VertexHandler implements ApiHandler, SingleCompletionHandler {
}
yield {
type: "text",
- text: chunk.content_block.text,
+ text: chunk.content_block!.text,
}
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 {
type: "text",
- text: chunk.delta.text,
+ text: chunk.delta!.text,
}
break
+ }
}
break
+ }
}
}
}
@@ -86,13 +250,34 @@ export class VertexHandler implements ApiHandler, SingleCompletionHandler {
async completePrompt(prompt: string): Promise {
try {
- const response = await this.client.messages.create({
- model: this.getModel().id,
- max_tokens: this.getModel().info.maxTokens || 8192,
+ const model = this.getModel()
+ const useCache = model.info.supportsPromptCache
+
+ const params = {
+ model: model.id,
+ max_tokens: model.info.maxTokens || 8192,
temperature: this.options.modelTemperature ?? 0,
- messages: [{ role: "user", content: prompt }],
+ 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,
- })
+ }
+
+ const response = (await this.client.messages.create(
+ params as Anthropic.Messages.MessageCreateParamsNonStreaming,
+ )) as unknown as VertexMessageResponse
const content = response.content[0]
if (content.type === "text") {
diff --git a/src/shared/api.ts b/src/shared/api.ts
index cea760c776..95399cca4a 100644
--- a/src/shared/api.ts
+++ b/src/shared/api.ts
@@ -435,41 +435,51 @@ export const vertexModels = {
contextWindow: 200_000,
supportsImages: true,
supportsComputerUse: true,
- supportsPromptCache: false,
+ supportsPromptCache: true,
inputPrice: 3.0,
outputPrice: 15.0,
+ cacheWritesPrice: 3.75,
+ cacheReadsPrice: 0.3,
},
"claude-3-5-sonnet@20240620": {
maxTokens: 8192,
contextWindow: 200_000,
supportsImages: true,
- supportsPromptCache: false,
+ supportsPromptCache: true,
inputPrice: 3.0,
outputPrice: 15.0,
+ cacheWritesPrice: 3.75,
+ cacheReadsPrice: 0.3,
},
"claude-3-5-haiku@20241022": {
maxTokens: 8192,
contextWindow: 200_000,
supportsImages: false,
- supportsPromptCache: false,
+ supportsPromptCache: true,
inputPrice: 1.0,
outputPrice: 5.0,
+ cacheWritesPrice: 1.25,
+ cacheReadsPrice: 0.1,
},
"claude-3-opus@20240229": {
maxTokens: 4096,
contextWindow: 200_000,
supportsImages: true,
- supportsPromptCache: false,
+ supportsPromptCache: true,
inputPrice: 15.0,
outputPrice: 75.0,
+ cacheWritesPrice: 18.75,
+ cacheReadsPrice: 1.5,
},
"claude-3-haiku@20240307": {
maxTokens: 4096,
contextWindow: 200_000,
supportsImages: true,
- supportsPromptCache: false,
+ supportsPromptCache: true,
inputPrice: 0.25,
outputPrice: 1.25,
+ cacheWritesPrice: 0.3,
+ cacheReadsPrice: 0.03,
},
} as const satisfies Record
From 5c5bf8502094fb87397eebadda89acd3512dcf84 Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Wed, 26 Feb 2025 21:36:19 -0500
Subject: [PATCH 02/27] Stop removing commas from terminal output
---
.changeset/sour-parents-hug.md | 5 +++++
src/integrations/terminal/TerminalProcess.ts | 3 ---
2 files changed, 5 insertions(+), 3 deletions(-)
create mode 100644 .changeset/sour-parents-hug.md
diff --git a/.changeset/sour-parents-hug.md b/.changeset/sour-parents-hug.md
new file mode 100644
index 0000000000..a24286b6bb
--- /dev/null
+++ b/.changeset/sour-parents-hug.md
@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+Stop removing commas from terminal output
diff --git a/src/integrations/terminal/TerminalProcess.ts b/src/integrations/terminal/TerminalProcess.ts
index 5597350db3..4e85c10575 100644
--- a/src/integrations/terminal/TerminalProcess.ts
+++ b/src/integrations/terminal/TerminalProcess.ts
@@ -110,9 +110,6 @@ export class TerminalProcess extends EventEmitter {
data = lines.join("\n")
}
- // FIXME: right now it seems that data chunks returned to us from the shell integration stream contains random commas, which from what I can tell is not the expected behavior. There has to be a better solution here than just removing all commas.
- data = data.replace(/,/g, "")
-
// 2. Set isHot depending on the command
// Set to hot to stall API requests until terminal is cool again
this.isHot = true
From 4806ab5420048af6526348e5b128dd4724c9fcc8 Mon Sep 17 00:00:00 2001
From: dleffel
Date: Wed, 26 Feb 2025 21:34:56 -0800
Subject: [PATCH 03/27] Fix missing tooltips in several components.
---
.../src/components/chat/Announcement.tsx | 1 +
.../src/components/chat/ChatTextArea.tsx | 5 +++
webview-ui/src/components/chat/ChatView.tsx | 33 ++++++++++++++++++-
webview-ui/src/components/chat/TaskHeader.tsx | 13 ++++++--
4 files changed, 49 insertions(+), 3 deletions(-)
diff --git a/webview-ui/src/components/chat/Announcement.tsx b/webview-ui/src/components/chat/Announcement.tsx
index a2e96606ef..93d0c9d750 100644
--- a/webview-ui/src/components/chat/Announcement.tsx
+++ b/webview-ui/src/components/chat/Announcement.tsx
@@ -25,6 +25,7 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx
index be2b2a9798..dcbe085147 100644
--- a/webview-ui/src/components/chat/ChatTextArea.tsx
+++ b/webview-ui/src/components/chat/ChatTextArea.tsx
@@ -798,6 +798,7 @@ const ChatTextArea = forwardRef(
{
const value = e.target.value
if (value === "prompts-action") {
@@ -849,6 +850,7 @@ const ChatTextArea = forwardRef(
{
const value = e.target.value
if (value === "settings-action") {
@@ -915,6 +917,7 @@ const ChatTextArea = forwardRef(
role="button"
aria-label="enhance prompt"
data-testid="enhance-prompt-button"
+ title="Enhance prompt with additional context"
className={`input-icon-button ${
textAreaDisabled ? "disabled" : ""
} codicon codicon-sparkle`}
@@ -927,11 +930,13 @@ const ChatTextArea = forwardRef(
className={`input-icon-button ${
shouldDisableImages ? "disabled" : ""
} codicon codicon-device-camera`}
+ title="Add images to message"
onClick={() => !shouldDisableImages && onSelectImages()}
style={{ fontSize: 16.5 }}
/>
!textAreaDisabled && onSend()}
style={{ fontSize: 15 }}
/>
diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx
index 98369cf095..fcd1ba9a3b 100644
--- a/webview-ui/src/components/chat/ChatView.tsx
+++ b/webview-ui/src/components/chat/ChatView.tsx
@@ -1077,7 +1077,8 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
onClick={() => {
scrollToBottomSmooth()
disableAutoScrollRef.current = false
- }}>
+ }}
+ title="Scroll to bottom of chat">
@@ -1101,6 +1102,25 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
flex: secondaryButtonText ? 1 : 2,
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)}>
{primaryButtonText}
@@ -1113,6 +1133,17 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
flex: isStreaming ? 2 : 1,
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)}>
{isStreaming ? "Cancel" : secondaryButtonText}
diff --git a/webview-ui/src/components/chat/TaskHeader.tsx b/webview-ui/src/components/chat/TaskHeader.tsx
index 341855f796..fb7db6f617 100644
--- a/webview-ui/src/components/chat/TaskHeader.tsx
+++ b/webview-ui/src/components/chat/TaskHeader.tsx
@@ -180,7 +180,11 @@ const TaskHeader: React.FC = ({
${totalCost?.toFixed(4)}
)}
-
+
@@ -348,13 +352,18 @@ export const highlightMentions = (text?: string, withShadow = true) => {
const TaskActions = ({ item }: { item: HistoryItem | undefined }) => (
-
vscode.postMessage({ type: "exportCurrentTask" })}>
+ vscode.postMessage({ type: "exportCurrentTask" })}>
{!!item?.size && item.size > 0 && (
vscode.postMessage({ type: "deleteTaskWithId", text: item.id })}>
{prettyBytes(item.size)}
From 10c6f8fb67bc358a8a57e16a103205f113ec6687 Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Thu, 27 Feb 2025 01:31:30 -0500
Subject: [PATCH 04/27] Graduate checkpoints out of beta
---
.changeset/eighty-cheetahs-fetch.md | 5 +++
src/core/Cline.ts | 18 ++++----
src/core/webview/ClineProvider.ts | 30 ++++++-------
.../webview/__tests__/ClineProvider.test.ts | 4 +-
src/shared/ExtensionMessage.ts | 2 +-
src/shared/WebviewMessage.ts | 2 +-
src/shared/globalState.ts | 2 +-
.../src/components/chat/Announcement.tsx | 45 +++++++++----------
.../src/components/settings/SettingsView.tsx | 45 +++++++++----------
.../src/context/ExtensionStateContext.tsx | 6 +--
10 files changed, 80 insertions(+), 79 deletions(-)
create mode 100644 .changeset/eighty-cheetahs-fetch.md
diff --git a/.changeset/eighty-cheetahs-fetch.md b/.changeset/eighty-cheetahs-fetch.md
new file mode 100644
index 0000000000..ca103880c8
--- /dev/null
+++ b/.changeset/eighty-cheetahs-fetch.md
@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+Graduate checkpoints out of beta
diff --git a/src/core/Cline.ts b/src/core/Cline.ts
index 532b9cbe99..00897eecf4 100644
--- a/src/core/Cline.ts
+++ b/src/core/Cline.ts
@@ -115,7 +115,7 @@ export class Cline {
isInitialized = false
// checkpoints
- checkpointsEnabled: boolean = false
+ enableCheckpoints: boolean = false
private checkpointService?: CheckpointService
// streaming
@@ -159,7 +159,7 @@ export class Cline {
this.fuzzyMatchThreshold = fuzzyMatchThreshold ?? 1.0
this.providerRef = new WeakRef(provider)
this.diffViewProvider = new DiffViewProvider(cwd)
- this.checkpointsEnabled = enableCheckpoints ?? false
+ this.enableCheckpoints = enableCheckpoints ?? false
if (historyItem) {
this.taskId = historyItem.id
@@ -3337,7 +3337,7 @@ export class Cline {
// Checkpoints
private async getCheckpointService() {
- if (!this.checkpointsEnabled) {
+ if (!this.enableCheckpoints) {
throw new Error("Checkpoints are disabled")
}
@@ -3378,7 +3378,7 @@ export class Cline {
commitHash: string
mode: "full" | "checkpoint"
}) {
- if (!this.checkpointsEnabled) {
+ if (!this.enableCheckpoints) {
return
}
@@ -3417,12 +3417,12 @@ export class Cline {
)
} catch (err) {
this.providerRef.deref()?.log("[checkpointDiff] disabling checkpoints for this task")
- this.checkpointsEnabled = false
+ this.enableCheckpoints = false
}
}
public async checkpointSave({ isFirst }: { isFirst: boolean }) {
- if (!this.checkpointsEnabled) {
+ if (!this.enableCheckpoints) {
return
}
@@ -3443,7 +3443,7 @@ export class Cline {
}
} catch (err) {
this.providerRef.deref()?.log("[checkpointSave] disabling checkpoints for this task")
- this.checkpointsEnabled = false
+ this.enableCheckpoints = false
}
}
@@ -3456,7 +3456,7 @@ export class Cline {
commitHash: string
mode: "preview" | "restore"
}) {
- if (!this.checkpointsEnabled) {
+ if (!this.enableCheckpoints) {
return
}
@@ -3511,7 +3511,7 @@ export class Cline {
this.providerRef.deref()?.cancelTask()
} catch (err) {
this.providerRef.deref()?.log("[checkpointRestore] disabling checkpoints for this task")
- this.checkpointsEnabled = false
+ this.enableCheckpoints = false
}
}
}
diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index 5e6170e2ee..633c7d7293 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -64,7 +64,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
private cline?: Cline
private workspaceTracker?: WorkspaceTracker
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
customModesManager: CustomModesManager
@@ -317,7 +317,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
apiConfiguration,
customModePrompts,
diffEnabled,
- checkpointsEnabled,
+ enableCheckpoints,
fuzzyMatchThreshold,
mode,
customInstructions: globalInstructions,
@@ -332,7 +332,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
apiConfiguration,
customInstructions: effectiveInstructions,
enableDiff: diffEnabled,
- enableCheckpoints: checkpointsEnabled,
+ enableCheckpoints,
fuzzyMatchThreshold,
task,
images,
@@ -347,7 +347,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
apiConfiguration,
customModePrompts,
diffEnabled,
- checkpointsEnabled,
+ enableCheckpoints,
fuzzyMatchThreshold,
mode,
customInstructions: globalInstructions,
@@ -362,7 +362,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
apiConfiguration,
customInstructions: effectiveInstructions,
enableDiff: diffEnabled,
- enableCheckpoints: checkpointsEnabled,
+ enableCheckpoints,
fuzzyMatchThreshold,
historyItem,
experiments,
@@ -1017,9 +1017,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.updateGlobalState("diffEnabled", diffEnabled)
await this.postStateToWebview()
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()
break
case "browserViewportSize":
@@ -1939,11 +1939,11 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await fs.unlink(legacyMessagesFilePath)
}
- const { checkpointsEnabled } = await this.getState()
+ const { enableCheckpoints } = await this.getState()
const baseDir = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
// Delete checkpoints branch.
- if (checkpointsEnabled && baseDir) {
+ if (enableCheckpoints && baseDir) {
const branchSummary = await simpleGit(baseDir)
.branch(["-D", `roo-code-checkpoints-${id}`])
.catch(() => undefined)
@@ -1999,7 +1999,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
alwaysAllowModeSwitch,
soundEnabled,
diffEnabled,
- checkpointsEnabled,
+ enableCheckpoints,
taskHistory,
soundVolume,
browserViewportSize,
@@ -2048,7 +2048,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
.sort((a: HistoryItem, b: HistoryItem) => b.ts - a.ts),
soundEnabled: soundEnabled ?? false,
diffEnabled: diffEnabled ?? true,
- checkpointsEnabled: checkpointsEnabled ?? false,
+ enableCheckpoints: enableCheckpoints ?? true,
shouldShowAnnouncement: lastShownAnnouncementId !== this.latestAnnouncementId,
allowedCommands,
soundVolume: soundVolume ?? 0.5,
@@ -2181,7 +2181,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
allowedCommands,
soundEnabled,
diffEnabled,
- checkpointsEnabled,
+ enableCheckpoints,
soundVolume,
browserViewportSize,
fuzzyMatchThreshold,
@@ -2265,7 +2265,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.getGlobalState("allowedCommands") as Promise,
this.getGlobalState("soundEnabled") as Promise,
this.getGlobalState("diffEnabled") as Promise,
- this.getGlobalState("checkpointsEnabled") as Promise,
+ this.getGlobalState("enableCheckpoints") as Promise,
this.getGlobalState("soundVolume") as Promise,
this.getGlobalState("browserViewportSize") as Promise,
this.getGlobalState("fuzzyMatchThreshold") as Promise,
@@ -2376,7 +2376,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
allowedCommands,
soundEnabled: soundEnabled ?? false,
diffEnabled: diffEnabled ?? true,
- checkpointsEnabled: checkpointsEnabled ?? false,
+ enableCheckpoints: enableCheckpoints ?? true,
soundVolume,
browserViewportSize: browserViewportSize ?? "900x600",
screenshotQuality: screenshotQuality ?? 75,
diff --git a/src/core/webview/__tests__/ClineProvider.test.ts b/src/core/webview/__tests__/ClineProvider.test.ts
index 6449cc93be..c8742cd3f4 100644
--- a/src/core/webview/__tests__/ClineProvider.test.ts
+++ b/src/core/webview/__tests__/ClineProvider.test.ts
@@ -369,7 +369,7 @@ describe("ClineProvider", () => {
uriScheme: "vscode",
soundEnabled: false,
diffEnabled: false,
- checkpointsEnabled: false,
+ enableCheckpoints: false,
writeDelayMs: 1000,
browserViewportSize: "900x600",
fuzzyMatchThreshold: 1.0,
@@ -677,7 +677,7 @@ describe("ClineProvider", () => {
},
mode: "code",
diffEnabled: true,
- checkpointsEnabled: false,
+ enableCheckpoints: false,
fuzzyMatchThreshold: 1.0,
experiments: experimentDefault,
} as any)
diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts
index e87edffed1..34abd38dbf 100644
--- a/src/shared/ExtensionMessage.ts
+++ b/src/shared/ExtensionMessage.ts
@@ -111,7 +111,7 @@ export interface ExtensionState {
soundEnabled?: boolean
soundVolume?: number
diffEnabled?: boolean
- checkpointsEnabled: boolean
+ enableCheckpoints: boolean
browserViewportSize?: string
screenshotQuality?: number
fuzzyMatchThreshold?: number
diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts
index fde7442cc1..8d3a114e65 100644
--- a/src/shared/WebviewMessage.ts
+++ b/src/shared/WebviewMessage.ts
@@ -52,7 +52,7 @@ export interface WebviewMessage {
| "soundEnabled"
| "soundVolume"
| "diffEnabled"
- | "checkpointsEnabled"
+ | "enableCheckpoints"
| "browserViewportSize"
| "screenshotQuality"
| "openMcpSettings"
diff --git a/src/shared/globalState.ts b/src/shared/globalState.ts
index 2cc90456a7..0863b34db2 100644
--- a/src/shared/globalState.ts
+++ b/src/shared/globalState.ts
@@ -53,7 +53,7 @@ export type GlobalStateKey =
| "soundEnabled"
| "soundVolume"
| "diffEnabled"
- | "checkpointsEnabled"
+ | "enableCheckpoints"
| "browserViewportSize"
| "screenshotQuality"
| "fuzzyMatchThreshold"
diff --git a/webview-ui/src/components/chat/Announcement.tsx b/webview-ui/src/components/chat/Announcement.tsx
index a2e96606ef..13c77fe442 100644
--- a/webview-ui/src/components/chat/Announcement.tsx
+++ b/webview-ui/src/components/chat/Announcement.tsx
@@ -1,8 +1,5 @@
import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { memo } from "react"
-// import VSCodeButtonLink from "./VSCodeButtonLink"
-// import { getOpenRouterAuthUrl } from "./ApiOptions"
-// import { vscode } from "../utils/vscode"
interface AnnouncementProps {
version: string
@@ -28,36 +25,38 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
style={{ position: "absolute", top: "8px", right: "8px" }}>
- 🎉{" "}Introducing Roo Code 3.2
+ 🎉{" "}Automatic Checkpoints Now Enabled
- 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.
- Custom Modes: Celebrating Our New Identity
+ What's New
- 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:
- QA Engineers who write thorough test cases and catch edge cases
- Product Managers who excel at user stories and feature prioritization
- UI/UX Designers who craft beautiful, accessible interfaces
- Code Reviewers who ensure quality and maintainability
+ Peace of mind when making significant changes
+ Ability to visually inspect changes between steps
+ Easy rollback if you're not satisfied with certain code modifications
+ Improved navigation through complex task execution
- Just click the icon to
- get started with Custom Modes!
- Join Us for the Next Chapter
+ Customize Your Experience
- 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{" "}
-
- reddit.com/r/RooCode
-
- .
+ While we recommend keeping this feature enabled, you can disable it if needed.{" "}
+ {
+ e.preventDefault()
+ window.postMessage({ type: "action", action: "settingsButtonClicked" }, "*")
+ }}
+ style={{ display: "inline", padding: "0 2px" }}>
+ Open Settings
+ {" "}
+ and look for the "Enable automatic checkpoints" option in the Advanced Settings section.
)
diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx
index d3e65a99ea..51ef4fe81d 100644
--- a/webview-ui/src/components/settings/SettingsView.tsx
+++ b/webview-ui/src/components/settings/SettingsView.tsx
@@ -52,7 +52,7 @@ const SettingsView = forwardRef(({ onDone },
alwaysAllowWrite,
alwaysApproveResubmit,
browserViewportSize,
- checkpointsEnabled,
+ enableCheckpoints,
diffEnabled,
experiments,
fuzzyMatchThreshold,
@@ -143,7 +143,7 @@ const SettingsView = forwardRef(({ onDone },
vscode.postMessage({ type: "soundEnabled", bool: soundEnabled })
vscode.postMessage({ type: "soundVolume", value: soundVolume })
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: "fuzzyMatchThreshold", value: fuzzyMatchThreshold ?? 1.0 })
vscode.postMessage({ type: "writeDelayMs", value: writeDelayMs })
@@ -706,6 +706,25 @@ const SettingsView = forwardRef(({ onDone },
+
+
{
+ setCachedStateField("enableCheckpoints", e.target.checked)
+ }}>
+ Enable automatic checkpoints
+
+
+ When enabled, Roo will automatically create checkpoints during task execution, making it
+ easy to review changes or revert to earlier states.
+
+
+
(({ onDone },
)}
-
-
- ⚠️
- {
- setCachedStateField("checkpointsEnabled", e.target.checked)
- }}>
- Enable experimental checkpoints
-
-
-
- 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.
-
-
-
{Object.entries(experimentConfigsMap)
.filter((config) => config[0] !== "DIFF_STRATEGY")
.map((config) => (
diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx
index ae5c5b9539..3dfc87de75 100644
--- a/webview-ui/src/context/ExtensionStateContext.tsx
+++ b/webview-ui/src/context/ExtensionStateContext.tsx
@@ -32,7 +32,7 @@ export interface ExtensionStateContextType extends ExtensionState {
setSoundEnabled: (value: boolean) => void
setSoundVolume: (value: number) => void
setDiffEnabled: (value: boolean) => void
- setCheckpointsEnabled: (value: boolean) => void
+ setEnableCheckpoints: (value: boolean) => void
setBrowserViewportSize: (value: string) => void
setFuzzyMatchThreshold: (value: number) => void
preferredLanguage: string
@@ -79,7 +79,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
soundEnabled: false,
soundVolume: 0.5,
diffEnabled: false,
- checkpointsEnabled: false,
+ enableCheckpoints: true,
fuzzyMatchThreshold: 1.0,
preferredLanguage: "English",
writeDelayMs: 1000,
@@ -219,7 +219,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
setSoundEnabled: (value) => setState((prevState) => ({ ...prevState, soundEnabled: value })),
setSoundVolume: (value) => setState((prevState) => ({ ...prevState, soundVolume: 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) =>
setState((prevState) => ({ ...prevState, browserViewportSize: value })),
setFuzzyMatchThreshold: (value) => setState((prevState) => ({ ...prevState, fuzzyMatchThreshold: value })),
From ea38d9ebbac80f4170f74b4c7d978e588b132d2d Mon Sep 17 00:00:00 2001
From: Aitor Oses
Date: Thu, 27 Feb 2025 09:04:54 +0100
Subject: [PATCH 05/27] Enable prompt caching for Claude Sonnet 3.7 Vertex AI
model
---
src/shared/api.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/shared/api.ts b/src/shared/api.ts
index 5cda333031..cd6aead1a5 100644
--- a/src/shared/api.ts
+++ b/src/shared/api.ts
@@ -441,7 +441,7 @@ export const vertexModels = {
contextWindow: 200_000,
supportsImages: true,
supportsComputerUse: true,
- supportsPromptCache: false,
+ supportsPromptCache: true,
inputPrice: 3.0,
outputPrice: 15.0,
},
From 0b583ed15ee80acdd853c6e994c1694f3d4f0cca Mon Sep 17 00:00:00 2001
From: cte
Date: Thu, 27 Feb 2025 02:34:14 -0800
Subject: [PATCH 06/27] Fix AnthropicHandler#completePrompt
---
src/api/providers/anthropic.ts | 98 +++++++++++++++++-----------------
1 file changed, 49 insertions(+), 49 deletions(-)
diff --git a/src/api/providers/anthropic.ts b/src/api/providers/anthropic.ts
index 8c5a1795b1..eca81eab2e 100644
--- a/src/api/providers/anthropic.ts
+++ b/src/api/providers/anthropic.ts
@@ -30,29 +30,7 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
let stream: AnthropicStream
const cacheControl: CacheControlEphemeral = { type: "ephemeral" }
- let { id: modelId, info: modelInfo } = this.getModel()
- const maxTokens = this.options.modelMaxTokens || 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"
-
- // 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.anthropicThinking ?? maxBudgetTokens, maxBudgetTokens),
- 1024,
- )
-
- thinking = { type: "enabled", budget_tokens: budgetTokens }
- temperature = 1.0
- }
+ let { id: modelId, temperature, maxTokens, thinking } = this.getModel()
switch (modelId) {
case "claude-3-7-sonnet-20250219":
@@ -202,40 +180,62 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler {
}
}
- getModel(): { id: AnthropicModelId; info: ModelInfo } {
+ getModel() {
const modelId = this.options.apiModelId
+ let temperature = this.options.modelTemperature ?? ANTHROPIC_DEFAULT_TEMPERATURE
+ let thinking: BetaThinkingConfigParam | undefined = undefined
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]
+
+ // 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"
+ }
+
+ const maxTokens = this.options.modelMaxTokens || info.maxTokens || 8192
+
+ if (info.thinking) {
+ // Anthropic "Thinking" models require a temperature of 1.0.
+ temperature = 1.0
+
+ // 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.anthropicThinking ?? maxBudgetTokens, maxBudgetTokens),
+ 1024,
+ )
+
+ thinking = { type: "enabled", budget_tokens: budgetTokens }
+ }
+
+ return { id, info, temperature, maxTokens, thinking }
}
- return { id: anthropicDefaultModelId, info: anthropicModels[anthropicDefaultModelId] }
+ 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): Promise {
- 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,
- })
+ async completePrompt(prompt: string) {
+ let { id: modelId, temperature, maxTokens, thinking } = this.getModel()
- const content = response.content[0]
+ const message = await this.client.messages.create({
+ model: modelId,
+ max_tokens: maxTokens,
+ temperature,
+ thinking,
+ messages: [{ role: "user", content: prompt }],
+ stream: false,
+ })
- if (content.type === "text") {
- return content.text
- }
-
- return ""
- } catch (error) {
- if (error instanceof Error) {
- throw new Error(`Anthropic completion error: ${error.message}`)
- }
-
- throw error
- }
+ const content = message.content.find(({ type }) => type === "text")
+ return content?.type === "text" ? content.text : ""
}
}
From d66b5d2db62f0a6cb8650b8f465d14cf77bbcd36 Mon Sep 17 00:00:00 2001
From: cte
Date: Thu, 27 Feb 2025 02:40:39 -0800
Subject: [PATCH 07/27] Fix tests
---
src/api/providers/__tests__/anthropic.test.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/api/providers/__tests__/anthropic.test.ts b/src/api/providers/__tests__/anthropic.test.ts
index ff7bdb4054..82e098f65f 100644
--- a/src/api/providers/__tests__/anthropic.test.ts
+++ b/src/api/providers/__tests__/anthropic.test.ts
@@ -153,7 +153,7 @@ describe("AnthropicHandler", () => {
})
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")
})
From 210afc681e799ade14fa2886e07cce0cb6aa2496 Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Thu, 27 Feb 2025 09:44:50 -0500
Subject: [PATCH 08/27] v3.7.7
---
.changeset/gorgeous-feet-dress.md | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 .changeset/gorgeous-feet-dress.md
diff --git a/.changeset/gorgeous-feet-dress.md b/.changeset/gorgeous-feet-dress.md
new file mode 100644
index 0000000000..fe2183052d
--- /dev/null
+++ b/.changeset/gorgeous-feet-dress.md
@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+v3.7.7
From dc83617b4d2da06b830e848476a1c5d9179a361a Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Thu, 27 Feb 2025 09:51:05 -0500
Subject: [PATCH 09/27] Revert "Stop removing commas from terminal output"
---
.changeset/sour-parents-hug.md | 5 -----
src/integrations/terminal/TerminalProcess.ts | 3 +++
2 files changed, 3 insertions(+), 5 deletions(-)
delete mode 100644 .changeset/sour-parents-hug.md
diff --git a/.changeset/sour-parents-hug.md b/.changeset/sour-parents-hug.md
deleted file mode 100644
index a24286b6bb..0000000000
--- a/.changeset/sour-parents-hug.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"roo-cline": patch
----
-
-Stop removing commas from terminal output
diff --git a/src/integrations/terminal/TerminalProcess.ts b/src/integrations/terminal/TerminalProcess.ts
index 4e85c10575..5597350db3 100644
--- a/src/integrations/terminal/TerminalProcess.ts
+++ b/src/integrations/terminal/TerminalProcess.ts
@@ -110,6 +110,9 @@ export class TerminalProcess extends EventEmitter {
data = lines.join("\n")
}
+ // FIXME: right now it seems that data chunks returned to us from the shell integration stream contains random commas, which from what I can tell is not the expected behavior. There has to be a better solution here than just removing all commas.
+ data = data.replace(/,/g, "")
+
// 2. Set isHot depending on the command
// Set to hot to stall API requests until terminal is cool again
this.isHot = true
From 8612ab574be39f863329c1b93b32d257bd67450f Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
Date: Thu, 27 Feb 2025 15:56:03 +0000
Subject: [PATCH 10/27] changeset version bump
---
.changeset/eighty-cheetahs-fetch.md | 5 -----
.changeset/gorgeous-feet-dress.md | 5 -----
CHANGELOG.md | 7 +++++++
package-lock.json | 4 ++--
package.json | 2 +-
5 files changed, 10 insertions(+), 13 deletions(-)
delete mode 100644 .changeset/eighty-cheetahs-fetch.md
delete mode 100644 .changeset/gorgeous-feet-dress.md
diff --git a/.changeset/eighty-cheetahs-fetch.md b/.changeset/eighty-cheetahs-fetch.md
deleted file mode 100644
index ca103880c8..0000000000
--- a/.changeset/eighty-cheetahs-fetch.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"roo-cline": patch
----
-
-Graduate checkpoints out of beta
diff --git a/.changeset/gorgeous-feet-dress.md b/.changeset/gorgeous-feet-dress.md
deleted file mode 100644
index fe2183052d..0000000000
--- a/.changeset/gorgeous-feet-dress.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"roo-cline": patch
----
-
-v3.7.7
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 13b0695335..ff5d8d6aaf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
# Roo Code Changelog
+## 3.7.7
+
+### Patch Changes
+
+- Graduate checkpoints out of beta
+- v3.7.7
+
## [3.7.6]
- Handle really long text better in the in the ChatRow similar to TaskHeader (thanks @joemanley201!)
diff --git a/package-lock.json b/package-lock.json
index 808e2f2f10..c1f748983f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "roo-cline",
- "version": "3.7.6",
+ "version": "3.7.7",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "roo-cline",
- "version": "3.7.6",
+ "version": "3.7.7",
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.10.2",
"@anthropic-ai/sdk": "^0.37.0",
diff --git a/package.json b/package.json
index 463e9d597a..8441488bac 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
"displayName": "Roo Code (prev. Roo Cline)",
"description": "A whole dev team of AI agents in your editor.",
"publisher": "RooVeterinaryInc",
- "version": "3.7.6",
+ "version": "3.7.7",
"icon": "assets/icons/rocket.png",
"galleryBanner": {
"color": "#617A91",
From 4786815fe8bccf47af9202ff8cb99c6f1ef8ad16 Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Thu, 27 Feb 2025 11:02:13 -0500
Subject: [PATCH 11/27] Update CHANGELOG.md
---
CHANGELOG.md | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ff5d8d6aaf..d0cf8f79c3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,11 +1,10 @@
# Roo Code Changelog
-## 3.7.7
-
-### Patch Changes
+## [3.7.7]
- Graduate checkpoints out of beta
-- v3.7.7
+- Fix enhance prompt button when using Thinking Sonnet
+- Add tooltips to make what buttons do more obvious
## [3.7.6]
From eec1769b6b5883d3179e2d1a370ed01b83078286 Mon Sep 17 00:00:00 2001
From: Catalin Lupuleti
Date: Thu, 27 Feb 2025 18:56:28 +0000
Subject: [PATCH 12/27] Added cache costs for Claude Sonnet 3.7 via Vertex AI
---
src/shared/api.ts | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/shared/api.ts b/src/shared/api.ts
index e7e4c54db6..d2b4ed728f 100644
--- a/src/shared/api.ts
+++ b/src/shared/api.ts
@@ -444,6 +444,8 @@ export const vertexModels = {
supportsPromptCache: false,
inputPrice: 3.0,
outputPrice: 15.0,
+ cacheWritesPrice: 3.75,
+ cacheReadsPrice: 0.3,
},
"claude-3-5-sonnet-v2@20241022": {
maxTokens: 8192,
From 1f0211ee6418752201b7d9b34ffb12608ba4a54d Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Sun, 23 Feb 2025 20:52:10 -0600
Subject: [PATCH 13/27] Allow users to set custom system prompts
---
src/__mocks__/fs/promises.ts | 1 -
src/__mocks__/jest.setup.ts | 30 +++
.../__tests__/custom-system-prompt.test.ts | 172 ++++++++++++++++++
.../prompts/sections/custom-system-prompt.ts | 60 ++++++
src/core/prompts/system.ts | 15 ++
.../src/components/prompts/PromptsView.tsx | 40 ++++
6 files changed, 317 insertions(+), 1 deletion(-)
create mode 100644 src/core/prompts/__tests__/custom-system-prompt.test.ts
create mode 100644 src/core/prompts/sections/custom-system-prompt.ts
diff --git a/src/__mocks__/fs/promises.ts b/src/__mocks__/fs/promises.ts
index d5f076247a..e496a7fa51 100644
--- a/src/__mocks__/fs/promises.ts
+++ b/src/__mocks__/fs/promises.ts
@@ -140,7 +140,6 @@ const mockFs = {
currentPath += "/" + parts[parts.length - 1]
mockDirectories.add(currentPath)
return Promise.resolve()
- return Promise.resolve()
}),
access: jest.fn().mockImplementation(async (path: string) => {
diff --git a/src/__mocks__/jest.setup.ts b/src/__mocks__/jest.setup.ts
index 6bd00e9567..836279bfe4 100644
--- a/src/__mocks__/jest.setup.ts
+++ b/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)
+ }
+}
diff --git a/src/core/prompts/__tests__/custom-system-prompt.test.ts b/src/core/prompts/__tests__/custom-system-prompt.test.ts
new file mode 100644
index 0000000000..7594c13e6d
--- /dev/null
+++ b/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
+
+// 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")
+ })
+})
diff --git a/src/core/prompts/sections/custom-system-prompt.ts b/src/core/prompts/sections/custom-system-prompt.ts
new file mode 100644
index 0000000000..eca2b98b8d
--- /dev/null
+++ b/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 {
+ 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 {
+ const filePath = getSystemPromptFilePath(cwd, mode)
+ return safeReadFile(filePath)
+}
+
+/**
+ * Ensures the .roo directory exists, creating it if necessary
+ */
+export async function ensureRooDirectory(cwd: string): Promise {
+ 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
+ }
+ }
+}
diff --git a/src/core/prompts/system.ts b/src/core/prompts/system.ts
index 91bbd07387..90791f6358 100644
--- a/src/core/prompts/system.ts
+++ b/src/core/prompts/system.ts
@@ -23,6 +23,7 @@ import {
getModesSection,
addCustomInstructions,
} from "./sections"
+import { loadSystemPromptFile } from "./sections/custom-system-prompt"
import fs from "fs/promises"
import path from "path"
@@ -119,11 +120,25 @@ export const SYSTEM_PROMPT = async (
return undefined
}
+ // Try to load custom system prompt from file
+ const fileCustomSystemPrompt = await loadSystemPromptFile(cwd, mode)
+
// Check if it's a custom mode
const promptComponent = getPromptComponent(customModePrompts?.[mode])
+
// 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]
+ // 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
const effectiveDiffStrategy = diffEnabled ? diffStrategy : undefined
diff --git a/webview-ui/src/components/prompts/PromptsView.tsx b/webview-ui/src/components/prompts/PromptsView.tsx
index 061fa789de..2bfafeff5c 100644
--- a/webview-ui/src/components/prompts/PromptsView.tsx
+++ b/webview-ui/src/components/prompts/PromptsView.tsx
@@ -88,6 +88,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
const [showConfigMenu, setShowConfigMenu] = useState(false)
const [isCreateModeDialogOpen, setIsCreateModeDialogOpen] = useState(false)
const [activeSupportTab, setActiveSupportTab] = useState("ENHANCE")
+ const [isSystemPromptDisclosureOpen, setIsSystemPromptDisclosureOpen] = useState(false)
// Direct update functions
const updateAgentPrompt = useCallback(
@@ -971,6 +972,45 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
+
+ {/* Custom System Prompt Disclosure */}
+
+
setIsSystemPromptDisclosureOpen(!isSystemPromptDisclosureOpen)}
+ className="flex items-center text-xs text-vscode-foreground hover:text-vscode-textLink-foreground focus:outline-none"
+ aria-expanded={isSystemPromptDisclosureOpen}>
+
+ Advanced: Override System Prompt
+
+
+ {isSystemPromptDisclosureOpen && (
+
+ You can completely replace the system prompt for this mode (aside from the role
+ definition and custom instructions) by creating a file at{" "}
+ {
+ 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"}
+ {" "}
+ in your workspace. This is a very advanced feature that bypasses built-in safeguards and
+ consistency checks (especially around tool usage), so be careful!
+
+ )}
+
Date: Thu, 27 Feb 2025 15:18:54 -0500
Subject: [PATCH 14/27] Add gpt-4.5-preview
---
.changeset/flat-avocados-carry.md | 5 +++++
src/api/providers/__tests__/openai-native.test.ts | 2 +-
src/shared/api.ts | 10 +++++++++-
3 files changed, 15 insertions(+), 2 deletions(-)
create mode 100644 .changeset/flat-avocados-carry.md
diff --git a/.changeset/flat-avocados-carry.md b/.changeset/flat-avocados-carry.md
new file mode 100644
index 0000000000..f0128f21e0
--- /dev/null
+++ b/.changeset/flat-avocados-carry.md
@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+Add gpt-4.5-preview
diff --git a/src/api/providers/__tests__/openai-native.test.ts b/src/api/providers/__tests__/openai-native.test.ts
index d6a855849c..eda744c335 100644
--- a/src/api/providers/__tests__/openai-native.test.ts
+++ b/src/api/providers/__tests__/openai-native.test.ts
@@ -357,7 +357,7 @@ describe("OpenAiNativeHandler", () => {
const modelInfo = handler.getModel()
expect(modelInfo.id).toBe(mockOptions.apiModelId)
expect(modelInfo.info).toBeDefined()
- expect(modelInfo.info.maxTokens).toBe(4096)
+ expect(modelInfo.info.maxTokens).toBe(16384)
expect(modelInfo.info.contextWindow).toBe(128_000)
})
diff --git a/src/shared/api.ts b/src/shared/api.ts
index 442282d587..47b023ce6f 100644
--- a/src/shared/api.ts
+++ b/src/shared/api.ts
@@ -678,8 +678,16 @@ export const openAiNativeModels = {
inputPrice: 1.1,
outputPrice: 4.4,
},
+ "gpt-4.5-preview": {
+ maxTokens: 16_384,
+ contextWindow: 128_000,
+ supportsImages: true,
+ supportsPromptCache: false,
+ inputPrice: 75,
+ outputPrice: 150,
+ },
"gpt-4o": {
- maxTokens: 4_096,
+ maxTokens: 16_384,
contextWindow: 128_000,
supportsImages: true,
supportsPromptCache: false,
From 820ebc97c5251e04194b014bd60ab51e08de1481 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
Date: Thu, 27 Feb 2025 20:41:23 +0000
Subject: [PATCH 15/27] changeset version bump
---
.changeset/flat-avocados-carry.md | 5 -----
CHANGELOG.md | 6 ++++++
package-lock.json | 4 ++--
package.json | 2 +-
4 files changed, 9 insertions(+), 8 deletions(-)
delete mode 100644 .changeset/flat-avocados-carry.md
diff --git a/.changeset/flat-avocados-carry.md b/.changeset/flat-avocados-carry.md
deleted file mode 100644
index f0128f21e0..0000000000
--- a/.changeset/flat-avocados-carry.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"roo-cline": patch
----
-
-Add gpt-4.5-preview
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d0cf8f79c3..e3aa95d448 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
# Roo Code Changelog
+## 3.7.8
+
+### Patch Changes
+
+- Add gpt-4.5-preview
+
## [3.7.7]
- Graduate checkpoints out of beta
diff --git a/package-lock.json b/package-lock.json
index c1f748983f..e7d7718b75 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "roo-cline",
- "version": "3.7.7",
+ "version": "3.7.8",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "roo-cline",
- "version": "3.7.7",
+ "version": "3.7.8",
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.10.2",
"@anthropic-ai/sdk": "^0.37.0",
diff --git a/package.json b/package.json
index 8441488bac..a4a2298a48 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
"displayName": "Roo Code (prev. Roo Cline)",
"description": "A whole dev team of AI agents in your editor.",
"publisher": "RooVeterinaryInc",
- "version": "3.7.7",
+ "version": "3.7.8",
"icon": "assets/icons/rocket.png",
"galleryBanner": {
"color": "#617A91",
From ca7d746990ace8208b5f444ae6f16ea0f25525a1 Mon Sep 17 00:00:00 2001
From: R00-B0T
Date: Thu, 27 Feb 2025 20:41:50 +0000
Subject: [PATCH 16/27] Updating CHANGELOG.md format
---
CHANGELOG.md | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e3aa95d448..fe8156abf7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,6 @@
# Roo Code Changelog
-## 3.7.8
-
-### Patch Changes
+## [3.7.8]
- Add gpt-4.5-preview
From 75e7ef728d0c8512a2f8835a3139bba194f8b327 Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Thu, 27 Feb 2025 16:09:28 -0500
Subject: [PATCH 17/27] Update CHANGELOG.md
---
CHANGELOG.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fe8156abf7..9622ce0c99 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,7 +2,9 @@
## [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]
From 3514f6506b5b0e24919bad29e65b8eba11afced5 Mon Sep 17 00:00:00 2001
From: Catalin Lupuleti
Date: Thu, 27 Feb 2025 21:56:56 +0000
Subject: [PATCH 18/27] Added support for Claude Sonnet 3.7 thinking via Vertex
AI
---
package-lock.json | 10 +-
package.json | 2 +-
src/api/providers/vertex.ts | 110 +++++++++++++++---
src/core/webview/ClineProvider.ts | 5 +
src/shared/api.ts | 14 +++
src/shared/globalState.ts | 2 +
.../src/components/settings/ApiOptions.tsx | 3 +
.../components/settings/ThinkingBudget.tsx | 30 +++--
8 files changed, 143 insertions(+), 33 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index c1f748983f..547f20a930 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,7 +10,7 @@
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.10.2",
"@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",
"@google/generative-ai": "^0.18.0",
"@mistralai/mistralai": "^1.3.6",
@@ -150,11 +150,11 @@
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
},
"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": {
- "@anthropic-ai/sdk": ">=0.14 <1",
+ "@anthropic-ai/sdk": ">=0.35 <1",
"google-auth-library": "^9.4.2"
}
},
diff --git a/package.json b/package.json
index 8441488bac..35db01621a 100644
--- a/package.json
+++ b/package.json
@@ -305,7 +305,7 @@
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.10.2",
"@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",
"@google/generative-ai": "^0.18.0",
"@mistralai/mistralai": "^1.3.6",
diff --git a/src/api/providers/vertex.ts b/src/api/providers/vertex.ts
index 70562766c3..69fb7d26f7 100644
--- a/src/api/providers/vertex.ts
+++ b/src/api/providers/vertex.ts
@@ -2,6 +2,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
import { AnthropicVertex } from "@anthropic-ai/vertex-sdk"
import { Stream as AnthropicStream } from "@anthropic-ai/sdk/streaming"
import { ApiHandler, SingleCompletionHandler } from "../"
+import { BetaThinkingConfigParam } from "@anthropic-ai/sdk/resources/beta"
import { ApiHandlerOptions, ModelInfo, vertexDefaultModelId, VertexModelId, vertexModels } from "../../shared/api"
import { ApiStream } from "../transform/stream"
@@ -70,15 +71,25 @@ interface VertexMessageStreamEvent {
usage?: {
output_tokens: number
}
- content_block?: {
- type: "text"
- text: string
- }
+ content_block?:
+ | {
+ type: "text"
+ text: string
+ }
+ | {
+ type: "thinking"
+ thinking: string
+ }
index?: number
- delta?: {
- type: "text_delta"
- text: string
- }
+ delta?:
+ | {
+ type: "text_delta"
+ text: string
+ }
+ | {
+ type: "thinking_delta"
+ thinking: string
+ }
}
// https://docs.anthropic.com/en/api/claude-on-vertex-ai
@@ -145,6 +156,7 @@ export class VertexHandler implements ApiHandler, SingleCompletionHandler {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
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
@@ -158,9 +170,10 @@ export class VertexHandler implements ApiHandler, SingleCompletionHandler {
// Create the stream with appropriate caching configuration
const params = {
- model: model.id,
- max_tokens: model.info.maxTokens || 8192,
- temperature: this.options.modelTemperature ?? 0,
+ model: id,
+ max_tokens: maxTokens,
+ temperature,
+ thinking,
// Cache the system prompt if caching is enabled
system: useCache
? [
@@ -220,6 +233,19 @@ export class VertexHandler implements ApiHandler, SingleCompletionHandler {
}
break
}
+ case "thinking": {
+ if (chunk.index! > 0) {
+ yield {
+ type: "reasoning",
+ text: "\n",
+ }
+ }
+ yield {
+ type: "reasoning",
+ text: (chunk.content_block as any).thinking,
+ }
+ break
+ }
}
break
}
@@ -232,6 +258,13 @@ export class VertexHandler implements ApiHandler, SingleCompletionHandler {
}
break
}
+ case "thinking_delta": {
+ yield {
+ type: "reasoning",
+ text: (chunk.delta as any).thinking,
+ }
+ break
+ }
}
break
}
@@ -239,24 +272,63 @@ export class VertexHandler implements ApiHandler, SingleCompletionHandler {
}
}
- getModel(): { id: VertexModelId; info: ModelInfo } {
+ getModel(): {
+ id: VertexModelId
+ info: ModelInfo
+ temperature: number
+ maxTokens: number
+ thinking?: BetaThinkingConfigParam
+ } {
const modelId = this.options.apiModelId
+ let temperature = this.options.modelTemperature ?? 0
+ let thinking: BetaThinkingConfigParam | undefined = undefined
+
if (modelId && modelId in vertexModels) {
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.vertexThinking ?? this.options.anthropicThinking ?? 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 {
try {
- const model = this.getModel()
- const useCache = model.info.supportsPromptCache
+ let { id, info, temperature, maxTokens, thinking } = this.getModel()
+ const useCache = info.supportsPromptCache
const params = {
- model: model.id,
- max_tokens: model.info.maxTokens || 8192,
- temperature: this.options.modelTemperature ?? 0,
+ model: id,
+ max_tokens: maxTokens,
+ temperature,
+ thinking,
system: "", // No system prompt needed for single completions
messages: [
{
diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index 633c7d7293..5417e54ff7 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -1652,6 +1652,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
lmStudioBaseUrl,
anthropicBaseUrl,
anthropicThinking,
+ vertexThinking,
geminiApiKey,
openAiNativeApiKey,
deepSeekApiKey,
@@ -1701,6 +1702,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.updateGlobalState("lmStudioBaseUrl", lmStudioBaseUrl),
this.updateGlobalState("anthropicBaseUrl", anthropicBaseUrl),
this.updateGlobalState("anthropicThinking", anthropicThinking),
+ this.updateGlobalState("vertexThinking", vertexThinking),
this.storeSecret("geminiApiKey", geminiApiKey),
this.storeSecret("openAiNativeApiKey", openAiNativeApiKey),
this.storeSecret("deepSeekApiKey", deepSeekApiKey),
@@ -2158,6 +2160,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
lmStudioBaseUrl,
anthropicBaseUrl,
anthropicThinking,
+ vertexThinking,
geminiApiKey,
openAiNativeApiKey,
deepSeekApiKey,
@@ -2242,6 +2245,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.getGlobalState("lmStudioBaseUrl") as Promise,
this.getGlobalState("anthropicBaseUrl") as Promise,
this.getGlobalState("anthropicThinking") as Promise,
+ this.getGlobalState("vertexThinking") as Promise,
this.getSecret("geminiApiKey") as Promise,
this.getSecret("openAiNativeApiKey") as Promise,
this.getSecret("deepSeekApiKey") as Promise,
@@ -2343,6 +2347,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
lmStudioBaseUrl,
anthropicBaseUrl,
anthropicThinking,
+ vertexThinking,
geminiApiKey,
openAiNativeApiKey,
deepSeekApiKey,
diff --git a/src/shared/api.ts b/src/shared/api.ts
index 442282d587..f048761d0f 100644
--- a/src/shared/api.ts
+++ b/src/shared/api.ts
@@ -41,6 +41,7 @@ export interface ApiHandlerOptions {
awsUseProfile?: boolean
vertexProjectId?: string
vertexRegion?: string
+ vertexThinking?: number
openAiBaseUrl?: string
openAiApiKey?: string
openAiModelId?: string
@@ -436,6 +437,18 @@ export const openRouterDefaultModelInfo: ModelInfo = {
export type VertexModelId = keyof typeof vertexModels
export const vertexDefaultModelId: VertexModelId = "claude-3-7-sonnet@20250219"
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": {
maxTokens: 8192,
contextWindow: 200_000,
@@ -446,6 +459,7 @@ export const vertexModels = {
outputPrice: 15.0,
cacheWritesPrice: 3.75,
cacheReadsPrice: 0.3,
+ thinking: false,
},
"claude-3-5-sonnet-v2@20241022": {
maxTokens: 8192,
diff --git a/src/shared/globalState.ts b/src/shared/globalState.ts
index 0863b34db2..05b868a450 100644
--- a/src/shared/globalState.ts
+++ b/src/shared/globalState.ts
@@ -24,6 +24,7 @@ export type GlobalStateKey =
| "awsUseProfile"
| "vertexProjectId"
| "vertexRegion"
+ | "vertexThinking"
| "lastShownAnnouncementId"
| "customInstructions"
| "alwaysAllowReadOnly"
@@ -43,6 +44,7 @@ export type GlobalStateKey =
| "lmStudioBaseUrl"
| "anthropicBaseUrl"
| "anthropicThinking"
+ | "vertexThinking"
| "azureApiVersion"
| "openAiStreamingEnabled"
| "openRouterModelId"
diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx
index c30035cef0..42ac5cdcb3 100644
--- a/webview-ui/src/components/settings/ApiOptions.tsx
+++ b/webview-ui/src/components/settings/ApiOptions.tsx
@@ -7,6 +7,7 @@ import * as vscodemodels from "vscode"
import {
ApiConfiguration,
ModelInfo,
+ ApiProvider,
anthropicDefaultModelId,
anthropicModels,
azureOpenAiDefaultApiVersion,
@@ -1380,9 +1381,11 @@ const ApiOptions = ({
/>
(field: K, value: ApiConfiguration[K]) => void
modelInfo?: ModelInfo
+ provider?: ApiProvider
}
-export const ThinkingBudget = ({ apiConfiguration, setApiConfigurationField, modelInfo }: ThinkingBudgetProps) => {
+export const ThinkingBudget = ({
+ apiConfiguration,
+ setApiConfigurationField,
+ modelInfo,
+ provider,
+}: ThinkingBudgetProps) => {
+ const isVertexProvider = provider === "vertex"
+ const budgetField = isVertexProvider ? "vertexThinking" : "anthropicThinking"
+
const tokens = apiConfiguration?.modelMaxTokens || modelInfo?.maxTokens || 64_000
const tokensMin = 8192
const tokensMax = modelInfo?.maxTokens || 64_000
- const thinkingTokens = apiConfiguration?.anthropicThinking || 8192
+ // Get the appropriate thinking tokens based on provider
+ const thinkingTokens = useMemo(() => {
+ const value = isVertexProvider ? apiConfiguration?.vertexThinking : apiConfiguration?.anthropicThinking
+ return value || Math.min(Math.floor(0.8 * tokens), 8192)
+ }, [apiConfiguration, isVertexProvider, tokens])
+
const thinkingTokensMin = 1024
const thinkingTokensMax = Math.floor(0.8 * tokens)
useEffect(() => {
if (thinkingTokens > thinkingTokensMax) {
- setApiConfigurationField("anthropicThinking", thinkingTokensMax)
+ setApiConfigurationField(budgetField, thinkingTokensMax)
}
- }, [thinkingTokens, thinkingTokensMax, setApiConfigurationField])
+ }, [thinkingTokens, thinkingTokensMax, setApiConfigurationField, budgetField])
- if (!modelInfo || !modelInfo.thinking) {
+ if (!modelInfo?.thinking) {
return null
}
@@ -52,7 +66,7 @@ export const ThinkingBudget = ({ apiConfiguration, setApiConfigurationField, mod
max={thinkingTokensMax}
step={1024}
value={[thinkingTokens]}
- onValueChange={([value]) => setApiConfigurationField("anthropicThinking", value)}
+ onValueChange={([value]) => setApiConfigurationField(budgetField, value)}
/>
{thinkingTokens}
From 5eba1d53fbeef6f71f027d8317d9f99d120e8026 Mon Sep 17 00:00:00 2001
From: Catalin Lupuleti
Date: Thu, 27 Feb 2025 22:15:17 +0000
Subject: [PATCH 19/27] Added tests for Claude Sonnet Thinking
---
src/api/providers/__tests__/vertex.test.ts | 250 ++++++++++++++++++
.../settings/__tests__/ApiOptions.test.tsx | 57 +++-
.../__tests__/ThinkingBudget.test.tsx | 145 ++++++++++
3 files changed, 451 insertions(+), 1 deletion(-)
create mode 100644 webview-ui/src/components/settings/__tests__/ThinkingBudget.test.tsx
diff --git a/src/api/providers/__tests__/vertex.test.ts b/src/api/providers/__tests__/vertex.test.ts
index 6e81fd771b..076f902ca2 100644
--- a/src/api/providers/__tests__/vertex.test.ts
+++ b/src/api/providers/__tests__/vertex.test.ts
@@ -2,6 +2,7 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { AnthropicVertex } from "@anthropic-ai/vertex-sdk"
+import { BetaThinkingConfigParam } from "@anthropic-ai/sdk/resources/beta"
import { VertexHandler } from "../vertex"
import { ApiStreamChunk } from "../../transform/stream"
@@ -431,6 +432,138 @@ describe("VertexHandler", () => {
})
})
+ 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", () => {
it("should complete prompt successfully", async () => {
const result = await handler.completePrompt("Test prompt")
@@ -500,4 +633,121 @@ describe("VertexHandler", () => {
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,
+ vertexThinking: 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,
+ vertexThinking: 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 use anthropicThinking value if vertexThinking is not provided", () => {
+ const handler = new VertexHandler({
+ apiModelId: "claude-3-7-sonnet@20250219:thinking",
+ vertexProjectId: "test-project",
+ vertexRegion: "us-central1",
+ modelMaxTokens: 16384,
+ anthropicThinking: 6000, // Should be used as fallback
+ })
+
+ expect((handler.getModel().thinking as any).budget_tokens).toBe(6000)
+ })
+
+ 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,
+ vertexThinking: 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
+ }),
+ )
+ })
+ })
})
diff --git a/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx b/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx
index 73394bae10..65ae137003 100644
--- a/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx
+++ b/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx
@@ -46,6 +46,21 @@ jest.mock("../TemperatureControl", () => ({
),
}))
+// Mock ThinkingBudget component
+jest.mock("../ThinkingBudget", () => ({
+ ThinkingBudget: ({ apiConfiguration, setApiConfigurationField, modelInfo, provider }: any) =>
+ modelInfo?.thinking ? (
+
+
+
+ ) : null,
+}))
+
describe("ApiOptions", () => {
const renderApiOptions = (props = {}) => {
render(
@@ -72,5 +87,45 @@ describe("ApiOptions", () => {
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.
+ })
})
diff --git a/webview-ui/src/components/settings/__tests__/ThinkingBudget.test.tsx b/webview-ui/src/components/settings/__tests__/ThinkingBudget.test.tsx
new file mode 100644
index 0000000000..54f6b1037b
--- /dev/null
+++ b/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) => (
+ 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(
+ ,
+ )
+
+ expect(container.firstChild).toBeNull()
+ })
+
+ it("should render sliders when model supports thinking", () => {
+ render( )
+
+ expect(screen.getAllByTestId("slider")).toHaveLength(2)
+ })
+
+ it("should use anthropicThinking field for Anthropic provider", () => {
+ const setApiConfigurationField = jest.fn()
+
+ render(
+ ,
+ )
+
+ const sliders = screen.getAllByTestId("slider")
+ fireEvent.change(sliders[1], { target: { value: "5000" } })
+
+ expect(setApiConfigurationField).toHaveBeenCalledWith("anthropicThinking", 5000)
+ })
+
+ it("should use vertexThinking field for Vertex provider", () => {
+ const setApiConfigurationField = jest.fn()
+
+ render(
+ ,
+ )
+
+ const sliders = screen.getAllByTestId("slider")
+ fireEvent.change(sliders[1], { target: { value: "5000" } })
+
+ expect(setApiConfigurationField).toHaveBeenCalledWith("vertexThinking", 5000)
+ })
+
+ it("should cap thinking tokens at 80% of max tokens", () => {
+ const setApiConfigurationField = jest.fn()
+
+ render(
+ ,
+ )
+
+ // Effect should trigger and cap the value
+ expect(setApiConfigurationField).toHaveBeenCalledWith("anthropicThinking", 8000) // 80% of 10000
+ })
+
+ it("should use default thinking tokens if not provided", () => {
+ render( )
+
+ // 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( )
+
+ 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(
+ ,
+ )
+
+ const sliders = screen.getAllByTestId("slider")
+ fireEvent.change(sliders[0], { target: { value: "12000" } })
+
+ expect(setApiConfigurationField).toHaveBeenCalledWith("modelMaxTokens", 12000)
+ })
+})
From 2b3d23ebd750bfaf19efd6fbcc5bbcc8f1cb3aef Mon Sep 17 00:00:00 2001
From: Catalin Lupuleti <105351510+lupuletic@users.noreply.github.com>
Date: Thu, 27 Feb 2025 22:17:09 +0000
Subject: [PATCH 20/27] Update src/shared/globalState.ts
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
---
src/shared/globalState.ts | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/shared/globalState.ts b/src/shared/globalState.ts
index 05b868a450..6e29e03835 100644
--- a/src/shared/globalState.ts
+++ b/src/shared/globalState.ts
@@ -44,7 +44,6 @@ export type GlobalStateKey =
| "lmStudioBaseUrl"
| "anthropicBaseUrl"
| "anthropicThinking"
- | "vertexThinking"
| "azureApiVersion"
| "openAiStreamingEnabled"
| "openRouterModelId"
From 87b70cef83bafcf8ea4751de165ba41c35b38ba2 Mon Sep 17 00:00:00 2001
From: Catalin Lupuleti
Date: Thu, 27 Feb 2025 22:20:35 +0000
Subject: [PATCH 21/27] Removed unnecessary comment
---
src/shared/globalState.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/shared/globalState.ts b/src/shared/globalState.ts
index 6e29e03835..05b868a450 100644
--- a/src/shared/globalState.ts
+++ b/src/shared/globalState.ts
@@ -44,6 +44,7 @@ export type GlobalStateKey =
| "lmStudioBaseUrl"
| "anthropicBaseUrl"
| "anthropicThinking"
+ | "vertexThinking"
| "azureApiVersion"
| "openAiStreamingEnabled"
| "openRouterModelId"
From dd4fb6b3097430f98345e85d4c563e29baade089 Mon Sep 17 00:00:00 2001
From: Catalin Lupuleti <105351510+lupuletic@users.noreply.github.com>
Date: Thu, 27 Feb 2025 22:45:12 +0000
Subject: [PATCH 22/27] Update src/shared/globalState.ts
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
---
src/shared/globalState.ts | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/shared/globalState.ts b/src/shared/globalState.ts
index 05b868a450..6e29e03835 100644
--- a/src/shared/globalState.ts
+++ b/src/shared/globalState.ts
@@ -44,7 +44,6 @@ export type GlobalStateKey =
| "lmStudioBaseUrl"
| "anthropicBaseUrl"
| "anthropicThinking"
- | "vertexThinking"
| "azureApiVersion"
| "openAiStreamingEnabled"
| "openRouterModelId"
From 8cbce2ded08e107454dee7d2eec256973d3e85e0 Mon Sep 17 00:00:00 2001
From: cte
Date: Thu, 27 Feb 2025 16:06:47 -0800
Subject: [PATCH 23/27] Add provider-agnostic modelMaxThinkingTokens setting
---
src/api/providers/__tests__/vertex.test.ts | 18 +++---------------
src/api/providers/anthropic.ts | 2 +-
src/api/providers/openrouter.ts | 2 +-
src/api/providers/vertex.ts | 5 +----
src/core/webview/ClineProvider.ts | 15 +++++----------
.../__tests__/checkExistApiConfig.test.ts | 2 +-
src/shared/api.ts | 3 +--
src/shared/globalState.ts | 3 +--
.../src/components/settings/ThinkingBudget.tsx | 13 +++++--------
.../settings/__tests__/ApiOptions.test.tsx | 7 +------
.../settings/__tests__/ThinkingBudget.test.tsx | 16 ++++++++--------
11 files changed, 28 insertions(+), 58 deletions(-)
diff --git a/src/api/providers/__tests__/vertex.test.ts b/src/api/providers/__tests__/vertex.test.ts
index 076f902ca2..9cf92f0a16 100644
--- a/src/api/providers/__tests__/vertex.test.ts
+++ b/src/api/providers/__tests__/vertex.test.ts
@@ -641,7 +641,7 @@ describe("VertexHandler", () => {
vertexProjectId: "test-project",
vertexRegion: "us-central1",
modelMaxTokens: 16384,
- vertexThinking: 4096,
+ modelMaxThinkingTokens: 4096,
})
const modelInfo = thinkingHandler.getModel()
@@ -662,7 +662,7 @@ describe("VertexHandler", () => {
vertexProjectId: "test-project",
vertexRegion: "us-central1",
modelMaxTokens: 16384,
- vertexThinking: 5000,
+ modelMaxThinkingTokens: 5000,
})
expect((handlerWithBudget.getModel().thinking as any).budget_tokens).toBe(5000)
@@ -688,25 +688,13 @@ describe("VertexHandler", () => {
expect((handlerWithSmallMaxTokens.getModel().thinking as any).budget_tokens).toBe(1024)
})
- it("should use anthropicThinking value if vertexThinking is not provided", () => {
- const handler = new VertexHandler({
- apiModelId: "claude-3-7-sonnet@20250219:thinking",
- vertexProjectId: "test-project",
- vertexRegion: "us-central1",
- modelMaxTokens: 16384,
- anthropicThinking: 6000, // Should be used as fallback
- })
-
- expect((handler.getModel().thinking as any).budget_tokens).toBe(6000)
- })
-
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,
- vertexThinking: 4096,
+ modelMaxThinkingTokens: 4096,
})
const mockCreate = jest.fn().mockImplementation(async (options) => {
diff --git a/src/api/providers/anthropic.ts b/src/api/providers/anthropic.ts
index eca81eab2e..fc0b99c59b 100644
--- a/src/api/providers/anthropic.ts
+++ b/src/api/providers/anthropic.ts
@@ -206,7 +206,7 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler {
// least 1024 tokens.
const maxBudgetTokens = Math.floor(maxTokens * 0.8)
const budgetTokens = Math.max(
- Math.min(this.options.anthropicThinking ?? maxBudgetTokens, maxBudgetTokens),
+ Math.min(this.options.modelMaxThinkingTokens ?? maxBudgetTokens, maxBudgetTokens),
1024,
)
diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts
index 69bcb0074c..82c02e20a7 100644
--- a/src/api/providers/openrouter.ts
+++ b/src/api/providers/openrouter.ts
@@ -117,7 +117,7 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler {
// least 1024 tokens.
const maxBudgetTokens = Math.floor((maxTokens || 8192) * 0.8)
const budgetTokens = Math.max(
- Math.min(this.options.anthropicThinking ?? maxBudgetTokens, maxBudgetTokens),
+ Math.min(this.options.modelMaxThinkingTokens ?? maxBudgetTokens, maxBudgetTokens),
1024,
)
diff --git a/src/api/providers/vertex.ts b/src/api/providers/vertex.ts
index 69fb7d26f7..a25fad07ee 100644
--- a/src/api/providers/vertex.ts
+++ b/src/api/providers/vertex.ts
@@ -300,10 +300,7 @@ export class VertexHandler implements ApiHandler, SingleCompletionHandler {
temperature = 1.0 // Thinking requires temperature 1.0
const maxBudgetTokens = Math.floor(maxTokens * 0.8)
const budgetTokens = Math.max(
- Math.min(
- this.options.vertexThinking ?? this.options.anthropicThinking ?? maxBudgetTokens,
- maxBudgetTokens,
- ),
+ Math.min(this.options.modelMaxThinkingTokens ?? maxBudgetTokens, maxBudgetTokens),
1024,
)
thinking = { type: "enabled", budget_tokens: budgetTokens }
diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index 5417e54ff7..7b6f2c8971 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -1651,8 +1651,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
lmStudioModelId,
lmStudioBaseUrl,
anthropicBaseUrl,
- anthropicThinking,
- vertexThinking,
geminiApiKey,
openAiNativeApiKey,
deepSeekApiKey,
@@ -1673,6 +1671,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
requestyModelInfo,
modelTemperature,
modelMaxTokens,
+ modelMaxThinkingTokens,
} = apiConfiguration
await Promise.all([
this.updateGlobalState("apiProvider", apiProvider),
@@ -1701,8 +1700,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.updateGlobalState("lmStudioModelId", lmStudioModelId),
this.updateGlobalState("lmStudioBaseUrl", lmStudioBaseUrl),
this.updateGlobalState("anthropicBaseUrl", anthropicBaseUrl),
- this.updateGlobalState("anthropicThinking", anthropicThinking),
- this.updateGlobalState("vertexThinking", vertexThinking),
this.storeSecret("geminiApiKey", geminiApiKey),
this.storeSecret("openAiNativeApiKey", openAiNativeApiKey),
this.storeSecret("deepSeekApiKey", deepSeekApiKey),
@@ -1723,6 +1720,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.updateGlobalState("requestyModelInfo", requestyModelInfo),
this.updateGlobalState("modelTemperature", modelTemperature),
this.updateGlobalState("modelMaxTokens", modelMaxTokens),
+ this.updateGlobalState("anthropicThinking", modelMaxThinkingTokens),
])
if (this.cline) {
this.cline.api = buildApiHandler(apiConfiguration)
@@ -2159,8 +2157,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
lmStudioModelId,
lmStudioBaseUrl,
anthropicBaseUrl,
- anthropicThinking,
- vertexThinking,
geminiApiKey,
openAiNativeApiKey,
deepSeekApiKey,
@@ -2216,6 +2212,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
requestyModelInfo,
modelTemperature,
modelMaxTokens,
+ modelMaxThinkingTokens,
maxOpenTabsContext,
] = await Promise.all([
this.getGlobalState("apiProvider") as Promise,
@@ -2244,8 +2241,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.getGlobalState("lmStudioModelId") as Promise,
this.getGlobalState("lmStudioBaseUrl") as Promise,
this.getGlobalState("anthropicBaseUrl") as Promise,
- this.getGlobalState("anthropicThinking") as Promise,
- this.getGlobalState("vertexThinking") as Promise,
this.getSecret("geminiApiKey") as Promise,
this.getSecret("openAiNativeApiKey") as Promise,
this.getSecret("deepSeekApiKey") as Promise,
@@ -2301,6 +2296,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.getGlobalState("requestyModelInfo") as Promise,
this.getGlobalState("modelTemperature") as Promise,
this.getGlobalState("modelMaxTokens") as Promise,
+ this.getGlobalState("anthropicThinking") as Promise,
this.getGlobalState("maxOpenTabsContext") as Promise,
])
@@ -2346,8 +2342,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
lmStudioModelId,
lmStudioBaseUrl,
anthropicBaseUrl,
- anthropicThinking,
- vertexThinking,
geminiApiKey,
openAiNativeApiKey,
deepSeekApiKey,
@@ -2368,6 +2362,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
requestyModelInfo,
modelTemperature,
modelMaxTokens,
+ modelMaxThinkingTokens,
},
lastShownAnnouncementId,
customInstructions,
diff --git a/src/shared/__tests__/checkExistApiConfig.test.ts b/src/shared/__tests__/checkExistApiConfig.test.ts
index 62517d6958..c99ddddbc4 100644
--- a/src/shared/__tests__/checkExistApiConfig.test.ts
+++ b/src/shared/__tests__/checkExistApiConfig.test.ts
@@ -32,7 +32,7 @@ describe("checkExistKey", () => {
apiKey: "test-key",
apiProvider: undefined,
anthropicBaseUrl: undefined,
- anthropicThinking: undefined,
+ modelMaxThinkingTokens: undefined,
}
expect(checkExistKey(config)).toBe(true)
})
diff --git a/src/shared/api.ts b/src/shared/api.ts
index b36781d630..f88bb5e8b5 100644
--- a/src/shared/api.ts
+++ b/src/shared/api.ts
@@ -21,7 +21,6 @@ export interface ApiHandlerOptions {
apiModelId?: string
apiKey?: string // anthropic
anthropicBaseUrl?: string
- anthropicThinking?: number
vsCodeLmModelSelector?: vscode.LanguageModelChatSelector
glamaModelId?: string
glamaModelInfo?: ModelInfo
@@ -41,7 +40,6 @@ export interface ApiHandlerOptions {
awsUseProfile?: boolean
vertexProjectId?: string
vertexRegion?: string
- vertexThinking?: number
openAiBaseUrl?: string
openAiApiKey?: string
openAiModelId?: string
@@ -70,6 +68,7 @@ export interface ApiHandlerOptions {
requestyModelInfo?: ModelInfo
modelTemperature?: number
modelMaxTokens?: number
+ modelMaxThinkingTokens?: number
}
export type ApiConfiguration = ApiHandlerOptions & {
diff --git a/src/shared/globalState.ts b/src/shared/globalState.ts
index 6e29e03835..aabc77cc01 100644
--- a/src/shared/globalState.ts
+++ b/src/shared/globalState.ts
@@ -24,7 +24,6 @@ export type GlobalStateKey =
| "awsUseProfile"
| "vertexProjectId"
| "vertexRegion"
- | "vertexThinking"
| "lastShownAnnouncementId"
| "customInstructions"
| "alwaysAllowReadOnly"
@@ -43,7 +42,6 @@ export type GlobalStateKey =
| "lmStudioModelId"
| "lmStudioBaseUrl"
| "anthropicBaseUrl"
- | "anthropicThinking"
| "azureApiVersion"
| "openAiStreamingEnabled"
| "openRouterModelId"
@@ -83,5 +81,6 @@ export type GlobalStateKey =
| "unboundModelInfo"
| "modelTemperature"
| "modelMaxTokens"
+ | "anthropicThinking" // TODO: Rename to `modelMaxThinkingTokens`.
| "mistralCodestralUrl"
| "maxOpenTabsContext"
diff --git a/webview-ui/src/components/settings/ThinkingBudget.tsx b/webview-ui/src/components/settings/ThinkingBudget.tsx
index d21e1fb7ea..557a69538d 100644
--- a/webview-ui/src/components/settings/ThinkingBudget.tsx
+++ b/webview-ui/src/components/settings/ThinkingBudget.tsx
@@ -17,27 +17,24 @@ export const ThinkingBudget = ({
modelInfo,
provider,
}: ThinkingBudgetProps) => {
- const isVertexProvider = provider === "vertex"
- const budgetField = isVertexProvider ? "vertexThinking" : "anthropicThinking"
-
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 = isVertexProvider ? apiConfiguration?.vertexThinking : apiConfiguration?.anthropicThinking
+ const value = apiConfiguration?.modelMaxThinkingTokens
return value || Math.min(Math.floor(0.8 * tokens), 8192)
- }, [apiConfiguration, isVertexProvider, tokens])
+ }, [apiConfiguration, tokens])
const thinkingTokensMin = 1024
const thinkingTokensMax = Math.floor(0.8 * tokens)
useEffect(() => {
if (thinkingTokens > thinkingTokensMax) {
- setApiConfigurationField(budgetField, thinkingTokensMax)
+ setApiConfigurationField("modelMaxThinkingTokens", thinkingTokensMax)
}
- }, [thinkingTokens, thinkingTokensMax, setApiConfigurationField, budgetField])
+ }, [thinkingTokens, thinkingTokensMax, setApiConfigurationField])
if (!modelInfo?.thinking) {
return null
@@ -66,7 +63,7 @@ export const ThinkingBudget = ({
max={thinkingTokensMax}
step={1024}
value={[thinkingTokens]}
- onValueChange={([value]) => setApiConfigurationField(budgetField, value)}
+ onValueChange={([value]) => setApiConfigurationField("modelMaxThinkingTokens", value)}
/>
{thinkingTokens}
diff --git a/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx b/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx
index 65ae137003..06ed95585a 100644
--- a/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx
+++ b/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx
@@ -51,12 +51,7 @@ jest.mock("../ThinkingBudget", () => ({
ThinkingBudget: ({ apiConfiguration, setApiConfigurationField, modelInfo, provider }: any) =>
modelInfo?.thinking ? (
-
+
) : null,
}))
diff --git a/webview-ui/src/components/settings/__tests__/ThinkingBudget.test.tsx b/webview-ui/src/components/settings/__tests__/ThinkingBudget.test.tsx
index 54f6b1037b..212316ea9a 100644
--- a/webview-ui/src/components/settings/__tests__/ThinkingBudget.test.tsx
+++ b/webview-ui/src/components/settings/__tests__/ThinkingBudget.test.tsx
@@ -60,13 +60,13 @@ describe("ThinkingBudget", () => {
expect(screen.getAllByTestId("slider")).toHaveLength(2)
})
- it("should use anthropicThinking field for Anthropic provider", () => {
+ it("should use modelMaxThinkingTokens field for Anthropic provider", () => {
const setApiConfigurationField = jest.fn()
render(
,
@@ -75,16 +75,16 @@ describe("ThinkingBudget", () => {
const sliders = screen.getAllByTestId("slider")
fireEvent.change(sliders[1], { target: { value: "5000" } })
- expect(setApiConfigurationField).toHaveBeenCalledWith("anthropicThinking", 5000)
+ expect(setApiConfigurationField).toHaveBeenCalledWith("modelMaxThinkingTokens", 5000)
})
- it("should use vertexThinking field for Vertex provider", () => {
+ it("should use modelMaxThinkingTokens field for Vertex provider", () => {
const setApiConfigurationField = jest.fn()
render(
,
@@ -93,7 +93,7 @@ describe("ThinkingBudget", () => {
const sliders = screen.getAllByTestId("slider")
fireEvent.change(sliders[1], { target: { value: "5000" } })
- expect(setApiConfigurationField).toHaveBeenCalledWith("vertexThinking", 5000)
+ expect(setApiConfigurationField).toHaveBeenCalledWith("modelMaxThinkingTokens", 5000)
})
it("should cap thinking tokens at 80% of max tokens", () => {
@@ -102,13 +102,13 @@ describe("ThinkingBudget", () => {
render(
,
)
// Effect should trigger and cap the value
- expect(setApiConfigurationField).toHaveBeenCalledWith("anthropicThinking", 8000) // 80% of 10000
+ expect(setApiConfigurationField).toHaveBeenCalledWith("modelMaxThinkingTokens", 8000) // 80% of 10000
})
it("should use default thinking tokens if not provided", () => {
From 21fed4cb799ff8397d5d1f1348252156dfcbbf71 Mon Sep 17 00:00:00 2001
From: Chris Estreich
Date: Thu, 27 Feb 2025 22:26:13 -0800
Subject: [PATCH 24/27] Delete task confirmation enhancements
---
.changeset/chilly-bugs-pay.md | 5 ++
webview-ui/src/components/chat/TaskHeader.tsx | 86 +++++++++++++------
.../components/history/DeleteTaskDialog.tsx | 42 ++++++---
.../src/components/history/HistoryView.tsx | 30 +++----
.../history/__tests__/HistoryView.test.tsx | 60 +++++++++----
5 files changed, 149 insertions(+), 74 deletions(-)
create mode 100644 .changeset/chilly-bugs-pay.md
diff --git a/.changeset/chilly-bugs-pay.md b/.changeset/chilly-bugs-pay.md
new file mode 100644
index 0000000000..b30f8241ef
--- /dev/null
+++ b/.changeset/chilly-bugs-pay.md
@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+Delete task confirmation enhancements
diff --git a/webview-ui/src/components/chat/TaskHeader.tsx b/webview-ui/src/components/chat/TaskHeader.tsx
index fb7db6f617..319a9aeccd 100644
--- a/webview-ui/src/components/chat/TaskHeader.tsx
+++ b/webview-ui/src/components/chat/TaskHeader.tsx
@@ -3,16 +3,19 @@ import { useWindowSize } from "react-use"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import prettyBytes from "pretty-bytes"
+import { vscode } from "@/utils/vscode"
+import { formatLargeNumber } from "@/utils/format"
+import { Button } from "@/components/ui"
+
import { ClineMessage } from "../../../../src/shared/ExtensionMessage"
-import { useExtensionState } from "../../context/ExtensionStateContext"
-import { vscode } from "../../utils/vscode"
-import Thumbnails from "../common/Thumbnails"
import { mentionRegexGlobal } from "../../../../src/shared/context-mentions"
-import { formatLargeNumber } from "../../utils/format"
-import { normalizeApiConfiguration } from "../settings/ApiOptions"
-import { Button } from "../ui"
import { HistoryItem } from "../../../../src/shared/HistoryItem"
+import { useExtensionState } from "../../context/ExtensionStateContext"
+import Thumbnails from "../common/Thumbnails"
+import { normalizeApiConfiguration } from "../settings/ApiOptions"
+import { DeleteTaskDialog } from "../history/DeleteTaskDialog"
+
interface TaskHeaderProps {
task: ClineMessage
tokensIn: number
@@ -46,7 +49,21 @@ const TaskHeader: React.FC = ({
const contextWindow = selectedModelInfo?.contextWindow || 1
/*
- When dealing with event listeners in React components that depend on state variables, we face a challenge. We want our listener to always use the most up-to-date version of a callback function that relies on current state, but we don't want to constantly add and remove event listeners as that function updates. This scenario often arises with resize listeners or other window events. Simply adding the listener in a useEffect with an empty dependency array risks using stale state, while including the callback in the dependencies can lead to unnecessary re-registrations of the listener. There are react hook libraries that provide a elegant solution to this problem by utilizing the useRef hook to maintain a reference to the latest callback function without triggering re-renders or effect re-runs. This approach ensures that our event listener always has access to the most current state while minimizing performance overhead and potential memory leaks from multiple listener registrations.
+ When dealing with event listeners in React components that depend on state
+ variables, we face a challenge. We want our listener to always use the most
+ up-to-date version of a callback function that relies on current state, but
+ we don't want to constantly add and remove event listeners as that function
+ updates. This scenario often arises with resize listeners or other window
+ events. Simply adding the listener in a useEffect with an empty dependency
+ array risks using stale state, while including the callback in the
+ dependencies can lead to unnecessary re-registrations of the listener. There
+ are react hook libraries that provide a elegant solution to this problem by
+ utilizing the useRef hook to maintain a reference to the latest callback
+ function without triggering re-renders or effect re-runs. This approach
+ ensures that our event listener always has access to the most current state
+ while minimizing performance overhead and potential memory leaks from
+ multiple listener registrations.
+
Sources
- https://usehooks-ts.com/react-hook/use-event-listener
- https://streamich.github.io/react-use/?path=/story/sensors-useevent--docs
@@ -350,27 +367,48 @@ export const highlightMentions = (text?: string, withShadow = true) => {
})
}
-const TaskActions = ({ item }: { item: HistoryItem | undefined }) => (
-
-
vscode.postMessage({ type: "exportCurrentTask" })}>
-
-
- {!!item?.size && item.size > 0 && (
+const TaskActions = ({ item }: { item: HistoryItem | undefined }) => {
+ const [deleteTaskId, setDeleteTaskId] = useState
(null)
+
+ return (
+
vscode.postMessage({ type: "deleteTaskWithId", text: item.id })}>
-
- {prettyBytes(item.size)}
+ title="Export task history"
+ onClick={() => vscode.postMessage({ type: "exportCurrentTask" })}>
+
- )}
-
-)
+ {!!item?.size && item.size > 0 && (
+ <>
+ {
+ e.stopPropagation()
+
+ if (e.shiftKey) {
+ vscode.postMessage({ type: "deleteTaskWithId", text: item.id })
+ } else {
+ setDeleteTaskId(item.id)
+ }
+ }}>
+
+ {prettyBytes(item.size)}
+
+ {deleteTaskId && (
+ !open && setDeleteTaskId(null)}
+ open
+ />
+ )}
+ >
+ )}
+
+ )
+}
const ContextWindowProgress = ({ contextWindow, contextTokens }: { contextWindow: number; contextTokens: number }) => (
<>
diff --git a/webview-ui/src/components/history/DeleteTaskDialog.tsx b/webview-ui/src/components/history/DeleteTaskDialog.tsx
index b40adeae3d..31d85abd37 100644
--- a/webview-ui/src/components/history/DeleteTaskDialog.tsx
+++ b/webview-ui/src/components/history/DeleteTaskDialog.tsx
@@ -1,4 +1,7 @@
-import React from "react"
+import { useCallback, useEffect } from "react"
+import { useKeyPress } from "react-use"
+import { AlertDialogProps } from "@radix-ui/react-alert-dialog"
+
import {
AlertDialog,
AlertDialogAction,
@@ -8,25 +11,36 @@ import {
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
-} from "@/components/ui/alert-dialog"
-import { Button } from "@/components/ui"
+ Button,
+} from "@/components/ui"
+
import { vscode } from "@/utils/vscode"
-interface DeleteTaskDialogProps {
+interface DeleteTaskDialogProps extends AlertDialogProps {
taskId: string
- open: boolean
- onOpenChange: (open: boolean) => void
}
-export const DeleteTaskDialog = ({ taskId, open, onOpenChange }: DeleteTaskDialogProps) => {
- const handleDelete = () => {
- vscode.postMessage({ type: "deleteTaskWithId", text: taskId })
- onOpenChange(false)
- }
+export const DeleteTaskDialog = ({ taskId, ...props }: DeleteTaskDialogProps) => {
+ const [isEnterPressed] = useKeyPress("Enter")
+
+ const { onOpenChange } = props
+
+ const onDelete = useCallback(() => {
+ if (taskId) {
+ vscode.postMessage({ type: "deleteTaskWithId", text: taskId })
+ onOpenChange?.(false)
+ }
+ }, [taskId, onOpenChange])
+
+ useEffect(() => {
+ if (taskId && isEnterPressed) {
+ onDelete()
+ }
+ }, [taskId, isEnterPressed, onDelete])
return (
-
-
+
+ onOpenChange?.(false)}>
Delete Task
@@ -38,7 +52,7 @@ export const DeleteTaskDialog = ({ taskId, open, onOpenChange }: DeleteTaskDialo
Cancel
-
+
Delete
diff --git a/webview-ui/src/components/history/HistoryView.tsx b/webview-ui/src/components/history/HistoryView.tsx
index ca60e1fcb8..49d71e5ddd 100644
--- a/webview-ui/src/components/history/HistoryView.tsx
+++ b/webview-ui/src/components/history/HistoryView.tsx
@@ -38,13 +38,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
vscode.postMessage({ type: "showTaskWithId", text: id })
}
- const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
- const [taskToDelete, setTaskToDelete] = useState(null)
-
- const handleDeleteHistoryItem = (id: string) => {
- setTaskToDelete(id)
- setDeleteDialogOpen(true)
- }
+ const [deleteTaskId, setDeleteTaskId] = useState(null)
const formatDate = (timestamp: number) => {
const date = new Date(timestamp)
@@ -230,10 +224,15 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
{
e.stopPropagation()
- handleDeleteHistoryItem(item.id)
+
+ if (e.shiftKey) {
+ vscode.postMessage({ type: "deleteTaskWithId", text: item.id })
+ } else {
+ setDeleteTaskId(item.id)
+ }
}}>
{item.size && prettyBytes(item.size)}
@@ -403,17 +402,8 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
)}
/>
- {taskToDelete && (
- {
- setDeleteDialogOpen(open)
- if (!open) {
- setTaskToDelete(null)
- }
- }}
- />
+ {deleteTaskId && (
+ !open && setDeleteTaskId(null)} open />
)}
)
diff --git a/webview-ui/src/components/history/__tests__/HistoryView.test.tsx b/webview-ui/src/components/history/__tests__/HistoryView.test.tsx
index 12b0181af6..4b761d6fc4 100644
--- a/webview-ui/src/components/history/__tests__/HistoryView.test.tsx
+++ b/webview-ui/src/components/history/__tests__/HistoryView.test.tsx
@@ -135,26 +135,54 @@ describe("HistoryView", () => {
})
})
- it("handles task deletion", async () => {
- const onDone = jest.fn()
- render( )
+ describe("task deletion", () => {
+ it("shows confirmation dialog on regular click", () => {
+ const onDone = jest.fn()
+ render( )
- // Find and hover over first task
- const taskContainer = screen.getByTestId("virtuoso-item-1")
- fireEvent.mouseEnter(taskContainer)
+ // Find and hover over first task
+ const taskContainer = screen.getByTestId("virtuoso-item-1")
+ fireEvent.mouseEnter(taskContainer)
- // Click delete button to open confirmation dialog
- const deleteButton = within(taskContainer).getByTitle("Delete Task")
- fireEvent.click(deleteButton)
+ // Click delete button to open confirmation dialog
+ const deleteButton = within(taskContainer).getByTitle("Delete Task (Shift + Click to skip confirmation)")
+ fireEvent.click(deleteButton)
- // Find and click the confirm delete button in the dialog
- const confirmDeleteButton = screen.getByRole("button", { name: /delete/i })
- fireEvent.click(confirmDeleteButton)
+ // Verify dialog is shown
+ const dialog = screen.getByRole("alertdialog")
+ expect(dialog).toBeInTheDocument()
- // Verify vscode message was sent
- expect(vscode.postMessage).toHaveBeenCalledWith({
- type: "deleteTaskWithId",
- text: "1",
+ // Find and click the confirm delete button in the dialog
+ const confirmDeleteButton = within(dialog).getByRole("button", { name: /delete/i })
+ fireEvent.click(confirmDeleteButton)
+
+ // Verify vscode message was sent
+ expect(vscode.postMessage).toHaveBeenCalledWith({
+ type: "deleteTaskWithId",
+ text: "1",
+ })
+ })
+
+ it("deletes immediately on shift-click without confirmation", () => {
+ const onDone = jest.fn()
+ render( )
+
+ // Find and hover over first task
+ const taskContainer = screen.getByTestId("virtuoso-item-1")
+ fireEvent.mouseEnter(taskContainer)
+
+ // Shift-click delete button
+ const deleteButton = within(taskContainer).getByTitle("Delete Task (Shift + Click to skip confirmation)")
+ fireEvent.click(deleteButton, { shiftKey: true })
+
+ // Verify no dialog is shown
+ expect(screen.queryByRole("alertdialog")).not.toBeInTheDocument()
+
+ // Verify vscode message was sent
+ expect(vscode.postMessage).toHaveBeenCalledWith({
+ type: "deleteTaskWithId",
+ text: "1",
+ })
})
})
From b3fd1a2e232f059b9ba6d480795445624ff8206c Mon Sep 17 00:00:00 2001
From: Chris Estreich
Date: Fri, 28 Feb 2025 09:08:58 -0800
Subject: [PATCH 25/27] Prettier thinking blocks
---
webview-ui/src/components/chat/ChatRow.tsx | 59 +++------
.../src/components/chat/ReasoningBlock.tsx | 117 +++++++++++-------
webview-ui/src/index.css | 2 +
3 files changed, 89 insertions(+), 89 deletions(-)
diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx
index 4017ccf318..1533bba3a8 100644
--- a/webview-ui/src/components/chat/ChatRow.tsx
+++ b/webview-ui/src/components/chat/ChatRow.tsx
@@ -16,7 +16,7 @@ import { vscode } from "../../utils/vscode"
import CodeAccordian, { removeLeadingNonAlphanumeric } from "../common/CodeAccordian"
import CodeBlock, { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
import MarkdownBlock from "../common/MarkdownBlock"
-import ReasoningBlock from "./ReasoningBlock"
+import { ReasoningBlock } from "./ReasoningBlock"
import Thumbnails from "../common/Thumbnails"
import McpResourceRow from "../mcp/McpResourceRow"
import McpToolRow from "../mcp/McpToolRow"
@@ -25,12 +25,12 @@ import { CheckpointSaved } from "./checkpoints/CheckpointSaved"
interface ChatRowProps {
message: ClineMessage
- isExpanded: boolean
- onToggleExpand: () => void
lastModifiedMessage?: ClineMessage
+ isExpanded: boolean
isLast: boolean
- onHeightChange: (isTaller: boolean) => void
isStreaming: boolean
+ onToggleExpand: () => void
+ onHeightChange: (isTaller: boolean) => void
}
interface ChatRowContentProps extends Omit {}
@@ -43,10 +43,7 @@ const ChatRow = memo(
const prevHeightRef = useRef(0)
const [chatrow, { height }] = useSize(
-
+
,
)
@@ -75,33 +72,32 @@ export default ChatRow
export const ChatRowContent = ({
message,
- isExpanded,
- onToggleExpand,
lastModifiedMessage,
+ isExpanded,
isLast,
isStreaming,
+ onToggleExpand,
}: ChatRowContentProps) => {
const { mcpServers, alwaysAllowMcp, currentCheckpoint } = useExtensionState()
- const [reasoningCollapsed, setReasoningCollapsed] = useState(false)
+ const [reasoningCollapsed, setReasoningCollapsed] = useState(true)
- // Auto-collapse reasoning when new messages arrive
- useEffect(() => {
- if (!isLast && message.say === "reasoning") {
- setReasoningCollapsed(true)
- }
- }, [isLast, message.say])
const [cost, apiReqCancelReason, apiReqStreamingFailedMessage] = useMemo(() => {
if (message.text !== null && message.text !== undefined && message.say === "api_req_started") {
const info: ClineApiReqInfo = JSON.parse(message.text)
return [info.cost, info.cancelReason, info.streamingFailedMessage]
}
+
return [undefined, undefined, undefined]
}, [message.text, message.say])
- // when resuming task, last wont be api_req_failed but a resume_task message, so api_req_started will show loading spinner. that's why we just remove the last api_req_started that failed without streaming anything
+
+ // When resuming task, last wont be api_req_failed but a resume_task
+ // message, so api_req_started will show loading spinner. That's why we just
+ // remove the last api_req_started that failed without streaming anything.
const apiRequestFailedMessage =
isLast && lastModifiedMessage?.ask === "api_req_failed" // if request is retried then the latest message is a api_req_retried
? lastModifiedMessage?.text
: undefined
+
const isCommandExecuting =
isLast && lastModifiedMessage?.ask === "command" && lastModifiedMessage?.text?.includes(COMMAND_OUTPUT_STRING)
@@ -428,32 +424,6 @@ export const ChatRowContent = ({
/>
>
)
- // case "inspectSite":
- // const isInspecting =
- // isLast && lastModifiedMessage?.say === "inspect_site_result" && !lastModifiedMessage?.images
- // return (
- // <>
- //
- // {isInspecting ?
: toolIcon("inspect")}
- //
- // {message.type === "ask" ? (
- // <>Roo wants to inspect this website:>
- // ) : (
- // <>Roo is inspecting this website:>
- // )}
- //
- //
- //
- //
- //
- // >
- // )
case "switchMode":
return (
<>
@@ -501,6 +471,7 @@ export const ChatRowContent = ({
return (
setReasoningCollapsed(!reasoningCollapsed)}
/>
diff --git a/webview-ui/src/components/chat/ReasoningBlock.tsx b/webview-ui/src/components/chat/ReasoningBlock.tsx
index 0c9971f269..fa12899092 100644
--- a/webview-ui/src/components/chat/ReasoningBlock.tsx
+++ b/webview-ui/src/components/chat/ReasoningBlock.tsx
@@ -1,70 +1,97 @@
-import React, { useEffect, useRef } from "react"
-import { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
+import { useCallback, useEffect, useRef, useState } from "react"
+import { CaretDownIcon, CaretUpIcon, CounterClockwiseClockIcon } from "@radix-ui/react-icons"
+
import MarkdownBlock from "../common/MarkdownBlock"
+import { useMount } from "react-use"
interface ReasoningBlockProps {
content: string
+ elapsed?: number
isCollapsed?: boolean
onToggleCollapse?: () => void
- autoHeight?: boolean
}
-const ReasoningBlock: React.FC = ({
- content,
- isCollapsed = false,
- onToggleCollapse,
- autoHeight = false,
-}) => {
+export const ReasoningBlock = ({ content, elapsed, isCollapsed = false, onToggleCollapse }: ReasoningBlockProps) => {
const contentRef = useRef(null)
+ const elapsedRef = useRef(0)
+ const [thought, setThought] = useState()
+ const [prevThought, setPrevThought] = useState("Thinking")
+ const [isTransitioning, setIsTransitioning] = useState(false)
+ const cursorRef = useRef(0)
+ const queueRef = useRef([])
- // Scroll to bottom when content updates
useEffect(() => {
if (contentRef.current && !isCollapsed) {
contentRef.current.scrollTop = contentRef.current.scrollHeight
}
}, [content, isCollapsed])
+ useEffect(() => {
+ if (elapsed) {
+ elapsedRef.current = elapsed
+ }
+ }, [elapsed])
+
+ // Process the transition queue.
+ const processNextTransition = useCallback(() => {
+ const nextThought = queueRef.current.pop()
+ queueRef.current = []
+
+ if (nextThought) {
+ setIsTransitioning(true)
+ }
+
+ setTimeout(() => {
+ if (nextThought) {
+ setPrevThought(nextThought)
+ setIsTransitioning(false)
+ }
+
+ setTimeout(() => processNextTransition(), 500)
+ }, 200)
+ }, [])
+
+ useMount(() => {
+ processNextTransition()
+ })
+
+ useEffect(() => {
+ if (content.length - cursorRef.current > 160) {
+ setThought("... " + content.slice(cursorRef.current))
+ cursorRef.current = content.length
+ }
+ }, [content])
+
+ useEffect(() => {
+ if (thought && thought !== prevThought) {
+ queueRef.current.push(thought)
+ }
+ }, [thought, prevThought])
+
return (
-
+
-
Reasoning
-
+ className="flex items-center justify-between gap-1 px-3 py-2 cursor-pointer text-muted-foreground"
+ onClick={onToggleCollapse}>
+
+ {prevThought}
+
+
+ {elapsedRef.current > 1000 && (
+ <>
+
+
{Math.round(elapsedRef.current / 1000)}s
+ >
+ )}
+ {isCollapsed ?
:
}
+
{!isCollapsed && (
-
)
}
-
-export default ReasoningBlock
diff --git a/webview-ui/src/index.css b/webview-ui/src/index.css
index 53025be01a..74c8463b37 100644
--- a/webview-ui/src/index.css
+++ b/webview-ui/src/index.css
@@ -64,6 +64,8 @@
--color-vscode-editor-foreground: var(--vscode-editor-foreground);
--color-vscode-editor-background: var(--vscode-editor-background);
+ --color-vscode-editorGroup-border: var(--vscode-editorGroup-border);
+
--color-vscode-button-foreground: var(--vscode-button-foreground);
--color-vscode-button-background: var(--vscode-button-background);
--color-vscode-button-secondaryForeground: var(--vscode-button-secondaryForeground);
From 360e47d641e9acbb33586342a079efaf150ef85f Mon Sep 17 00:00:00 2001
From: Chris Estreich
Date: Fri, 28 Feb 2025 09:09:42 -0800
Subject: [PATCH 26/27] Add changeset
---
.changeset/young-hornets-taste.md | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 .changeset/young-hornets-taste.md
diff --git a/.changeset/young-hornets-taste.md b/.changeset/young-hornets-taste.md
new file mode 100644
index 0000000000..1b9c3d94e8
--- /dev/null
+++ b/.changeset/young-hornets-taste.md
@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+Prettier thinking blocks
From 9b30065231061dfbcae9d8ef6e14082fe9064757 Mon Sep 17 00:00:00 2001
From: Chris Estreich
Date: Fri, 28 Feb 2025 08:25:04 -0800
Subject: [PATCH 27/27] Make the copy action on history and history preview
consistent
---
.../src/components/history/CopyButton.tsx | 32 +++++
.../src/components/history/ExportButton.tsx | 16 +++
.../src/components/history/HistoryPreview.tsx | 124 ++++--------------
.../src/components/history/HistoryView.tsx | 51 +------
webview-ui/src/index.css | 2 +
webview-ui/src/utils/__tests__/format.test.ts | 51 +++++++
webview-ui/src/utils/format.ts | 15 +++
7 files changed, 147 insertions(+), 144 deletions(-)
create mode 100644 webview-ui/src/components/history/CopyButton.tsx
create mode 100644 webview-ui/src/components/history/ExportButton.tsx
create mode 100644 webview-ui/src/utils/__tests__/format.test.ts
diff --git a/webview-ui/src/components/history/CopyButton.tsx b/webview-ui/src/components/history/CopyButton.tsx
new file mode 100644
index 0000000000..0e693b4470
--- /dev/null
+++ b/webview-ui/src/components/history/CopyButton.tsx
@@ -0,0 +1,32 @@
+import { useCallback } from "react"
+
+import { useClipboard } from "@/components/ui/hooks"
+import { Button } from "@/components/ui"
+import { cn } from "@/lib/utils"
+
+type CopyButtonProps = {
+ itemTask: string
+}
+
+export const CopyButton = ({ itemTask }: CopyButtonProps) => {
+ const { isCopied, copy } = useClipboard()
+
+ const onCopy = useCallback(
+ (e: React.MouseEvent) => {
+ e.stopPropagation()
+ !isCopied && copy(itemTask)
+ },
+ [isCopied, copy, itemTask],
+ )
+
+ return (
+
+
+
+ )
+}
diff --git a/webview-ui/src/components/history/ExportButton.tsx b/webview-ui/src/components/history/ExportButton.tsx
new file mode 100644
index 0000000000..6617e475bd
--- /dev/null
+++ b/webview-ui/src/components/history/ExportButton.tsx
@@ -0,0 +1,16 @@
+import { vscode } from "@/utils/vscode"
+import { Button } from "@/components/ui"
+
+export const ExportButton = ({ itemId }: { itemId: string }) => (
+ {
+ e.stopPropagation()
+ vscode.postMessage({ type: "exportTaskWithId", text: itemId })
+ }}>
+
+
+)
diff --git a/webview-ui/src/components/history/HistoryPreview.tsx b/webview-ui/src/components/history/HistoryPreview.tsx
index b2898fc6a8..bf53845da7 100644
--- a/webview-ui/src/components/history/HistoryPreview.tsx
+++ b/webview-ui/src/components/history/HistoryPreview.tsx
@@ -1,9 +1,11 @@
-import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
-import { useExtensionState } from "../../context/ExtensionStateContext"
-import { vscode } from "../../utils/vscode"
import { memo } from "react"
-import { formatLargeNumber } from "../../utils/format"
-import { useCopyToClipboard } from "../../utils/clipboard"
+
+import { vscode } from "@/utils/vscode"
+import { formatLargeNumber, formatDate } from "@/utils/format"
+import { Button } from "@/components/ui"
+
+import { useExtensionState } from "../../context/ExtensionStateContext"
+import { CopyButton } from "./CopyButton"
type HistoryPreviewProps = {
showHistoryView: () => void
@@ -11,52 +13,15 @@ type HistoryPreviewProps = {
const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
const { taskHistory } = useExtensionState()
- const { showCopyFeedback, copyWithFeedback } = useCopyToClipboard()
+
const handleHistorySelect = (id: string) => {
vscode.postMessage({ type: "showTaskWithId", text: id })
}
- const formatDate = (timestamp: number) => {
- const date = new Date(timestamp)
- return date
- ?.toLocaleString("en-US", {
- month: "long",
- day: "numeric",
- hour: "numeric",
- minute: "2-digit",
- hour12: true,
- })
- .replace(", ", " ")
- .replace(" at", ",")
- .toUpperCase()
- }
-
return (
- {showCopyFeedback &&
Prompt Copied to Clipboard
}
-
{
display: "flex",
alignItems: "center",
}}>
-
-
- Recent Tasks
-
+
+ Recent Tasks
-
-
+
{taskHistory
.filter((item) => item.ts && item.task)
.slice(0, 3)
@@ -103,48 +57,25 @@ const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
key={item.id}
className="history-preview-item"
onClick={() => handleHistorySelect(item.id)}>
-
-
-
+
+
+
{formatDate(item.ts)}
- copyWithFeedback(item.task, e)}>
-
-
+
{item.task}
-
+
Tokens: ↑{formatLargeNumber(item.tokensIn || 0)} ↓
{formatLargeNumber(item.tokensOut || 0)}
@@ -168,21 +99,14 @@ const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
))}
-
-
+ showHistoryView()}
- style={{
- opacity: 0.9,
- }}>
-
- View all history
-
-
+ className="font-normal text-vscode-descriptionForeground">
+ View all history
+
diff --git a/webview-ui/src/components/history/HistoryView.tsx b/webview-ui/src/components/history/HistoryView.tsx
index 49d71e5ddd..d50a569c8d 100644
--- a/webview-ui/src/components/history/HistoryView.tsx
+++ b/webview-ui/src/components/history/HistoryView.tsx
@@ -5,12 +5,14 @@ import prettyBytes from "pretty-bytes"
import { Virtuoso } from "react-virtuoso"
import { VSCodeButton, VSCodeTextField, VSCodeRadioGroup, VSCodeRadio } from "@vscode/webview-ui-toolkit/react"
+import { vscode } from "@/utils/vscode"
+import { formatLargeNumber, formatDate } from "@/utils/format"
+import { highlightFzfMatch } from "@/utils/highlight"
+import { Button } from "@/components/ui"
+
import { useExtensionState } from "../../context/ExtensionStateContext"
-import { vscode } from "../../utils/vscode"
-import { formatLargeNumber } from "../../utils/format"
-import { highlightFzfMatch } from "../../utils/highlight"
-import { useCopyToClipboard } from "../../utils/clipboard"
-import { Button } from "../ui"
+import { ExportButton } from "./ExportButton"
+import { CopyButton } from "./CopyButton"
type HistoryViewProps = {
onDone: () => void
@@ -40,21 +42,6 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
const [deleteTaskId, setDeleteTaskId] = useState
(null)
- const formatDate = (timestamp: number) => {
- const date = new Date(timestamp)
- return date
- ?.toLocaleString("en-US", {
- month: "long",
- day: "numeric",
- hour: "numeric",
- minute: "2-digit",
- hour12: true,
- })
- .replace(", ", " ")
- .replace(" at", ",")
- .toUpperCase()
- }
-
const presentableTasks = useMemo(() => {
return taskHistory.filter((item) => item.ts && item.task)
}, [taskHistory])
@@ -409,28 +396,4 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
)
}
-const CopyButton = ({ itemTask }: { itemTask: string }) => {
- const { showCopyFeedback, copyWithFeedback } = useCopyToClipboard()
-
- return (
- copyWithFeedback(itemTask, e)}>
- {showCopyFeedback ? : }
-
- )
-}
-
-const ExportButton = ({ itemId }: { itemId: string }) => (
- {
- e.stopPropagation()
- vscode.postMessage({ type: "exportTaskWithId", text: itemId })
- }}>
-
-
-)
-
export default memo(HistoryView)
diff --git a/webview-ui/src/index.css b/webview-ui/src/index.css
index 53025be01a..0e80d1b0c6 100644
--- a/webview-ui/src/index.css
+++ b/webview-ui/src/index.css
@@ -23,6 +23,8 @@
@theme {
--font-display: var(--vscode-font-family);
+
+ --text-xs: calc(var(--vscode-font-size) * 0.85);
--text-sm: calc(var(--vscode-font-size) * 0.9);
--text-base: var(--vscode-font-size);
--text-lg: calc(var(--vscode-font-size) * 1.1);
diff --git a/webview-ui/src/utils/__tests__/format.test.ts b/webview-ui/src/utils/__tests__/format.test.ts
new file mode 100644
index 0000000000..7377874fd0
--- /dev/null
+++ b/webview-ui/src/utils/__tests__/format.test.ts
@@ -0,0 +1,51 @@
+// npx jest src/utils/__tests__/format.test.ts
+
+import { formatDate } from "../format"
+
+describe("formatDate", () => {
+ it("formats a timestamp correctly", () => {
+ // January 15, 2023, 10:30 AM
+ const timestamp = new Date(2023, 0, 15, 10, 30).getTime()
+ const result = formatDate(timestamp)
+
+ expect(result).toBe("JANUARY 15, 10:30 AM")
+ })
+
+ it("handles different months correctly", () => {
+ // February 28, 2023, 3:45 PM
+ const timestamp1 = new Date(2023, 1, 28, 15, 45).getTime()
+ expect(formatDate(timestamp1)).toBe("FEBRUARY 28, 3:45 PM")
+
+ // December 31, 2023, 11:59 PM
+ const timestamp2 = new Date(2023, 11, 31, 23, 59).getTime()
+ expect(formatDate(timestamp2)).toBe("DECEMBER 31, 11:59 PM")
+ })
+
+ it("handles AM/PM correctly", () => {
+ // Morning time - 7:05 AM
+ const morningTimestamp = new Date(2023, 5, 15, 7, 5).getTime()
+ expect(formatDate(morningTimestamp)).toBe("JUNE 15, 7:05 AM")
+
+ // Noon - 12:00 PM
+ const noonTimestamp = new Date(2023, 5, 15, 12, 0).getTime()
+ expect(formatDate(noonTimestamp)).toBe("JUNE 15, 12:00 PM")
+
+ // Evening time - 8:15 PM
+ const eveningTimestamp = new Date(2023, 5, 15, 20, 15).getTime()
+ expect(formatDate(eveningTimestamp)).toBe("JUNE 15, 8:15 PM")
+ })
+
+ it("handles single-digit minutes with leading zeros", () => {
+ // 9:05 AM
+ const timestamp = new Date(2023, 3, 10, 9, 5).getTime()
+ expect(formatDate(timestamp)).toBe("APRIL 10, 9:05 AM")
+ })
+
+ it("converts the result to uppercase", () => {
+ const timestamp = new Date(2023, 8, 21, 16, 45).getTime()
+ const result = formatDate(timestamp)
+
+ expect(result).toBe(result.toUpperCase())
+ expect(result).toBe("SEPTEMBER 21, 4:45 PM")
+ })
+})
diff --git a/webview-ui/src/utils/format.ts b/webview-ui/src/utils/format.ts
index 2e473c9b8a..12e9996205 100644
--- a/webview-ui/src/utils/format.ts
+++ b/webview-ui/src/utils/format.ts
@@ -10,3 +10,18 @@ export function formatLargeNumber(num: number): string {
}
return num.toString()
}
+
+export const formatDate = (timestamp: number) => {
+ const date = new Date(timestamp)
+ return date
+ .toLocaleString("en-US", {
+ month: "long",
+ day: "numeric",
+ hour: "numeric",
+ minute: "2-digit",
+ hour12: true,
+ })
+ .replace(", ", " ")
+ .replace(" at", ",")
+ .toUpperCase()
+}