From 603c6c6aea128cb6687306953ee17fa8480ed323 Mon Sep 17 00:00:00 2001 From: NaccOll Date: Mon, 4 Aug 2025 21:52:21 +0800 Subject: [PATCH 1/7] style: update highlightLayer style and align to textarea (#6648) --- webview-ui/src/components/chat/ChatTextArea.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index a52902f1e5..5135eca2f2 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -1011,8 +1011,14 @@ const ChatTextArea = forwardRef( "font-vscode-font-family", "text-vscode-editor-font-size", "leading-vscode-editor-line-height", - "py-2", - "px-[9px]", + isFocused + ? "border border-vscode-focusBorder outline outline-vscode-focusBorder" + : isDraggingOver + ? "border-2 border-dashed border-vscode-focusBorder" + : "border border-transparent", + isEditMode ? "pt-1.5 pb-10 px-2" : "py-1.5 px-2", + "px-[8px]", + "pr-9", "z-10", "forced-color-adjust-none", )} From f24c1e69a30caae616aee61545fa4d8ee37f5641 Mon Sep 17 00:00:00 2001 From: axb Date: Mon, 4 Aug 2025 22:02:30 +0800 Subject: [PATCH 2/7] use assistantMessageParser class instead of parseAssistantMessage (#5341) Co-authored-by: Daniel Riccio --- packages/types/src/experiment.ts | 3 +- .../AssistantMessageParser.ts | 251 +++++++++++ .../__tests__/AssistantMessageParser.spec.ts | 396 ++++++++++++++++++ src/core/task/Task.ts | 29 +- src/shared/__tests__/experiments.spec.ts | 3 + src/shared/experiments.ts | 2 + .../__tests__/ExtensionStateContext.spec.tsx | 2 + webview-ui/src/i18n/locales/ca/settings.json | 4 + webview-ui/src/i18n/locales/de/settings.json | 4 + webview-ui/src/i18n/locales/en/settings.json | 6 +- webview-ui/src/i18n/locales/es/settings.json | 4 + webview-ui/src/i18n/locales/fr/settings.json | 4 + webview-ui/src/i18n/locales/hi/settings.json | 4 + webview-ui/src/i18n/locales/id/settings.json | 4 + webview-ui/src/i18n/locales/it/settings.json | 4 + webview-ui/src/i18n/locales/ja/settings.json | 4 + webview-ui/src/i18n/locales/ko/settings.json | 4 + webview-ui/src/i18n/locales/nl/settings.json | 4 + webview-ui/src/i18n/locales/pl/settings.json | 4 + .../src/i18n/locales/pt-BR/settings.json | 4 + webview-ui/src/i18n/locales/ru/settings.json | 4 + webview-ui/src/i18n/locales/tr/settings.json | 4 + webview-ui/src/i18n/locales/vi/settings.json | 4 + .../src/i18n/locales/zh-CN/settings.json | 4 + .../src/i18n/locales/zh-TW/settings.json | 4 + 25 files changed, 755 insertions(+), 5 deletions(-) create mode 100644 src/core/assistant-message/AssistantMessageParser.ts create mode 100644 src/core/assistant-message/__tests__/AssistantMessageParser.spec.ts diff --git a/packages/types/src/experiment.ts b/packages/types/src/experiment.ts index 5424121d67..6574124629 100644 --- a/packages/types/src/experiment.ts +++ b/packages/types/src/experiment.ts @@ -6,7 +6,7 @@ import type { Keys, Equals, AssertEqual } from "./type-fu.js" * ExperimentId */ -export const experimentIds = ["powerSteering", "multiFileApplyDiff", "preventFocusDisruption"] as const +export const experimentIds = ["powerSteering", "multiFileApplyDiff", "preventFocusDisruption", "assistantMessageParser"] as const export const experimentIdsSchema = z.enum(experimentIds) @@ -20,6 +20,7 @@ export const experimentsSchema = z.object({ powerSteering: z.boolean().optional(), multiFileApplyDiff: z.boolean().optional(), preventFocusDisruption: z.boolean().optional(), + assistantMessageParser: z.boolean().optional(), }) export type Experiments = z.infer diff --git a/src/core/assistant-message/AssistantMessageParser.ts b/src/core/assistant-message/AssistantMessageParser.ts new file mode 100644 index 0000000000..364ec603f2 --- /dev/null +++ b/src/core/assistant-message/AssistantMessageParser.ts @@ -0,0 +1,251 @@ +import { type ToolName, toolNames } from "@roo-code/types" +import { TextContent, ToolUse, ToolParamName, toolParamNames } from "../../shared/tools" +import { AssistantMessageContent } from "./parseAssistantMessage" + +/** + * Parser for assistant messages. Maintains state between chunks + * to avoid reprocessing the entire message on each update. + */ +export class AssistantMessageParser { + private contentBlocks: AssistantMessageContent[] = [] + private currentTextContent: TextContent | undefined = undefined + private currentTextContentStartIndex = 0 + private currentToolUse: ToolUse | undefined = undefined + private currentToolUseStartIndex = 0 + private currentParamName: ToolParamName | undefined = undefined + private currentParamValueStartIndex = 0 + private readonly MAX_ACCUMULATOR_SIZE = 1024 * 1024 // 1MB limit + private readonly MAX_PARAM_LENGTH = 1024 * 100 // 100KB per parameter limit + private accumulator = "" + + /** + * Initialize a new AssistantMessageParser instance. + */ + constructor() { + this.reset() + } + + /** + * Reset the parser state. + */ + public reset(): void { + this.contentBlocks = [] + this.currentTextContent = undefined + this.currentTextContentStartIndex = 0 + this.currentToolUse = undefined + this.currentToolUseStartIndex = 0 + this.currentParamName = undefined + this.currentParamValueStartIndex = 0 + this.accumulator = "" + } + + /** + * Returns the current parsed content blocks + */ + + public getContentBlocks(): AssistantMessageContent[] { + // Return a shallow copy to prevent external mutation + return this.contentBlocks.slice() + } + /** + * Process a new chunk of text and update the parser state. + * @param chunk The new chunk of text to process. + */ + public processChunk(chunk: string): AssistantMessageContent[] { + if (this.accumulator.length + chunk.length > this.MAX_ACCUMULATOR_SIZE) { + throw new Error("Assistant message exceeds maximum allowed size") + } + // Store the current length of the accumulator before adding the new chunk + const accumulatorStartLength = this.accumulator.length + + for (let i = 0; i < chunk.length; i++) { + const char = chunk[i] + this.accumulator += char + const currentPosition = accumulatorStartLength + i + + // There should not be a param without a tool use. + if (this.currentToolUse && this.currentParamName) { + const currentParamValue = this.accumulator.slice(this.currentParamValueStartIndex) + if (currentParamValue.length > this.MAX_PARAM_LENGTH) { + // Reset to a safe state + this.currentParamName = undefined + this.currentParamValueStartIndex = 0 + continue + } + const paramClosingTag = `` + // Streamed param content: always write the currently accumulated value + if (currentParamValue.endsWith(paramClosingTag)) { + // End of param value. + // Do not trim content parameters to preserve newlines, but strip first and last newline only + const paramValue = currentParamValue.slice(0, -paramClosingTag.length) + this.currentToolUse.params[this.currentParamName] = + this.currentParamName === "content" + ? paramValue.replace(/^\n/, "").replace(/\n$/, "") + : paramValue.trim() + this.currentParamName = undefined + continue + } else { + // Partial param value is accumulating. + // Write the currently accumulated param content in real time + this.currentToolUse.params[this.currentParamName] = currentParamValue + continue + } + } + + // No currentParamName. + + if (this.currentToolUse) { + const currentToolValue = this.accumulator.slice(this.currentToolUseStartIndex) + const toolUseClosingTag = `` + if (currentToolValue.endsWith(toolUseClosingTag)) { + // End of a tool use. + this.currentToolUse.partial = false + + this.currentToolUse = undefined + continue + } else { + const possibleParamOpeningTags = toolParamNames.map((name) => `<${name}>`) + for (const paramOpeningTag of possibleParamOpeningTags) { + if (this.accumulator.endsWith(paramOpeningTag)) { + // Start of a new parameter. + const paramName = paramOpeningTag.slice(1, -1) + if (!toolParamNames.includes(paramName as ToolParamName)) { + // Handle invalid parameter name gracefully + continue + } + this.currentParamName = paramName as ToolParamName + this.currentParamValueStartIndex = this.accumulator.length + break + } + } + + // There's no current param, and not starting a new param. + + // Special case for write_to_file where file contents could + // contain the closing tag, in which case the param would have + // closed and we end up with the rest of the file contents here. + // To work around this, get the string between the starting + // content tag and the LAST content tag. + const contentParamName: ToolParamName = "content" + + if ( + this.currentToolUse.name === "write_to_file" && + this.accumulator.endsWith(``) + ) { + const toolContent = this.accumulator.slice(this.currentToolUseStartIndex) + const contentStartTag = `<${contentParamName}>` + const contentEndTag = `` + const contentStartIndex = toolContent.indexOf(contentStartTag) + contentStartTag.length + const contentEndIndex = toolContent.lastIndexOf(contentEndTag) + + if (contentStartIndex !== -1 && contentEndIndex !== -1 && contentEndIndex > contentStartIndex) { + // Don't trim content to preserve newlines, but strip first and last newline only + this.currentToolUse.params[contentParamName] = toolContent + .slice(contentStartIndex, contentEndIndex) + .replace(/^\n/, "") + .replace(/\n$/, "") + } + } + + // Partial tool value is accumulating. + continue + } + } + + // No currentToolUse. + + let didStartToolUse = false + const possibleToolUseOpeningTags = toolNames.map((name) => `<${name}>`) + + for (const toolUseOpeningTag of possibleToolUseOpeningTags) { + if (this.accumulator.endsWith(toolUseOpeningTag)) { + // Extract and validate the tool name + const extractedToolName = toolUseOpeningTag.slice(1, -1) + + // Check if the extracted tool name is valid + if (!toolNames.includes(extractedToolName as ToolName)) { + // Invalid tool name, treat as plain text and continue + continue + } + + // Start of a new tool use. + this.currentToolUse = { + type: "tool_use", + name: extractedToolName as ToolName, + params: {}, + partial: true, + } + + this.currentToolUseStartIndex = this.accumulator.length + + // This also indicates the end of the current text content. + if (this.currentTextContent) { + this.currentTextContent.partial = false + + // Remove the partially accumulated tool use tag from the + // end of text ( block === this.currentToolUse) + if (idx === -1) { + this.contentBlocks.push(this.currentToolUse) + } + + didStartToolUse = true + break + } + } + + if (!didStartToolUse) { + // No tool use, so it must be text either at the beginning or + // between tools. + if (this.currentTextContent === undefined) { + // If this is the first chunk and we're at the beginning of processing, + // set the start index to the current position in the accumulator + this.currentTextContentStartIndex = currentPosition + + // Create a new text content block and add it to contentBlocks + this.currentTextContent = { + type: "text", + content: this.accumulator.slice(this.currentTextContentStartIndex).trim(), + partial: true, + } + + // Add the new text content to contentBlocks immediately + // Ensures it appears in the UI right away + this.contentBlocks.push(this.currentTextContent) + } else { + // Update the existing text content + this.currentTextContent.content = this.accumulator.slice(this.currentTextContentStartIndex).trim() + } + } + } + // Do not call finalizeContentBlocks() here. + // Instead, update any partial blocks in the array and add new ones as they're completed. + // This matches the behavior of the original parseAssistantMessage function. + return this.getContentBlocks() + } + + /** + * Finalize any partial content blocks. + * Should be called after processing the last chunk. + */ + public finalizeContentBlocks(): void { + // Mark all partial blocks as complete + for (const block of this.contentBlocks) { + if (block.partial) { + block.partial = false + } + if (block.type === "text" && typeof block.content === "string") { + block.content = block.content.trim() + } + } + } +} diff --git a/src/core/assistant-message/__tests__/AssistantMessageParser.spec.ts b/src/core/assistant-message/__tests__/AssistantMessageParser.spec.ts new file mode 100644 index 0000000000..828bf9ed22 --- /dev/null +++ b/src/core/assistant-message/__tests__/AssistantMessageParser.spec.ts @@ -0,0 +1,396 @@ +// npx vitest src/core/assistant-message/__tests__/AssistantMessageParser.spec.ts + +import { describe, it, expect, beforeEach } from "vitest" +import { AssistantMessageParser } from "../AssistantMessageParser" +import { AssistantMessageContent } from "../parseAssistantMessage" +import { TextContent, ToolUse } from "../../../shared/tools" +import { toolNames } from "@roo-code/types" + +/** + * Helper to filter out empty text content blocks. + */ +const isEmptyTextContent = (block: any) => block.type === "text" && (block as TextContent).content === "" + +/** + * Helper to simulate streaming by feeding the parser deterministic "random"-sized chunks (1-10 chars). + * Uses a seeded pseudo-random number generator for deterministic chunking. + */ + +// Simple linear congruential generator (LCG) for deterministic pseudo-random numbers +function createSeededRandom(seed: number) { + let state = seed + return { + next: () => { + // LCG parameters from Numerical Recipes + state = (state * 1664525 + 1013904223) % 0x100000000 + return state / 0x100000000 + }, + } +} + +function streamChunks( + parser: AssistantMessageParser, + message: string, +): ReturnType { + let result: AssistantMessageContent[] = [] + let i = 0 + const rng = createSeededRandom(42) // Fixed seed for deterministic tests + while (i < message.length) { + // Deterministic chunk size between 1 and 10, but not exceeding message length + const chunkSize = Math.min(message.length - i, Math.floor(rng.next() * 10) + 1) + const chunk = message.slice(i, i + chunkSize) + result = parser.processChunk(chunk) + i += chunkSize + } + return result +} + +describe("AssistantMessageParser (streaming)", () => { + let parser: AssistantMessageParser + + beforeEach(() => { + parser = new AssistantMessageParser() + }) + + describe("text content streaming", () => { + it("should accumulate a simple text message chunk by chunk", () => { + const message = "Hello, this is a test." + const result = streamChunks(parser, message) + expect(result).toHaveLength(1) + expect(result[0]).toEqual({ + type: "text", + content: message, + partial: true, + }) + }) + + it("should accumulate multi-line text message chunk by chunk", () => { + const message = "Line 1\nLine 2\nLine 3" + const result = streamChunks(parser, message) + expect(result).toHaveLength(1) + expect(result[0]).toEqual({ + type: "text", + content: message, + partial: true, + }) + }) + }) + + describe("tool use streaming", () => { + it("should parse a tool use with parameter, streamed char by char", () => { + const message = "src/file.ts" + const result = streamChunks(parser, message).filter((block) => !isEmptyTextContent(block)) + expect(result).toHaveLength(1) + const toolUse = result[0] as ToolUse + expect(toolUse.type).toBe("tool_use") + expect(toolUse.name).toBe("read_file") + expect(toolUse.params.path).toBe("src/file.ts") + expect(toolUse.partial).toBe(false) + }) + + it("should mark tool use as partial when not closed", () => { + const message = "src/file.ts" + const result = streamChunks(parser, message).filter((block) => !isEmptyTextContent(block)) + expect(result).toHaveLength(1) + const toolUse = result[0] as ToolUse + expect(toolUse.type).toBe("tool_use") + expect(toolUse.name).toBe("read_file") + expect(toolUse.params.path).toBe("src/file.ts") + expect(toolUse.partial).toBe(true) + }) + + it("should handle a partial parameter in a tool use", () => { + const message = "src/file" + const result = streamChunks(parser, message).filter((block) => !isEmptyTextContent(block)) + expect(result).toHaveLength(1) + const toolUse = result[0] as ToolUse + expect(toolUse.type).toBe("tool_use") + expect(toolUse.name).toBe("read_file") + expect(toolUse.params.path).toBe("src/file") + expect(toolUse.partial).toBe(true) + }) + + it("should handle tool use with multiple parameters streamed", () => { + const message = + "src/file.ts1020" + const result = streamChunks(parser, message).filter((block) => !isEmptyTextContent(block)) + expect(result).toHaveLength(1) + const toolUse = result[0] as ToolUse + expect(toolUse.type).toBe("tool_use") + expect(toolUse.name).toBe("read_file") + expect(toolUse.params.path).toBe("src/file.ts") + expect(toolUse.params.start_line).toBe("10") + expect(toolUse.params.end_line).toBe("20") + expect(toolUse.partial).toBe(false) + }) + }) + + describe("mixed content streaming", () => { + it("should parse text followed by a tool use, streamed", () => { + const message = "Text before tool src/file.ts" + const result = streamChunks(parser, message) + expect(result).toHaveLength(2) + const textContent = result[0] as TextContent + expect(textContent.type).toBe("text") + expect(textContent.content).toBe("Text before tool") + expect(textContent.partial).toBe(false) + const toolUse = result[1] as ToolUse + expect(toolUse.type).toBe("tool_use") + expect(toolUse.name).toBe("read_file") + expect(toolUse.params.path).toBe("src/file.ts") + expect(toolUse.partial).toBe(false) + }) + + it("should parse a tool use followed by text, streamed", () => { + const message = "src/file.tsText after tool" + const result = streamChunks(parser, message).filter((block) => !isEmptyTextContent(block)) + expect(result).toHaveLength(2) + const toolUse = result[0] as ToolUse + expect(toolUse.type).toBe("tool_use") + expect(toolUse.name).toBe("read_file") + expect(toolUse.params.path).toBe("src/file.ts") + expect(toolUse.partial).toBe(false) + const textContent = result[1] as TextContent + expect(textContent.type).toBe("text") + expect(textContent.content).toBe("Text after tool") + expect(textContent.partial).toBe(true) + }) + + it("should parse multiple tool uses separated by text, streamed", () => { + const message = + "First: file1.tsSecond: file2.ts" + const result = streamChunks(parser, message) + expect(result).toHaveLength(4) + expect(result[0].type).toBe("text") + expect((result[0] as TextContent).content).toBe("First:") + expect(result[1].type).toBe("tool_use") + expect((result[1] as ToolUse).name).toBe("read_file") + expect((result[1] as ToolUse).params.path).toBe("file1.ts") + expect(result[2].type).toBe("text") + expect((result[2] as TextContent).content).toBe("Second:") + expect(result[3].type).toBe("tool_use") + expect((result[3] as ToolUse).name).toBe("read_file") + expect((result[3] as ToolUse).params.path).toBe("file2.ts") + }) + }) + + describe("special and edge cases", () => { + it("should handle the write_to_file tool with content that contains closing tags", () => { + const message = `src/file.ts + function example() { + // This has XML-like content: + return true; + } + 5` + + const result = streamChunks(parser, message).filter((block) => !isEmptyTextContent(block)) + + expect(result).toHaveLength(1) + const toolUse = result[0] as ToolUse + expect(toolUse.type).toBe("tool_use") + expect(toolUse.name).toBe("write_to_file") + expect(toolUse.params.path).toBe("src/file.ts") + expect(toolUse.params.line_count).toBe("5") + expect(toolUse.params.content).toContain("function example()") + expect(toolUse.params.content).toContain("// This has XML-like content: ") + expect(toolUse.params.content).toContain("return true;") + expect(toolUse.partial).toBe(false) + }) + it("should handle empty messages", () => { + const message = "" + const result = streamChunks(parser, message) + expect(result).toHaveLength(0) + }) + + it("should handle malformed tool use tags as plain text", () => { + const message = "This has a malformed tag" + const result = streamChunks(parser, message) + expect(result).toHaveLength(1) + expect(result[0].type).toBe("text") + expect((result[0] as TextContent).content).toBe(message) + }) + + it("should handle tool use with no parameters", () => { + const message = "" + const result = streamChunks(parser, message).filter((block) => !isEmptyTextContent(block)) + expect(result).toHaveLength(1) + const toolUse = result[0] as ToolUse + expect(toolUse.type).toBe("tool_use") + expect(toolUse.name).toBe("browser_action") + expect(Object.keys(toolUse.params).length).toBe(0) + expect(toolUse.partial).toBe(false) + }) + + it("should handle a tool use with a parameter containing XML-like content", () => { + const message = "
.*
src
" + const result = streamChunks(parser, message).filter((block) => !isEmptyTextContent(block)) + expect(result).toHaveLength(1) + const toolUse = result[0] as ToolUse + expect(toolUse.type).toBe("tool_use") + expect(toolUse.name).toBe("search_files") + expect(toolUse.params.regex).toBe("
.*
") + expect(toolUse.params.path).toBe("src") + expect(toolUse.partial).toBe(false) + }) + + it("should handle consecutive tool uses without text in between", () => { + const message = "file1.tsfile2.ts" + const result = streamChunks(parser, message).filter((block) => !isEmptyTextContent(block)) + expect(result).toHaveLength(2) + const toolUse1 = result[0] as ToolUse + expect(toolUse1.type).toBe("tool_use") + expect(toolUse1.name).toBe("read_file") + expect(toolUse1.params.path).toBe("file1.ts") + expect(toolUse1.partial).toBe(false) + const toolUse2 = result[1] as ToolUse + expect(toolUse2.type).toBe("tool_use") + expect(toolUse2.name).toBe("read_file") + expect(toolUse2.params.path).toBe("file2.ts") + expect(toolUse2.partial).toBe(false) + }) + + it("should handle whitespace in parameters", () => { + const message = " src/file.ts " + const result = streamChunks(parser, message).filter((block) => !isEmptyTextContent(block)) + expect(result).toHaveLength(1) + const toolUse = result[0] as ToolUse + expect(toolUse.type).toBe("tool_use") + expect(toolUse.name).toBe("read_file") + expect(toolUse.params.path).toBe("src/file.ts") + expect(toolUse.partial).toBe(false) + }) + + it("should handle multi-line parameters", () => { + const message = `file.ts + line 1 + line 2 + line 3 + 3` + const result = streamChunks(parser, message).filter((block) => !isEmptyTextContent(block)) + + expect(result).toHaveLength(1) + const toolUse = result[0] as ToolUse + expect(toolUse.type).toBe("tool_use") + expect(toolUse.name).toBe("write_to_file") + expect(toolUse.params.path).toBe("file.ts") + expect(toolUse.params.content).toContain("line 1") + expect(toolUse.params.content).toContain("line 2") + expect(toolUse.params.content).toContain("line 3") + expect(toolUse.params.line_count).toBe("3") + expect(toolUse.partial).toBe(false) + }) + it("should handle a complex message with multiple content types", () => { + const message = `I'll help you with that task. + + src/index.ts + + Now let's modify the file: + + src/index.ts + // Updated content + console.log("Hello world"); + 2 + + Let's run the code: + + node src/index.ts` + + const result = streamChunks(parser, message) + + expect(result).toHaveLength(6) + + // First text block + expect(result[0].type).toBe("text") + expect((result[0] as TextContent).content).toBe("I'll help you with that task.") + + // First tool use (read_file) + expect(result[1].type).toBe("tool_use") + expect((result[1] as ToolUse).name).toBe("read_file") + + // Second text block + expect(result[2].type).toBe("text") + expect((result[2] as TextContent).content).toContain("Now let's modify the file:") + + // Second tool use (write_to_file) + expect(result[3].type).toBe("tool_use") + expect((result[3] as ToolUse).name).toBe("write_to_file") + + // Third text block + expect(result[4].type).toBe("text") + expect((result[4] as TextContent).content).toContain("Let's run the code:") + + // Third tool use (execute_command) + expect(result[5].type).toBe("tool_use") + expect((result[5] as ToolUse).name).toBe("execute_command") + }) + }) + + describe("size limit handling", () => { + it("should throw an error when MAX_ACCUMULATOR_SIZE is exceeded", () => { + // Create a message that exceeds 1MB (MAX_ACCUMULATOR_SIZE) + const largeMessage = "x".repeat(1024 * 1024 + 1) // 1MB + 1 byte + + expect(() => { + parser.processChunk(largeMessage) + }).toThrow("Assistant message exceeds maximum allowed size") + }) + + it("should gracefully handle a parameter that exceeds MAX_PARAM_LENGTH", () => { + // Create a parameter value that exceeds 100KB (MAX_PARAM_LENGTH) + const largeParamValue = "x".repeat(1024 * 100 + 1) // 100KB + 1 byte + const message = `test.txt${largeParamValue}After tool` + + // Process the message in chunks to simulate streaming + let result: AssistantMessageContent[] = [] + let error: Error | null = null + + try { + // Process the opening tags + result = parser.processChunk("test.txt") + + // Process the large parameter value in chunks + const chunkSize = 1000 + for (let i = 0; i < largeParamValue.length; i += chunkSize) { + const chunk = largeParamValue.slice(i, i + chunkSize) + result = parser.processChunk(chunk) + } + + // Process the closing tags and text after + result = parser.processChunk("After tool") + } catch (e) { + error = e as Error + } + + // Should not throw an error + expect(error).toBeNull() + + // Should have processed the content + expect(result.length).toBeGreaterThan(0) + + // The tool use should exist but the content parameter should be reset/empty + const toolUse = result.find((block) => block.type === "tool_use") as ToolUse + expect(toolUse).toBeDefined() + expect(toolUse.name).toBe("write_to_file") + expect(toolUse.params.path).toBe("test.txt") + + // The text after the tool should still be parsed + const textAfter = result.find( + (block) => block.type === "text" && (block as TextContent).content.includes("After tool"), + ) + expect(textAfter).toBeDefined() + }) + }) + + describe("finalizeContentBlocks", () => { + it("should mark all partial blocks as complete", () => { + const message = "src/file.ts" + streamChunks(parser, message) + let blocks = parser.getContentBlocks() + // The block may already be partial or not, depending on chunking. + // To ensure the test is robust, we only assert after finalizeContentBlocks. + parser.finalizeContentBlocks() + blocks = parser.getContentBlocks() + expect(blocks[0].partial).toBe(false) + }) + }) +}) diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 6eef70158f..3cb6abe7f7 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -77,7 +77,8 @@ import { ToolRepetitionDetector } from "../tools/ToolRepetitionDetector" import { FileContextTracker } from "../context-tracking/FileContextTracker" import { RooIgnoreController } from "../ignore/RooIgnoreController" import { RooProtectedController } from "../protect/RooProtectedController" -import { type AssistantMessageContent, parseAssistantMessage, presentAssistantMessage } from "../assistant-message" +import { type AssistantMessageContent, presentAssistantMessage, parseAssistantMessage } from "../assistant-message" +import { AssistantMessageParser } from "../assistant-message/AssistantMessageParser" import { truncateConversationIfNeeded } from "../sliding-window" import { ClineProvider } from "../webview/ClineProvider" import { MultiSearchReplaceDiffStrategy } from "../diff/strategies/multi-search-replace" @@ -249,6 +250,8 @@ export class Task extends EventEmitter implements TaskLike { didRejectTool = false didAlreadyUseTool = false didCompleteReadingStream = false + assistantMessageParser?: AssistantMessageParser + isAssistantMessageParserEnabled = false constructor({ provider, @@ -1553,6 +1556,9 @@ export class Task extends EventEmitter implements TaskLike { this.didAlreadyUseTool = false this.presentAssistantMessageLocked = false this.presentAssistantMessageHasPendingUpdates = false + if (this.assistantMessageParser) { + this.assistantMessageParser.reset() + } await this.diffViewProvider.reset() @@ -1587,9 +1593,14 @@ export class Task extends EventEmitter implements TaskLike { case "text": { assistantMessage += chunk.text - // Parse raw assistant message into content blocks. + // Parse raw assistant message chunk into content blocks. const prevLength = this.assistantMessageContent.length - this.assistantMessageContent = parseAssistantMessage(assistantMessage) + if (this.isAssistantMessageParserEnabled && this.assistantMessageParser) { + this.assistantMessageContent = this.assistantMessageParser.processChunk(chunk.text) + } else { + // Use the old parsing method when experiment is disabled + this.assistantMessageContent = parseAssistantMessage(assistantMessage) + } if (this.assistantMessageContent.length > prevLength) { // New content we need to present, reset to @@ -1709,6 +1720,13 @@ export class Task extends EventEmitter implements TaskLike { // Can't just do this b/c a tool could be in the middle of executing. // this.assistantMessageContent.forEach((e) => (e.partial = false)) + // Now that the stream is complete, finalize any remaining partial content blocks + if (this.isAssistantMessageParserEnabled && this.assistantMessageParser) { + this.assistantMessageParser.finalizeContentBlocks() + this.assistantMessageContent = this.assistantMessageParser.getContentBlocks() + } + // When using old parser, no finalization needed - parsing already happened during streaming + if (partialBlocks.length > 0) { // If there is content to update then it will complete and // update `this.userMessageContentReady` to true, which we @@ -1722,6 +1740,11 @@ export class Task extends EventEmitter implements TaskLike { await this.saveClineMessages() await this.providerRef.deref()?.postStateToWebview() + // Reset parser after each complete conversation round + if (this.assistantMessageParser) { + this.assistantMessageParser.reset() + } + // Now add to apiConversationHistory. // Need to save assistant responses to file before proceeding to // tool use since user can exit at any moment and we wouldn't be diff --git a/src/shared/__tests__/experiments.spec.ts b/src/shared/__tests__/experiments.spec.ts index 607c1e0b04..21401dc759 100644 --- a/src/shared/__tests__/experiments.spec.ts +++ b/src/shared/__tests__/experiments.spec.ts @@ -29,6 +29,7 @@ describe("experiments", () => { powerSteering: false, multiFileApplyDiff: false, preventFocusDisruption: false, + assistantMessageParser: false, } expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(false) }) @@ -38,6 +39,7 @@ describe("experiments", () => { powerSteering: true, multiFileApplyDiff: false, preventFocusDisruption: false, + assistantMessageParser: false, } expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(true) }) @@ -47,6 +49,7 @@ describe("experiments", () => { powerSteering: false, multiFileApplyDiff: false, preventFocusDisruption: false, + assistantMessageParser: false, } expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(false) }) diff --git a/src/shared/experiments.ts b/src/shared/experiments.ts index 548b55f68c..4be89afa1a 100644 --- a/src/shared/experiments.ts +++ b/src/shared/experiments.ts @@ -4,6 +4,7 @@ export const EXPERIMENT_IDS = { MULTI_FILE_APPLY_DIFF: "multiFileApplyDiff", POWER_STEERING: "powerSteering", PREVENT_FOCUS_DISRUPTION: "preventFocusDisruption", + ASSISTANT_MESSAGE_PARSER: "assistantMessageParser", } as const satisfies Record type _AssertExperimentIds = AssertEqual>> @@ -18,6 +19,7 @@ export const experimentConfigsMap: Record = { MULTI_FILE_APPLY_DIFF: { enabled: false }, POWER_STEERING: { enabled: false }, PREVENT_FOCUS_DISRUPTION: { enabled: false }, + ASSISTANT_MESSAGE_PARSER: { enabled: false }, } export const experimentDefault = Object.fromEntries( diff --git a/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx b/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx index 7c69f39c2b..a688cac885 100644 --- a/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx +++ b/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx @@ -229,6 +229,7 @@ describe("mergeExtensionState", () => { concurrentFileReads: true, multiFileApplyDiff: true, preventFocusDisruption: false, + assistantMessageParser: false, } as Record, } @@ -246,6 +247,7 @@ describe("mergeExtensionState", () => { concurrentFileReads: true, multiFileApplyDiff: true, preventFocusDisruption: false, + assistantMessageParser: false, }) }) }) diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index 9ab98a8980..cc1bbf5680 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -686,6 +686,10 @@ "PREVENT_FOCUS_DISRUPTION": { "name": "Edició en segon pla", "description": "Quan s'activa, evita la interrupció del focus de l'editor. Les edicions de fitxers es produeixen en segon pla sense obrir la vista diff o robar el focus. Pots continuar treballant sense interrupcions mentre Roo fa canvis. Els fitxers poden obrir-se sense focus per capturar diagnòstics o romandre completament tancats." + }, + "ASSISTANT_MESSAGE_PARSER": { + "name": "Utilitza el nou analitzador de missatges", + "description": "Activa l'analitzador de missatges en streaming experimental que millora el rendiment en respostes llargues processant els missatges de manera més eficient." } }, "promptCaching": { diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index 667b313468..6bee80a8a6 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -686,6 +686,10 @@ "PREVENT_FOCUS_DISRUPTION": { "name": "Hintergrundbearbeitung", "description": "Verhindert Editor-Fokus-Störungen wenn aktiviert. Dateibearbeitungen erfolgen im Hintergrund ohne Öffnung von Diff-Ansichten oder Fokus-Diebstahl. Du kannst ungestört weiterarbeiten, während Roo Änderungen vornimmt. Dateien können ohne Fokus geöffnet werden, um Diagnosen zu erfassen oder vollständig geschlossen bleiben." + }, + "ASSISTANT_MESSAGE_PARSER": { + "name": "Neuen Nachrichtenparser verwenden", + "description": "Aktiviere den experimentellen Streaming-Nachrichtenparser, der lange Antworten durch effizientere Verarbeitung spürbar schneller macht." } }, "promptCaching": { diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 46c15556c8..c52841ca83 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -682,9 +682,13 @@ "name": "Enable concurrent file edits", "description": "When enabled, Roo can edit multiple files in a single request. When disabled, Roo must edit files one at a time. Disabling this can help when working with less capable models or when you want more control over file modifications." }, - "PREVENT_FOCUS_DISRUPTION": { +"PREVENT_FOCUS_DISRUPTION": { "name": "Background editing", "description": "Prevent editor focus disruption when enabled. File edits happen in the background without opening diff views or stealing focus. You can continue working uninterrupted while Roo makes changes. Files can be opened without focus to capture diagnostics or kept closed entirely." + }, + "ASSISTANT_MESSAGE_PARSER": { + "name": "Use new message parser", + "description": "Enable the experimental streaming message parser that provides significant performance improvements for long assistant responses by processing messages more efficiently." } }, "promptCaching": { diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index 0f41e6ddda..42251f606a 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -686,6 +686,10 @@ "PREVENT_FOCUS_DISRUPTION": { "name": "Edición en segundo plano", "description": "Previene la interrupción del foco del editor cuando está habilitado. Las ediciones de archivos ocurren en segundo plano sin abrir vistas de diferencias o robar el foco. Puedes continuar trabajando sin interrupciones mientras Roo realiza cambios. Los archivos pueden abrirse sin foco para capturar diagnósticos o mantenerse completamente cerrados." + }, + "ASSISTANT_MESSAGE_PARSER": { + "name": "Usar el nuevo analizador de mensajes", + "description": "Activa el analizador de mensajes en streaming experimental que mejora el rendimiento en respuestas largas procesando los mensajes de forma más eficiente." } }, "promptCaching": { diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 5af186e6b1..c527b2e42f 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -686,6 +686,10 @@ "PREVENT_FOCUS_DISRUPTION": { "name": "Édition en arrière-plan", "description": "Empêche la perturbation du focus de l'éditeur lorsqu'activé. Les modifications de fichiers se font en arrière-plan sans ouvrir de vues de différences ou voler le focus. Vous pouvez continuer à travailler sans interruption pendant que Roo effectue des changements. Les fichiers peuvent être ouverts sans focus pour capturer les diagnostics ou rester complètement fermés." + }, + "ASSISTANT_MESSAGE_PARSER": { + "name": "Utiliser le nouveau parseur de messages", + "description": "Active le parseur de messages en streaming expérimental qui accélère nettement les longues réponses en traitant les messages plus efficacement." } }, "promptCaching": { diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index e3743a531e..5130f818da 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -687,6 +687,10 @@ "PREVENT_FOCUS_DISRUPTION": { "name": "बैकग्राउंड संपादन", "description": "सक्षम होने पर एडिटर फोकस व्यवधान को रोकता है। फ़ाइल संपादन diff व्यू खोले बिना या फोकस चुराए बिना बैकग्राउंड में होता है। आप Roo के बदलाव करते समय बिना किसी बाधा के काम जारी रख सकते हैं। फ़ाइलें डायग्नोस्टिक्स कैप्चर करने के लिए बिना फोकस के खुल सकती हैं या पूरी तरह बंद रह सकती हैं।" + }, + "ASSISTANT_MESSAGE_PARSER": { + "name": "नए मैसेज पार्सर का उपयोग करें", + "description": "प्रायोगिक स्ट्रीमिंग मैसेज पार्सर सक्षम करें, जो लंबे उत्तरों के लिए संदेशों को अधिक कुशलता से प्रोसेस करके प्रदर्शन को बेहतर बनाता है।" } }, "promptCaching": { diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index 0f47712f21..f0285a5130 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -716,6 +716,10 @@ "PREVENT_FOCUS_DISRUPTION": { "name": "Pengeditan Latar Belakang", "description": "Ketika diaktifkan, mencegah gangguan fokus editor. Pengeditan file terjadi di latar belakang tanpa membuka tampilan diff atau mencuri fokus. Anda dapat terus bekerja tanpa gangguan saat Roo melakukan perubahan. File mungkin dibuka tanpa fokus untuk menangkap diagnostik atau tetap tertutup sepenuhnya." + }, + "ASSISTANT_MESSAGE_PARSER": { + "name": "Gunakan parser pesan baru", + "description": "Aktifkan parser pesan streaming eksperimental yang meningkatkan kinerja untuk respons panjang dengan memproses pesan lebih efisien." } }, "promptCaching": { diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index e5bc317eff..afdd7b3707 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -687,6 +687,10 @@ "PREVENT_FOCUS_DISRUPTION": { "name": "Modifica in background", "description": "Previene l'interruzione del focus dell'editor quando abilitato. Le modifiche ai file avvengono in background senza aprire viste di differenze o rubare il focus. Puoi continuare a lavorare senza interruzioni mentre Roo effettua modifiche. I file possono essere aperti senza focus per catturare diagnostiche o rimanere completamente chiusi." + }, + "ASSISTANT_MESSAGE_PARSER": { + "name": "Usa il nuovo parser dei messaggi", + "description": "Abilita il parser di messaggi in streaming sperimentale che migliora nettamente le risposte lunghe elaborando i messaggi in modo più efficiente." } }, "promptCaching": { diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index ab4cda177a..debc7ad2ab 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -687,6 +687,10 @@ "PREVENT_FOCUS_DISRUPTION": { "name": "バックグラウンド編集", "description": "有効にすると、エディターのフォーカス中断を防ぎます。ファイル編集は差分ビューを開いたりフォーカスを奪ったりすることなく、バックグラウンドで行われます。Rooが変更を行っている間も中断されることなく作業を続けることができます。ファイルは診断をキャプチャするためにフォーカスなしで開くか、完全に閉じたままにできます。" + }, + "ASSISTANT_MESSAGE_PARSER": { + "name": "新しいメッセージパーサーを使う", + "description": "実験的なストリーミングメッセージパーサーを有効にします。長い回答をより効率的に処理し、遅延を減らします。" } }, "promptCaching": { diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index adad29a152..d48012862f 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -687,6 +687,10 @@ "PREVENT_FOCUS_DISRUPTION": { "name": "백그라운드 편집", "description": "활성화하면 편집기 포커스 방해를 방지합니다. 파일 편집이 diff 뷰를 열거나 포커스를 빼앗지 않고 백그라운드에서 수행됩니다. Roo가 변경사항을 적용하는 동안 방해받지 않고 계속 작업할 수 있습니다. 파일은 진단을 캡처하기 위해 포커스 없이 열거나 완전히 닫힌 상태로 유지할 수 있습니다." + }, + "ASSISTANT_MESSAGE_PARSER": { + "name": "새 메시지 파서 사용", + "description": "실험적 스트리밍 메시지 파서를 활성화합니다. 긴 응답을 더 효율적으로 처리해 지연을 줄입니다." } }, "promptCaching": { diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index e635c8d2c8..7722244dd4 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -687,6 +687,10 @@ "PREVENT_FOCUS_DISRUPTION": { "name": "Achtergrondbewerking", "description": "Voorkomt editor focus verstoring wanneer ingeschakeld. Bestandsbewerkingen gebeuren op de achtergrond zonder diff-weergaven te openen of focus te stelen. Je kunt ononderbroken doorwerken terwijl Roo wijzigingen aanbrengt. Bestanden kunnen zonder focus worden geopend om diagnostiek vast te leggen of volledig gesloten blijven." + }, + "ASSISTANT_MESSAGE_PARSER": { + "name": "Nieuwe berichtparser gebruiken", + "description": "Schakel de experimentele streaming-berichtparser in die lange antwoorden sneller maakt door berichten efficiënter te verwerken." } }, "promptCaching": { diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index d176693143..130453764a 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -687,6 +687,10 @@ "PREVENT_FOCUS_DISRUPTION": { "name": "Edycja w tle", "description": "Zapobiega zakłócaniu fokusa edytora gdy włączone. Edycje plików odbywają się w tle bez otwierania widoków różnic lub kradzieży fokusa. Możesz kontynuować pracę bez przeszkód podczas gdy Roo wprowadza zmiany. Pliki mogą być otwierane bez fokusa aby przechwycić diagnostykę lub pozostać całkowicie zamknięte." + }, + "ASSISTANT_MESSAGE_PARSER": { + "name": "Użyj nowego parsera wiadomości", + "description": "Włącz eksperymentalny parser wiadomości w strumieniu, który przyspiesza długie odpowiedzi dzięki bardziej wydajnemu przetwarzaniu wiadomości." } }, "promptCaching": { diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index a646229164..05e20bfee2 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -687,6 +687,10 @@ "PREVENT_FOCUS_DISRUPTION": { "name": "Edição em segundo plano", "description": "Previne a interrupção do foco do editor quando habilitado. As edições de arquivos acontecem em segundo plano sem abrir visualizações de diferenças ou roubar o foco. Você pode continuar trabalhando sem interrupções enquanto o Roo faz alterações. Os arquivos podem ser abertos sem foco para capturar diagnósticos ou permanecer completamente fechados." + }, + "ASSISTANT_MESSAGE_PARSER": { + "name": "Usar o novo parser de mensagens", + "description": "Ativa o parser de mensagens em streaming experimental que acelera respostas longas ao processar as mensagens de forma mais eficiente." } }, "promptCaching": { diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index 7476f0cb0a..6eeb5f134a 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -687,6 +687,10 @@ "PREVENT_FOCUS_DISRUPTION": { "name": "Фоновое редактирование", "description": "Предотвращает нарушение фокуса редактора при включении. Редактирование файлов происходит в фоновом режиме без открытия представлений различий или кражи фокуса. Вы можете продолжать работать без перерывов, пока Roo вносит изменения. Файлы могут открываться без фокуса для захвата диагностики или оставаться полностью закрытыми." + }, + "ASSISTANT_MESSAGE_PARSER": { + "name": "Использовать новый парсер сообщений", + "description": "Включите экспериментальный потоковый парсер сообщений, который ускоряет длинные ответы благодаря более эффективной обработке сообщений." } }, "promptCaching": { diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index 07e8dac1d6..f58ab8ad5e 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -687,6 +687,10 @@ "PREVENT_FOCUS_DISRUPTION": { "name": "Arka plan düzenleme", "description": "Etkinleştirildiğinde editör odak kesintisini önler. Dosya düzenlemeleri diff görünümlerini açmadan veya odağı çalmadan arka planda gerçekleşir. Roo değişiklikler yaparken kesintisiz çalışmaya devam edebilirsiniz. Dosyalar tanılamayı yakalamak için odaksız açılabilir veya tamamen kapalı kalabilir." + }, + "ASSISTANT_MESSAGE_PARSER": { + "name": "Yeni mesaj ayrıştırıcıyı kullan", + "description": "Uzun yanıtları daha verimli işleyerek hızlandıran deneysel akış mesaj ayrıştırıcısını etkinleştir." } }, "promptCaching": { diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index e1b91860b8..0b8461b469 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -687,6 +687,10 @@ "PREVENT_FOCUS_DISRUPTION": { "name": "Chỉnh sửa nền", "description": "Khi được bật, ngăn chặn gián đoạn tiêu điểm trình soạn thảo. Việc chỉnh sửa tệp diễn ra ở nền mà không mở chế độ xem diff hoặc chiếm tiêu điểm. Bạn có thể tiếp tục làm việc không bị gián đoạn trong khi Roo thực hiện thay đổi. Các tệp có thể được mở mà không có tiêu điểm để thu thập chẩn đoán hoặc giữ hoàn toàn đóng." + }, + "ASSISTANT_MESSAGE_PARSER": { + "name": "Dùng bộ phân tích tin nhắn mới", + "description": "Bật bộ phân tích tin nhắn streaming thử nghiệm. Tính năng này tăng tốc phản hồi dài bằng cách xử lý tin nhắn hiệu quả hơn." } }, "promptCaching": { diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index 2b390f349c..f9e82bf87e 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -687,6 +687,10 @@ "PREVENT_FOCUS_DISRUPTION": { "name": "后台编辑", "description": "启用后防止编辑器焦点干扰。文件编辑在后台进行,不会打开差异视图或抢夺焦点。你可以在 Roo 进行更改时继续不受干扰地工作。文件可以在不获取焦点的情况下打开以捕获诊断信息,或保持完全关闭状态。" + }, + "ASSISTANT_MESSAGE_PARSER": { + "name": "使用新的消息解析器", + "description": "启用实验性的流式消息解析器。通过更高效地处理消息,可显著提升长回复的性能。" } }, "promptCaching": { diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index b1ec67b8db..f638a782b9 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -687,6 +687,10 @@ "PREVENT_FOCUS_DISRUPTION": { "name": "背景編輯", "description": "啟用後可防止編輯器焦點中斷。檔案編輯會在背景進行,不會開啟 diff 檢視或搶奪焦點。您可以在 Roo 進行變更時繼續不受干擾地工作。檔案可能會在不獲得焦點的情況下開啟以捕獲診斷,或保持完全關閉。" + }, + "ASSISTANT_MESSAGE_PARSER": { + "name": "使用全新訊息解析器", + "description": "啟用實驗性的串流訊息解析器。透過更有效率地處理訊息,能顯著提升長回覆的效能。" } }, "promptCaching": { From a921d059e138a9d8e657f66a36865586b02b44f3 Mon Sep 17 00:00:00 2001 From: jues <95405836+jues@users.noreply.github.com> Date: Mon, 4 Aug 2025 22:06:46 +0800 Subject: [PATCH 3/7] Add Z AI provider (#6657) Co-authored-by: wangshan --- packages/types/src/provider-settings.ts | 8 + packages/types/src/providers/index.ts | 1 + packages/types/src/providers/zai.ts | 105 ++++++++ src/api/index.ts | 3 + src/api/providers/__tests__/zai.spec.ts | 231 ++++++++++++++++++ src/api/providers/index.ts | 1 + src/api/providers/zai.ts | 31 +++ .../src/components/settings/ApiOptions.tsx | 14 ++ .../src/components/settings/constants.ts | 3 + .../src/components/settings/providers/ZAi.tsx | 76 ++++++ .../components/settings/providers/index.ts | 1 + .../components/ui/hooks/useSelectedModel.ts | 12 + webview-ui/src/i18n/locales/ca/settings.json | 4 + webview-ui/src/i18n/locales/de/settings.json | 4 + webview-ui/src/i18n/locales/en/settings.json | 4 + webview-ui/src/i18n/locales/es/settings.json | 4 + webview-ui/src/i18n/locales/fr/settings.json | 4 + webview-ui/src/i18n/locales/hi/settings.json | 4 + webview-ui/src/i18n/locales/id/settings.json | 4 + webview-ui/src/i18n/locales/it/settings.json | 4 + webview-ui/src/i18n/locales/ja/settings.json | 4 + webview-ui/src/i18n/locales/ko/settings.json | 4 + webview-ui/src/i18n/locales/nl/settings.json | 4 + webview-ui/src/i18n/locales/pl/settings.json | 4 + .../src/i18n/locales/pt-BR/settings.json | 4 + webview-ui/src/i18n/locales/ru/settings.json | 4 + webview-ui/src/i18n/locales/tr/settings.json | 4 + webview-ui/src/i18n/locales/vi/settings.json | 4 + .../src/i18n/locales/zh-CN/settings.json | 4 + .../src/i18n/locales/zh-TW/settings.json | 4 + 30 files changed, 558 insertions(+) create mode 100644 packages/types/src/providers/zai.ts create mode 100644 src/api/providers/__tests__/zai.spec.ts create mode 100644 src/api/providers/zai.ts create mode 100644 webview-ui/src/components/settings/providers/ZAi.tsx diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 207c60a524..876f5114b6 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -36,6 +36,7 @@ export const providerNames = [ "huggingface", "cerebras", "sambanova", + "zai", ] as const export const providerNamesSchema = z.enum(providerNames) @@ -257,6 +258,11 @@ const sambaNovaSchema = apiModelIdProviderModelSchema.extend({ sambaNovaApiKey: z.string().optional(), }) +const zaiSchema = apiModelIdProviderModelSchema.extend({ + zaiApiKey: z.string().optional(), + zaiApiLine: z.union([z.literal("china"), z.literal("international")]).optional(), +}) + const defaultSchema = z.object({ apiProvider: z.undefined(), }) @@ -290,6 +296,7 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv litellmSchema.merge(z.object({ apiProvider: z.literal("litellm") })), cerebrasSchema.merge(z.object({ apiProvider: z.literal("cerebras") })), sambaNovaSchema.merge(z.object({ apiProvider: z.literal("sambanova") })), + zaiSchema.merge(z.object({ apiProvider: z.literal("zai") })), defaultSchema, ]) @@ -323,6 +330,7 @@ export const providerSettingsSchema = z.object({ ...litellmSchema.shape, ...cerebrasSchema.shape, ...sambaNovaSchema.shape, + ...zaiSchema.shape, ...codebaseIndexProviderSchema.shape, }) diff --git a/packages/types/src/providers/index.ts b/packages/types/src/providers/index.ts index d6584e70ec..b0e316bf55 100644 --- a/packages/types/src/providers/index.ts +++ b/packages/types/src/providers/index.ts @@ -22,3 +22,4 @@ export * from "./vertex.js" export * from "./vscode-llm.js" export * from "./xai.js" export * from "./doubao.js" +export * from "./zai.js" diff --git a/packages/types/src/providers/zai.ts b/packages/types/src/providers/zai.ts new file mode 100644 index 0000000000..f724744827 --- /dev/null +++ b/packages/types/src/providers/zai.ts @@ -0,0 +1,105 @@ +import type { ModelInfo } from "../model.js" + +// Z AI +// https://docs.z.ai/guides/llm/glm-4.5 +// https://docs.z.ai/guides/overview/pricing + +export type InternationalZAiModelId = keyof typeof internationalZAiModels +export const internationalZAiDefaultModelId: InternationalZAiModelId = "glm-4.5" +export const internationalZAiModels = { + "glm-4.5": { + maxTokens: 98_304, + contextWindow: 131_072, + supportsImages: false, + supportsPromptCache: true, + inputPrice: 0.6, + outputPrice: 2.2, + cacheWritesPrice: 0, + cacheReadsPrice: 0.11, + description: + "GLM-4.5 is Zhipu's latest featured model. Its comprehensive capabilities in reasoning, coding, and agent reach the state-of-the-art (SOTA) level among open-source models, with a context length of up to 128k.", + }, + "glm-4.5-air": { + maxTokens: 98_304, + contextWindow: 131_072, + supportsImages: false, + supportsPromptCache: true, + inputPrice: 0.2, + outputPrice: 1.1, + cacheWritesPrice: 0, + cacheReadsPrice: 0.03, + description: + "GLM-4.5-Air is the lightweight version of GLM-4.5. It balances performance and cost-effectiveness, and can flexibly switch to hybrid thinking models.", + }, +} as const satisfies Record + +export type MainlandZAiModelId = keyof typeof mainlandZAiModels +export const mainlandZAiDefaultModelId: MainlandZAiModelId = "glm-4.5" +export const mainlandZAiModels = { + "glm-4.5": { + maxTokens: 98_304, + contextWindow: 131_072, + supportsImages: false, + supportsPromptCache: true, + inputPrice: 0.29, + outputPrice: 1.14, + cacheWritesPrice: 0, + cacheReadsPrice: 0.057, + description: + "GLM-4.5 is Zhipu's latest featured model. Its comprehensive capabilities in reasoning, coding, and agent reach the state-of-the-art (SOTA) level among open-source models, with a context length of up to 128k.", + tiers: [ + { + contextWindow: 32_000, + inputPrice: 0.21, + outputPrice: 1.0, + cacheReadsPrice: 0.043, + }, + { + contextWindow: 128_000, + inputPrice: 0.29, + outputPrice: 1.14, + cacheReadsPrice: 0.057, + }, + { + contextWindow: Infinity, + inputPrice: 0.29, + outputPrice: 1.14, + cacheReadsPrice: 0.057, + }, + ], + }, + "glm-4.5-air": { + maxTokens: 98_304, + contextWindow: 131_072, + supportsImages: false, + supportsPromptCache: true, + inputPrice: 0.1, + outputPrice: 0.6, + cacheWritesPrice: 0, + cacheReadsPrice: 0.02, + description: + "GLM-4.5-Air is the lightweight version of GLM-4.5. It balances performance and cost-effectiveness, and can flexibly switch to hybrid thinking models.", + tiers: [ + { + contextWindow: 32_000, + inputPrice: 0.07, + outputPrice: 0.4, + cacheReadsPrice: 0.014, + }, + { + contextWindow: 128_000, + inputPrice: 0.1, + outputPrice: 0.6, + cacheReadsPrice: 0.02, + }, + { + contextWindow: Infinity, + inputPrice: 0.1, + outputPrice: 0.6, + cacheReadsPrice: 0.02, + }, + ], + }, +} as const satisfies Record + +export const ZAI_DEFAULT_TEMPERATURE = 0 diff --git a/src/api/index.ts b/src/api/index.ts index 5daa53396f..3ad3705eba 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -33,6 +33,7 @@ import { ClaudeCodeHandler, SambaNovaHandler, DoubaoHandler, + ZAiHandler, } from "./providers" export interface SingleCompletionHandler { @@ -124,6 +125,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler { return new CerebrasHandler(options) case "sambanova": return new SambaNovaHandler(options) + case "zai": + return new ZAiHandler(options) default: apiProvider satisfies "gemini-cli" | undefined return new AnthropicHandler(options) diff --git a/src/api/providers/__tests__/zai.spec.ts b/src/api/providers/__tests__/zai.spec.ts new file mode 100644 index 0000000000..6b93aaa43b --- /dev/null +++ b/src/api/providers/__tests__/zai.spec.ts @@ -0,0 +1,231 @@ +// npx vitest run src/api/providers/__tests__/zai.spec.ts + +// Mock vscode first to avoid import errors +vitest.mock("vscode", () => ({})) + +import OpenAI from "openai" +import { Anthropic } from "@anthropic-ai/sdk" + +import { + type InternationalZAiModelId, + type MainlandZAiModelId, + internationalZAiDefaultModelId, + mainlandZAiDefaultModelId, + internationalZAiModels, + mainlandZAiModels, + ZAI_DEFAULT_TEMPERATURE, +} from "@roo-code/types" + +import { ZAiHandler } from "../zai" + +vitest.mock("openai", () => { + const createMock = vitest.fn() + return { + default: vitest.fn(() => ({ chat: { completions: { create: createMock } } })), + } +}) + +describe("ZAiHandler", () => { + let handler: ZAiHandler + let mockCreate: any + + beforeEach(() => { + vitest.clearAllMocks() + mockCreate = (OpenAI as unknown as any)().chat.completions.create + }) + + describe("International Z AI", () => { + beforeEach(() => { + handler = new ZAiHandler({ zaiApiKey: "test-zai-api-key", zaiApiLine: "international" }) + }) + + it("should use the correct international Z AI base URL", () => { + new ZAiHandler({ zaiApiKey: "test-zai-api-key", zaiApiLine: "international" }) + expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ baseURL: "https://api.z.ai/api/paas/v4" })) + }) + + it("should use the provided API key for international", () => { + const zaiApiKey = "test-zai-api-key" + new ZAiHandler({ zaiApiKey, zaiApiLine: "international" }) + expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ apiKey: zaiApiKey })) + }) + + it("should return international default model when no model is specified", () => { + const model = handler.getModel() + expect(model.id).toBe(internationalZAiDefaultModelId) + expect(model.info).toEqual(internationalZAiModels[internationalZAiDefaultModelId]) + }) + + it("should return specified international model when valid model is provided", () => { + const testModelId: InternationalZAiModelId = "glm-4.5-air" + const handlerWithModel = new ZAiHandler({ + apiModelId: testModelId, + zaiApiKey: "test-zai-api-key", + zaiApiLine: "international", + }) + const model = handlerWithModel.getModel() + expect(model.id).toBe(testModelId) + expect(model.info).toEqual(internationalZAiModels[testModelId]) + }) + }) + + describe("China Z AI", () => { + beforeEach(() => { + handler = new ZAiHandler({ zaiApiKey: "test-zai-api-key", zaiApiLine: "china" }) + }) + + it("should use the correct China Z AI base URL", () => { + new ZAiHandler({ zaiApiKey: "test-zai-api-key", zaiApiLine: "china" }) + expect(OpenAI).toHaveBeenCalledWith( + expect.objectContaining({ baseURL: "https://open.bigmodel.cn/api/paas/v4" }), + ) + }) + + it("should use the provided API key for China", () => { + const zaiApiKey = "test-zai-api-key" + new ZAiHandler({ zaiApiKey, zaiApiLine: "china" }) + expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ apiKey: zaiApiKey })) + }) + + it("should return China default model when no model is specified", () => { + const model = handler.getModel() + expect(model.id).toBe(mainlandZAiDefaultModelId) + expect(model.info).toEqual(mainlandZAiModels[mainlandZAiDefaultModelId]) + }) + + it("should return specified China model when valid model is provided", () => { + const testModelId: MainlandZAiModelId = "glm-4.5-air" + const handlerWithModel = new ZAiHandler({ + apiModelId: testModelId, + zaiApiKey: "test-zai-api-key", + zaiApiLine: "china", + }) + const model = handlerWithModel.getModel() + expect(model.id).toBe(testModelId) + expect(model.info).toEqual(mainlandZAiModels[testModelId]) + }) + }) + + describe("Default behavior", () => { + it("should default to international when no zaiApiLine is specified", () => { + const handlerDefault = new ZAiHandler({ zaiApiKey: "test-zai-api-key" }) + expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ baseURL: "https://api.z.ai/api/paas/v4" })) + + const model = handlerDefault.getModel() + expect(model.id).toBe(internationalZAiDefaultModelId) + expect(model.info).toEqual(internationalZAiModels[internationalZAiDefaultModelId]) + }) + + it("should use 'not-provided' as default API key when none is specified", () => { + new ZAiHandler({ zaiApiLine: "international" }) + expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ apiKey: "not-provided" })) + }) + }) + + describe("API Methods", () => { + beforeEach(() => { + handler = new ZAiHandler({ zaiApiKey: "test-zai-api-key", zaiApiLine: "international" }) + }) + + it("completePrompt method should return text from Z AI API", async () => { + const expectedResponse = "This is a test response from Z AI" + mockCreate.mockResolvedValueOnce({ choices: [{ message: { content: expectedResponse } }] }) + const result = await handler.completePrompt("test prompt") + expect(result).toBe(expectedResponse) + }) + + it("should handle errors in completePrompt", async () => { + const errorMessage = "Z AI API error" + mockCreate.mockRejectedValueOnce(new Error(errorMessage)) + await expect(handler.completePrompt("test prompt")).rejects.toThrow( + `Z AI completion error: ${errorMessage}`, + ) + }) + + it("createMessage should yield text content from stream", async () => { + const testContent = "This is test content from Z AI stream" + + mockCreate.mockImplementationOnce(() => { + return { + [Symbol.asyncIterator]: () => ({ + next: vitest + .fn() + .mockResolvedValueOnce({ + done: false, + value: { choices: [{ delta: { content: testContent } }] }, + }) + .mockResolvedValueOnce({ done: true }), + }), + } + }) + + const stream = handler.createMessage("system prompt", []) + const firstChunk = await stream.next() + + expect(firstChunk.done).toBe(false) + expect(firstChunk.value).toEqual({ type: "text", text: testContent }) + }) + + it("createMessage should yield usage data from stream", async () => { + mockCreate.mockImplementationOnce(() => { + return { + [Symbol.asyncIterator]: () => ({ + next: vitest + .fn() + .mockResolvedValueOnce({ + done: false, + value: { + choices: [{ delta: {} }], + usage: { prompt_tokens: 10, completion_tokens: 20 }, + }, + }) + .mockResolvedValueOnce({ done: true }), + }), + } + }) + + const stream = handler.createMessage("system prompt", []) + const firstChunk = await stream.next() + + expect(firstChunk.done).toBe(false) + expect(firstChunk.value).toEqual({ type: "usage", inputTokens: 10, outputTokens: 20 }) + }) + + it("createMessage should pass correct parameters to Z AI client", async () => { + const modelId: InternationalZAiModelId = "glm-4.5" + const modelInfo = internationalZAiModels[modelId] + const handlerWithModel = new ZAiHandler({ + apiModelId: modelId, + zaiApiKey: "test-zai-api-key", + zaiApiLine: "international", + }) + + mockCreate.mockImplementationOnce(() => { + return { + [Symbol.asyncIterator]: () => ({ + async next() { + return { done: true } + }, + }), + } + }) + + const systemPrompt = "Test system prompt for Z AI" + const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Test message for Z AI" }] + + const messageGenerator = handlerWithModel.createMessage(systemPrompt, messages) + await messageGenerator.next() + + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + model: modelId, + max_tokens: modelInfo.maxTokens, + temperature: ZAI_DEFAULT_TEMPERATURE, + messages: expect.arrayContaining([{ role: "system", content: systemPrompt }]), + stream: true, + stream_options: { include_usage: true }, + }), + ) + }) + }) +}) diff --git a/src/api/providers/index.ts b/src/api/providers/index.ts index a1b8f25536..dfcf87b6c9 100644 --- a/src/api/providers/index.ts +++ b/src/api/providers/index.ts @@ -26,3 +26,4 @@ export { UnboundHandler } from "./unbound" export { VertexHandler } from "./vertex" export { VsCodeLmHandler } from "./vscode-lm" export { XAIHandler } from "./xai" +export { ZAiHandler } from "./zai" diff --git a/src/api/providers/zai.ts b/src/api/providers/zai.ts new file mode 100644 index 0000000000..e37e37f01b --- /dev/null +++ b/src/api/providers/zai.ts @@ -0,0 +1,31 @@ +import { + internationalZAiModels, + mainlandZAiModels, + internationalZAiDefaultModelId, + mainlandZAiDefaultModelId, + type InternationalZAiModelId, + type MainlandZAiModelId, + ZAI_DEFAULT_TEMPERATURE, +} from "@roo-code/types" + +import type { ApiHandlerOptions } from "../../shared/api" + +import { BaseOpenAiCompatibleProvider } from "./base-openai-compatible-provider" + +export class ZAiHandler extends BaseOpenAiCompatibleProvider { + constructor(options: ApiHandlerOptions) { + const isChina = options.zaiApiLine === "china" + const models = isChina ? mainlandZAiModels : internationalZAiModels + const defaultModelId = isChina ? mainlandZAiDefaultModelId : internationalZAiDefaultModelId + + super({ + ...options, + providerName: "Z AI", + baseURL: isChina ? "https://open.bigmodel.cn/api/paas/v4" : "https://api.z.ai/api/paas/v4", + apiKey: options.zaiApiKey ?? "not-provided", + defaultProviderModelId: defaultModelId, + providerModels: models, + defaultTemperature: ZAI_DEFAULT_TEMPERATURE, + }) + } +} diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index d70ca553ac..6c521ecfdf 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -28,6 +28,8 @@ import { bedrockDefaultModelId, vertexDefaultModelId, sambaNovaDefaultModelId, + internationalZAiDefaultModelId, + mainlandZAiDefaultModelId, } from "@roo-code/types" import { vscode } from "@src/utils/vscode" @@ -79,6 +81,7 @@ import { Vertex, VSCodeLM, XAI, + ZAi, } from "./providers" import { MODELS_BY_PROVIDER, PROVIDERS } from "./constants" @@ -306,6 +309,13 @@ const ApiOptions = ({ bedrock: { field: "apiModelId", default: bedrockDefaultModelId }, vertex: { field: "apiModelId", default: vertexDefaultModelId }, sambanova: { field: "apiModelId", default: sambaNovaDefaultModelId }, + zai: { + field: "apiModelId", + default: + apiConfiguration.zaiApiLine === "china" + ? mainlandZAiDefaultModelId + : internationalZAiDefaultModelId, + }, openai: { field: "openAiModelId" }, ollama: { field: "ollamaModelId" }, lmstudio: { field: "lmStudioModelId" }, @@ -530,6 +540,10 @@ const ApiOptions = ({ )} + {selectedProvider === "zai" && ( + + )} + {selectedProvider === "human-relay" && ( <>
diff --git a/webview-ui/src/components/settings/constants.ts b/webview-ui/src/components/settings/constants.ts index fae35b1693..c0ddaf89e1 100644 --- a/webview-ui/src/components/settings/constants.ts +++ b/webview-ui/src/components/settings/constants.ts @@ -16,6 +16,7 @@ import { chutesModels, sambaNovaModels, doubaoModels, + internationalZAiModels, } from "@roo-code/types" export const MODELS_BY_PROVIDER: Partial>> = { @@ -34,6 +35,7 @@ export const MODELS_BY_PROVIDER: Partial a.label.localeCompare(b.label)) diff --git a/webview-ui/src/components/settings/providers/ZAi.tsx b/webview-ui/src/components/settings/providers/ZAi.tsx new file mode 100644 index 0000000000..bc23f28346 --- /dev/null +++ b/webview-ui/src/components/settings/providers/ZAi.tsx @@ -0,0 +1,76 @@ +import { useCallback } from "react" +import { VSCodeTextField, VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react" + +import type { ProviderSettings } from "@roo-code/types" + +import { useAppTranslation } from "@src/i18n/TranslationContext" +import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink" + +import { inputEventTransform } from "../transforms" +import { cn } from "@/lib/utils" + +type ZAiProps = { + apiConfiguration: ProviderSettings + setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void +} + +export const ZAi = ({ apiConfiguration, setApiConfigurationField }: ZAiProps) => { + const { t } = useAppTranslation() + + const handleInputChange = useCallback( + ( + field: K, + transform: (event: E) => ProviderSettings[K] = inputEventTransform, + ) => + (event: E | Event) => { + setApiConfigurationField(field, transform(event as E)) + }, + [setApiConfigurationField], + ) + + return ( + <> +
+ + + + api.z.ai + + + open.bigmodel.cn + + +
+ {t("settings:providers.zaiEntrypointDescription")} +
+
+
+ + + +
+ {t("settings:providers.apiKeyStorageNotice")} +
+ {!apiConfiguration?.zaiApiKey && ( + + {t("settings:providers.getZaiApiKey")} + + )} +
+ + ) +} diff --git a/webview-ui/src/components/settings/providers/index.ts b/webview-ui/src/components/settings/providers/index.ts index 47430a0cc8..0f0048df0a 100644 --- a/webview-ui/src/components/settings/providers/index.ts +++ b/webview-ui/src/components/settings/providers/index.ts @@ -22,4 +22,5 @@ export { Unbound } from "./Unbound" export { Vertex } from "./Vertex" export { VSCodeLM } from "./VSCodeLM" export { XAI } from "./XAI" +export { ZAi } from "./ZAi" export { LiteLLM } from "./LiteLLM" diff --git a/webview-ui/src/components/ui/hooks/useSelectedModel.ts b/webview-ui/src/components/ui/hooks/useSelectedModel.ts index 0c6a84a65e..a191014981 100644 --- a/webview-ui/src/components/ui/hooks/useSelectedModel.ts +++ b/webview-ui/src/components/ui/hooks/useSelectedModel.ts @@ -40,6 +40,10 @@ import { sambaNovaDefaultModelId, doubaoModels, doubaoDefaultModelId, + internationalZAiDefaultModelId, + mainlandZAiDefaultModelId, + internationalZAiModels, + mainlandZAiModels, } from "@roo-code/types" import type { ModelRecord, RouterModels } from "@roo/api" @@ -203,6 +207,14 @@ function getSelectedModel({ const info = moonshotModels[id as keyof typeof moonshotModels] return { id, info } } + case "zai": { + const isChina = apiConfiguration.zaiApiLine === "china" + const models = isChina ? mainlandZAiModels : internationalZAiModels + const defaultModelId = isChina ? mainlandZAiDefaultModelId : internationalZAiDefaultModelId + const id = apiConfiguration.apiModelId ?? defaultModelId + const info = models[id as keyof typeof models] + return { id, info } + } case "openai-native": { const id = apiConfiguration.apiModelId ?? openAiNativeDefaultModelId const info = openAiNativeModels[id as keyof typeof openAiNativeModels] diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index cc1bbf5680..4ab333f48f 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -268,6 +268,10 @@ "moonshotApiKey": "Clau API de Moonshot", "getMoonshotApiKey": "Obtenir clau API de Moonshot", "moonshotBaseUrl": "Punt d'entrada de Moonshot", + "zaiApiKey": "Clau API de Z AI", + "getZaiApiKey": "Obtenir clau API de Z AI", + "zaiEntrypoint": "Punt d'entrada de Z AI", + "zaiEntrypointDescription": "Si us plau, seleccioneu el punt d'entrada de l'API apropiat segons la vostra ubicació. Si sou a la Xina, trieu open.bigmodel.cn. Altrament, trieu api.z.ai.", "geminiApiKey": "Clau API de Gemini", "getGroqApiKey": "Obtenir clau API de Groq", "groqApiKey": "Clau API de Groq", diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index 6bee80a8a6..ff893c3356 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -268,6 +268,10 @@ "moonshotApiKey": "Moonshot API-Schlüssel", "getMoonshotApiKey": "Moonshot API-Schlüssel erhalten", "moonshotBaseUrl": "Moonshot-Einstiegspunkt", + "zaiApiKey": "Z AI API-Schlüssel", + "getZaiApiKey": "Z AI API-Schlüssel erhalten", + "zaiEntrypoint": "Z AI Einstiegspunkt", + "zaiEntrypointDescription": "Bitte wählen Sie den entsprechenden API-Einstiegspunkt basierend auf Ihrem Standort. Wenn Sie sich in China befinden, wählen Sie open.bigmodel.cn. Andernfalls wählen Sie api.z.ai.", "geminiApiKey": "Gemini API-Schlüssel", "getGroqApiKey": "Groq API-Schlüssel erhalten", "groqApiKey": "Groq API-Schlüssel", diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index c52841ca83..a48213110a 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -267,6 +267,10 @@ "moonshotApiKey": "Moonshot API Key", "getMoonshotApiKey": "Get Moonshot API Key", "moonshotBaseUrl": "Moonshot Entrypoint", + "zaiApiKey": "Z AI API Key", + "getZaiApiKey": "Get Z AI API Key", + "zaiEntrypoint": "Z AI Entrypoint", + "zaiEntrypointDescription": "Please select the appropriate API entrypoint based on your location. If you are in China, choose open.bigmodel.cn. Otherwise, choose api.z.ai.", "geminiApiKey": "Gemini API Key", "getGroqApiKey": "Get Groq API Key", "groqApiKey": "Groq API Key", diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index 42251f606a..579426cdb6 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -268,6 +268,10 @@ "moonshotApiKey": "Clave API de Moonshot", "getMoonshotApiKey": "Obtener clave API de Moonshot", "moonshotBaseUrl": "Punto de entrada de Moonshot", + "zaiApiKey": "Clave API de Z AI", + "getZaiApiKey": "Obtener clave API de Z AI", + "zaiEntrypoint": "Punto de entrada de Z AI", + "zaiEntrypointDescription": "Por favor, seleccione el punto de entrada de API apropiado según su ubicación. Si está en China, elija open.bigmodel.cn. De lo contrario, elija api.z.ai.", "geminiApiKey": "Clave API de Gemini", "getGroqApiKey": "Obtener clave API de Groq", "groqApiKey": "Clave API de Groq", diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index c527b2e42f..52ac1aec34 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -268,6 +268,10 @@ "moonshotApiKey": "Clé API Moonshot", "getMoonshotApiKey": "Obtenir la clé API Moonshot", "moonshotBaseUrl": "Point d'entrée Moonshot", + "zaiApiKey": "Clé API Z AI", + "getZaiApiKey": "Obtenir la clé API Z AI", + "zaiEntrypoint": "Point d'entrée Z AI", + "zaiEntrypointDescription": "Veuillez sélectionner le point d'entrée API approprié en fonction de votre emplacement. Si vous êtes en Chine, choisissez open.bigmodel.cn. Sinon, choisissez api.z.ai.", "geminiApiKey": "Clé API Gemini", "getGroqApiKey": "Obtenir la clé API Groq", "groqApiKey": "Clé API Groq", diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 5130f818da..7926ae5ba9 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -268,6 +268,10 @@ "moonshotApiKey": "Moonshot API कुंजी", "getMoonshotApiKey": "Moonshot API कुंजी प्राप्त करें", "moonshotBaseUrl": "Moonshot प्रवेश बिंदु", + "zaiApiKey": "Z AI API कुंजी", + "getZaiApiKey": "Z AI API कुंजी प्राप्त करें", + "zaiEntrypoint": "Z AI प्रवेश बिंदु", + "zaiEntrypointDescription": "कृपया अपने स्थान के आधार पर उपयुक्त API प्रवेश बिंदु का चयन करें। यदि आप चीन में हैं, तो open.bigmodel.cn चुनें। अन्यथा, api.z.ai चुनें।", "geminiApiKey": "Gemini API कुंजी", "getGroqApiKey": "Groq API कुंजी प्राप्त करें", "groqApiKey": "Groq API कुंजी", diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index f0285a5130..66e7cb53a1 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -272,6 +272,10 @@ "moonshotApiKey": "Kunci API Moonshot", "getMoonshotApiKey": "Dapatkan Kunci API Moonshot", "moonshotBaseUrl": "Titik Masuk Moonshot", + "zaiApiKey": "Kunci API Z AI", + "getZaiApiKey": "Dapatkan Kunci API Z AI", + "zaiEntrypoint": "Titik Masuk Z AI", + "zaiEntrypointDescription": "Silakan pilih titik masuk API yang sesuai berdasarkan lokasi Anda. Jika Anda berada di China, pilih open.bigmodel.cn. Jika tidak, pilih api.z.ai.", "geminiApiKey": "Gemini API Key", "getGroqApiKey": "Dapatkan Groq API Key", "groqApiKey": "Groq API Key", diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index afdd7b3707..4cfe6ff231 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -268,6 +268,10 @@ "moonshotApiKey": "Chiave API Moonshot", "getMoonshotApiKey": "Ottieni chiave API Moonshot", "moonshotBaseUrl": "Punto di ingresso Moonshot", + "zaiApiKey": "Chiave API Z AI", + "getZaiApiKey": "Ottieni chiave API Z AI", + "zaiEntrypoint": "Punto di ingresso Z AI", + "zaiEntrypointDescription": "Si prega di selezionare il punto di ingresso API appropriato in base alla propria posizione. Se ti trovi in Cina, scegli open.bigmodel.cn. Altrimenti, scegli api.z.ai.", "geminiApiKey": "Chiave API Gemini", "getGroqApiKey": "Ottieni chiave API Groq", "groqApiKey": "Chiave API Groq", diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index debc7ad2ab..a83d78ed39 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -268,6 +268,10 @@ "moonshotApiKey": "Moonshot APIキー", "getMoonshotApiKey": "Moonshot APIキーを取得", "moonshotBaseUrl": "Moonshot エントリーポイント", + "zaiApiKey": "Z AI APIキー", + "getZaiApiKey": "Z AI APIキーを取得", + "zaiEntrypoint": "Z AI エントリーポイント", + "zaiEntrypointDescription": "お住まいの地域に応じて適切な API エントリーポイントを選択してください。中国にお住まいの場合は open.bigmodel.cn を選択してください。それ以外の場合は api.z.ai を選択してください。", "geminiApiKey": "Gemini APIキー", "getGroqApiKey": "Groq APIキーを取得", "groqApiKey": "Groq APIキー", diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index d48012862f..708b1c7ada 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -268,6 +268,10 @@ "moonshotApiKey": "Moonshot API 키", "getMoonshotApiKey": "Moonshot API 키 받기", "moonshotBaseUrl": "Moonshot 엔트리포인트", + "zaiApiKey": "Z AI API 키", + "getZaiApiKey": "Z AI API 키 받기", + "zaiEntrypoint": "Z AI 엔트리포인트", + "zaiEntrypointDescription": "위치에 따라 적절한 API 엔트리포인트를 선택하세요. 중국에 있다면 open.bigmodel.cn을 선택하세요. 그렇지 않으면 api.z.ai를 선택하세요.", "geminiApiKey": "Gemini API 키", "getGroqApiKey": "Groq API 키 받기", "groqApiKey": "Groq API 키", diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index 7722244dd4..dca4ba5c71 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -268,6 +268,10 @@ "moonshotApiKey": "Moonshot API-sleutel", "getMoonshotApiKey": "Moonshot API-sleutel ophalen", "moonshotBaseUrl": "Moonshot-ingangspunt", + "zaiApiKey": "Z AI API-sleutel", + "getZaiApiKey": "Z AI API-sleutel ophalen", + "zaiEntrypoint": "Z AI-ingangspunt", + "zaiEntrypointDescription": "Selecteer het juiste API-ingangspunt op basis van uw locatie. Als u zich in China bevindt, kies dan open.bigmodel.cn. Anders kiest u api.z.ai.", "geminiApiKey": "Gemini API-sleutel", "getGroqApiKey": "Groq API-sleutel ophalen", "groqApiKey": "Groq API-sleutel", diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index 130453764a..5037ceb569 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -268,6 +268,10 @@ "moonshotApiKey": "Klucz API Moonshot", "getMoonshotApiKey": "Uzyskaj klucz API Moonshot", "moonshotBaseUrl": "Punkt wejścia Moonshot", + "zaiApiKey": "Klucz API Z AI", + "getZaiApiKey": "Uzyskaj klucz API Z AI", + "zaiEntrypoint": "Punkt wejścia Z AI", + "zaiEntrypointDescription": "Wybierz odpowiedni punkt wejścia API w zależności od swojej lokalizacji. Jeśli jesteś w Chinach, wybierz open.bigmodel.cn. W przeciwnym razie wybierz api.z.ai.", "geminiApiKey": "Klucz API Gemini", "getGroqApiKey": "Uzyskaj klucz API Groq", "groqApiKey": "Klucz API Groq", diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index 05e20bfee2..c862cee357 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -268,6 +268,10 @@ "moonshotApiKey": "Chave de API Moonshot", "getMoonshotApiKey": "Obter chave de API Moonshot", "moonshotBaseUrl": "Ponto de entrada Moonshot", + "zaiApiKey": "Chave de API Z AI", + "getZaiApiKey": "Obter chave de API Z AI", + "zaiEntrypoint": "Ponto de entrada Z AI", + "zaiEntrypointDescription": "Selecione o ponto de entrada da API apropriado com base na sua localização. Se você estiver na China, escolha open.bigmodel.cn. Caso contrário, escolha api.z.ai.", "geminiApiKey": "Chave de API Gemini", "getGroqApiKey": "Obter chave de API Groq", "groqApiKey": "Chave de API Groq", diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index 6eeb5f134a..25b147f57e 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -268,6 +268,10 @@ "moonshotApiKey": "Moonshot API-ключ", "getMoonshotApiKey": "Получить Moonshot API-ключ", "moonshotBaseUrl": "Точка входа Moonshot", + "zaiApiKey": "Z AI API-ключ", + "getZaiApiKey": "Получить Z AI API-ключ", + "zaiEntrypoint": "Точка входа Z AI", + "zaiEntrypointDescription": "Пожалуйста, выберите подходящую точку входа API в зависимости от вашего местоположения. Если вы находитесь в Китае, выберите open.bigmodel.cn. В противном случае выберите api.z.ai.", "geminiApiKey": "Gemini API-ключ", "getGroqApiKey": "Получить Groq API-ключ", "groqApiKey": "Groq API-ключ", diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index f58ab8ad5e..1aa6ce9783 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -268,6 +268,10 @@ "moonshotApiKey": "Moonshot API Anahtarı", "getMoonshotApiKey": "Moonshot API Anahtarı Al", "moonshotBaseUrl": "Moonshot Giriş Noktası", + "zaiApiKey": "Z AI API Anahtarı", + "getZaiApiKey": "Z AI API Anahtarı Al", + "zaiEntrypoint": "Z AI Giriş Noktası", + "zaiEntrypointDescription": "Konumunuza göre uygun API giriş noktasını seçin. Çin'de iseniz open.bigmodel.cn'yi seçin. Aksi takdirde api.z.ai'yi seçin.", "geminiApiKey": "Gemini API Anahtarı", "getGroqApiKey": "Groq API Anahtarı Al", "groqApiKey": "Groq API Anahtarı", diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index 0b8461b469..3449012f9c 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -268,6 +268,10 @@ "moonshotApiKey": "Khóa API Moonshot", "getMoonshotApiKey": "Lấy khóa API Moonshot", "moonshotBaseUrl": "Điểm vào Moonshot", + "zaiApiKey": "Khóa API Z AI", + "getZaiApiKey": "Lấy khóa API Z AI", + "zaiEntrypoint": "Điểm vào Z AI", + "zaiEntrypointDescription": "Vui lòng chọn điểm vào API phù hợp dựa trên vị trí của bạn. Nếu bạn ở Trung Quốc, hãy chọn open.bigmodel.cn. Ngược lại, hãy chọn api.z.ai.", "geminiApiKey": "Khóa API Gemini", "getGroqApiKey": "Lấy khóa API Groq", "groqApiKey": "Khóa API Groq", diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index f9e82bf87e..e7c53cf757 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -268,6 +268,10 @@ "moonshotApiKey": "Moonshot API 密钥", "getMoonshotApiKey": "获取 Moonshot API 密钥", "moonshotBaseUrl": "Moonshot 服务站点", + "zaiApiKey": "Z AI API 密钥", + "getZaiApiKey": "获取 Z AI API 密钥", + "zaiEntrypoint": "Z AI 服务站点", + "zaiEntrypointDescription": "请根据您的位置选择适当的 API 服务站点。如果您在中国,请选择 open.bigmodel.cn。否则,请选择 api.z.ai。", "geminiApiKey": "Gemini API 密钥", "getGroqApiKey": "获取 Groq API 密钥", "groqApiKey": "Groq API 密钥", diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index f638a782b9..cfdcd6e696 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -268,6 +268,10 @@ "moonshotApiKey": "Moonshot API 金鑰", "getMoonshotApiKey": "取得 Moonshot API 金鑰", "moonshotBaseUrl": "Moonshot 服務站點", + "zaiApiKey": "Z AI API 金鑰", + "getZaiApiKey": "取得 Z AI API 金鑰", + "zaiEntrypoint": "Z AI 服務站點", + "zaiEntrypointDescription": "請根據您的位置選擇適當的 API 服務站點。如果您在中國,請選擇 open.bigmodel.cn。否則,請選擇 api.z.ai。", "geminiApiKey": "Gemini API 金鑰", "getGroqApiKey": "取得 Groq API 金鑰", "groqApiKey": "Groq API 金鑰", From 4e8b17486b08d9fe1c8b9e4f1ac42908de966c1e Mon Sep 17 00:00:00 2001 From: Kaan <92330562+AyazKaan@users.noreply.github.com> Date: Mon, 4 Aug 2025 18:56:05 +0300 Subject: [PATCH 4/7] feat(ui): Make mode selection dropdowns responsive (#6422) --- webview-ui/src/components/modes/ModesView.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/webview-ui/src/components/modes/ModesView.tsx b/webview-ui/src/components/modes/ModesView.tsx index c2b67bc450..93a429408a 100644 --- a/webview-ui/src/components/modes/ModesView.tsx +++ b/webview-ui/src/components/modes/ModesView.tsx @@ -615,9 +615,9 @@ const ModesView = ({ onDone }: ModesViewProps) => { variant="combobox" role="combobox" aria-expanded={open} - className="justify-between w-60" + className="justify-between w-full" data-testid="mode-select-trigger"> -
{getCurrentMode()?.name || t("prompts:modes.selectMode")}
+
{getCurrentMode()?.name || t("prompts:modes.selectMode")}
@@ -716,7 +716,7 @@ const ModesView = ({ onDone }: ModesViewProps) => { text: value, }) }}> - + From c34e4127718081cde6fddbc09294d37ecb8cb29c Mon Sep 17 00:00:00 2001 From: Chris Estreich Date: Mon, 4 Aug 2025 10:58:11 -0700 Subject: [PATCH 5/7] Bump @roo-code/types to v1.44.0 (#6675) --- packages/types/npm/package.json | 2 +- packages/types/src/cloud.ts | 1 + packages/types/src/global-settings.ts | 4 ++++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/types/npm/package.json b/packages/types/npm/package.json index 10a4805127..f73a83a7b6 100644 --- a/packages/types/npm/package.json +++ b/packages/types/npm/package.json @@ -1,6 +1,6 @@ { "name": "@roo-code/types", - "version": "1.43.0", + "version": "1.44.0", "description": "TypeScript type definitions for Roo Code.", "publishConfig": { "access": "public", diff --git a/packages/types/src/cloud.ts b/packages/types/src/cloud.ts index a4eb9f96a8..be9a039d43 100644 --- a/packages/types/src/cloud.ts +++ b/packages/types/src/cloud.ts @@ -17,6 +17,7 @@ export interface CloudUserInfo { organizationName?: string organizationRole?: string organizationImageUrl?: string + extensionBridgeEnabled?: boolean } /** diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index 6de4d7413f..41945ff470 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -134,6 +134,8 @@ export const globalSettingsSchema = z.object({ mcpEnabled: z.boolean().optional(), enableMcpServerCreation: z.boolean().optional(), + remoteControlEnabled: z.boolean().optional(), + mode: z.string().optional(), modeApiConfigs: z.record(z.string(), z.string()).optional(), customModes: z.array(modeConfigSchema).optional(), @@ -288,6 +290,8 @@ export const EVALS_SETTINGS: RooCodeSettings = { mcpEnabled: false, + remoteControlEnabled: false, + mode: "code", // "architect", customModes: [], From 7ca4901024854a27a66329d537592c4c96966162 Mon Sep 17 00:00:00 2001 From: "roomote[bot]" <219738659+roomote[bot]@users.noreply.github.com> Date: Mon, 4 Aug 2025 11:03:34 -0700 Subject: [PATCH 6/7] fix: prevent empty mode names from being saved (fixes #5766) (#5767) * fix: prevent empty mode names from being saved (fixes #5766) - Add frontend validation in ModesView to prevent empty names from being saved - Add onBlur handler to restore original name if field is left empty - Add backend validation in CustomModesManager.updateCustomMode using modeConfigSchema - Provide user feedback when validation fails - Trim whitespace from mode names before validation This prevents YAML parsing errors caused by empty mode name fields. * fix: improve UX by allowing users to empty mode name field - Remove restriction that prevented users from emptying the name field - Remove onBlur handler that automatically restored original name - Allow backend validation to handle empty names and show appropriate errors - Users can now type freely but invalid saves are prevented by backend validation Addresses feedback from @daniel-lxs in PR #5767 * fix: allow emptying mode name field but prevent saving when invalid - Modified onBlur handler to check if name is empty before saving - If empty, revert to original name instead of saving empty value - This provides better UX as requested in PR review * fix: add proper JSON formatting to source map writes for Windows compatibility --------- Co-authored-by: Roo Code --- src/core/config/CustomModesManager.ts | 12 ++++++---- webview-ui/src/components/modes/ModesView.tsx | 22 +++++++++++++------ .../src/vite-plugins/sourcemapPlugin.ts | 4 ++-- 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/core/config/CustomModesManager.ts b/src/core/config/CustomModesManager.ts index 095ed86cb7..a9a2e6a6b5 100644 --- a/src/core/config/CustomModesManager.ts +++ b/src/core/config/CustomModesManager.ts @@ -405,9 +405,13 @@ export class CustomModesManager { // Validate the mode configuration before saving const validationResult = modeConfigSchema.safeParse(config) if (!validationResult.success) { - const errors = validationResult.error.errors.map((e) => e.message).join(", ") - logger.error(`Invalid mode configuration for ${slug}`, { errors: validationResult.error.errors }) - throw new Error(`Invalid mode configuration: ${errors}`) + const errorMessages = validationResult.error.errors + .map((err) => `${err.path.join(".")}: ${err.message}`) + .join(", ") + const errorMessage = `Invalid mode configuration: ${errorMessages}` + logger.error("Mode validation failed", { slug, errors: validationResult.error.errors }) + vscode.window.showErrorMessage(t("common:customModes.errors.updateFailed", { error: errorMessage })) + return } const isProjectMode = config.source === "project" @@ -786,7 +790,7 @@ export class CustomModesManager { // This excludes the rules-{slug} folder from the path const relativePath = path.relative(modeRulesDir, filePath) // Normalize path to use forward slashes for cross-platform compatibility - const normalizedRelativePath = relativePath.replace(/\\/g, '/') + const normalizedRelativePath = relativePath.replace(/\\/g, "/") rulesFiles.push({ relativePath: normalizedRelativePath, content: content.trim() }) } } diff --git a/webview-ui/src/components/modes/ModesView.tsx b/webview-ui/src/components/modes/ModesView.tsx index 93a429408a..21c531937f 100644 --- a/webview-ui/src/components/modes/ModesView.tsx +++ b/webview-ui/src/components/modes/ModesView.tsx @@ -755,17 +755,25 @@ const ModesView = ({ onDone }: ModesViewProps) => { } }} onChange={(e) => { - setLocalModeName(e.target.value) + const newName = e.target.value + // Allow users to type freely, including emptying the field + setLocalModeName(newName) }} onBlur={() => { const customMode = findModeBySlug(visualMode, customModes) - if (customMode && localModeName.trim()) { + if (customMode) { + const trimmedName = localModeName.trim() // Only update if the name is not empty - updateCustomMode(visualMode, { - ...customMode, - name: localModeName, - source: customMode.source || "global", - }) + if (trimmedName) { + updateCustomMode(visualMode, { + ...customMode, + name: trimmedName, + source: customMode.source || "global", + }) + } else { + // Revert to the original name if empty + setLocalModeName(customMode.name) + } } // Clear the editing state setCurrentEditingModeSlug(null) diff --git a/webview-ui/src/vite-plugins/sourcemapPlugin.ts b/webview-ui/src/vite-plugins/sourcemapPlugin.ts index 9eb1e7b642..1449c888f2 100644 --- a/webview-ui/src/vite-plugins/sourcemapPlugin.ts +++ b/webview-ui/src/vite-plugins/sourcemapPlugin.ts @@ -88,8 +88,8 @@ export function sourcemapPlugin(): Plugin { }) } - // Write back the updated source map - fs.writeFileSync(mapPath, JSON.stringify(mapContent)) + // Write back the updated source map with proper formatting + fs.writeFileSync(mapPath, JSON.stringify(mapContent, null, 2)) console.log(`Updated source map for ${jsFile}`) } catch (error) { console.error(`Error processing source map for ${jsFile}:`, error) From 1d714c8ce4d925b7cc7c100702e215ee3cda1a48 Mon Sep 17 00:00:00 2001 From: Chris Estreich Date: Mon, 4 Aug 2025 13:58:14 -0700 Subject: [PATCH 7/7] Extension bridge (#6677) Co-authored-by: Matt Rubens --- pnpm-lock.yaml | 28 +++-- src/core/task/Task.ts | 29 ++++- src/core/webview/ClineProvider.ts | 115 +++++++++++++++++- src/core/webview/webviewMessageHandler.ts | 5 + src/extension.ts | 64 +++++----- src/package.json | 2 +- src/shared/ExtensionMessage.ts | 1 + src/shared/WebviewMessage.ts | 1 + src/utils/remoteControl.ts | 11 ++ .../src/components/account/AccountView.tsx | 71 ++++++++--- .../account/__tests__/AccountView.spec.tsx | 87 +++++++++++-- webview-ui/src/components/modes/ModesView.tsx | 4 +- .../src/context/ExtensionStateContext.tsx | 5 + webview-ui/src/i18n/locales/ca/account.json | 3 + webview-ui/src/i18n/locales/de/account.json | 3 + webview-ui/src/i18n/locales/en/account.json | 14 ++- webview-ui/src/i18n/locales/en/settings.json | 2 +- webview-ui/src/i18n/locales/es/account.json | 3 + webview-ui/src/i18n/locales/fr/account.json | 3 + webview-ui/src/i18n/locales/hi/account.json | 3 + webview-ui/src/i18n/locales/id/account.json | 3 + webview-ui/src/i18n/locales/it/account.json | 3 + webview-ui/src/i18n/locales/ja/account.json | 3 + webview-ui/src/i18n/locales/ko/account.json | 3 + webview-ui/src/i18n/locales/nl/account.json | 3 + webview-ui/src/i18n/locales/pl/account.json | 3 + .../src/i18n/locales/pt-BR/account.json | 3 + webview-ui/src/i18n/locales/ru/account.json | 3 + webview-ui/src/i18n/locales/tr/account.json | 3 + webview-ui/src/i18n/locales/vi/account.json | 3 + .../src/i18n/locales/zh-CN/account.json | 3 + .../src/i18n/locales/zh-TW/account.json | 3 + 32 files changed, 404 insertions(+), 86 deletions(-) create mode 100644 src/utils/remoteControl.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b2847df1a1..0d952b6aeb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -563,8 +563,8 @@ importers: specifier: ^1.14.0 version: 1.14.0(typescript@5.8.3) '@roo-code/cloud': - specifier: ^0.4.0 - version: 0.4.0 + specifier: ^0.5.0 + version: 0.5.0 '@roo-code/ipc': specifier: workspace:^ version: link:../packages/ipc @@ -3065,11 +3065,11 @@ packages: cpu: [x64] os: [win32] - '@roo-code/cloud@0.4.0': - resolution: {integrity: sha512-1a27RG2YjQFfsU5UlfbQnpj/K/6gYBcysp2FXaX9+VaaTh5ZzReQeHJ9uREnyE059zoFpVuNywwNxGadzyotWw==} + '@roo-code/cloud@0.5.0': + resolution: {integrity: sha512-4u6Ce2Rmr5a9nxhjGUMRRWUWhZc63EmF/UJ/+Az5/1JARMOp0kHN5Pwqz2QAgfD137+TFSBKQORpiN0GXrdt2w==} - '@roo-code/types@1.42.0': - resolution: {integrity: sha512-AITVSV6WFd17jE8lQXFy7PkHam8M+mMkT7o9ipGZZ3cV7SbrnmL/Hg/HjkA9lkdJYbcC5dEK94py8KVBQn8Umw==} + '@roo-code/types@1.44.0': + resolution: {integrity: sha512-3xbW4pYaCgWuHF5qOsiXpIcd281dlFTe1zboUGgcUUsB414Hu3pQI86PdgJxVGtZgxtaca0eHTQ2Sqjqq8nPlA==} '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} @@ -6267,8 +6267,8 @@ packages: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} - ioredis@5.7.0: - resolution: {integrity: sha512-NUcA93i1lukyXU+riqEyPtSEkyFq8tX90uL659J+qpCZ3rEdViB/APC58oAhIh3+bJln2hzdlZbBZsGNrlsR8g==} + ioredis@5.6.1: + resolution: {integrity: sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA==} engines: {node: '>=12.22.0'} ip-address@9.0.5: @@ -12191,16 +12191,18 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.40.2': optional: true - '@roo-code/cloud@0.4.0': + '@roo-code/cloud@0.5.0': dependencies: - '@roo-code/types': 1.42.0 - ioredis: 5.7.0 + '@roo-code/types': 1.44.0 + ioredis: 5.6.1 p-wait-for: 5.0.2 zod: 3.25.76 transitivePeerDependencies: - supports-color - '@roo-code/types@1.42.0': {} + '@roo-code/types@1.44.0': + dependencies: + zod: 3.25.76 '@sec-ant/readable-stream@0.4.1': {} @@ -15963,7 +15965,7 @@ snapshots: internmap@2.0.3: {} - ioredis@5.7.0: + ioredis@5.6.1: dependencies: '@ioredis/commands': 1.3.0 cluster-key-slot: 1.1.2 diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 3cb6abe7f7..e0c332d16f 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -32,7 +32,7 @@ import { isBlockingAsk, } from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" -import { CloudService } from "@roo-code/cloud" +import { CloudService, TaskBridgeService } from "@roo-code/cloud" // api import { ApiHandler, ApiHandlerCreateMessageMetadata, buildApiHandler } from "../../api" @@ -118,6 +118,7 @@ export type TaskOptions = { parentTask?: Task taskNumber?: number onCreated?: (task: Task) => void + enableTaskBridge?: boolean } export class Task extends EventEmitter implements TaskLike { @@ -237,6 +238,9 @@ export class Task extends EventEmitter implements TaskLike { checkpointService?: RepoPerTaskCheckpointService checkpointServiceInitializing = false + // Task Bridge + taskBridgeService?: TaskBridgeService + // Streaming isWaitingForFirstChunk = false isStreaming = false @@ -268,6 +272,7 @@ export class Task extends EventEmitter implements TaskLike { parentTask, taskNumber = -1, onCreated, + enableTaskBridge = false, }: TaskOptions) { super() @@ -345,6 +350,11 @@ export class Task extends EventEmitter implements TaskLike { this.toolRepetitionDetector = new ToolRepetitionDetector(this.consecutiveMistakeLimit) + // Initialize TaskBridgeService only if enabled + if (enableTaskBridge) { + this.taskBridgeService = TaskBridgeService.getInstance() + } + onCreated?.(this) if (startTask) { @@ -931,6 +941,11 @@ export class Task extends EventEmitter implements TaskLike { // Start / Abort / Resume private async startTask(task?: string, images?: string[]): Promise { + if (this.taskBridgeService) { + await this.taskBridgeService.initialize() + await this.taskBridgeService.subscribeToTask(this) + } + // `conversationHistory` (for API) and `clineMessages` (for webview) // need to be in sync. // If the extension process were killed, then on restart the @@ -982,6 +997,11 @@ export class Task extends EventEmitter implements TaskLike { } private async resumeTaskFromHistory() { + if (this.taskBridgeService) { + await this.taskBridgeService.initialize() + await this.taskBridgeService.subscribeToTask(this) + } + const modifiedClineMessages = await this.getSavedClineMessages() // Remove any resume messages that may have been added before @@ -1227,6 +1247,13 @@ export class Task extends EventEmitter implements TaskLike { this.pauseInterval = undefined } + // Unsubscribe from TaskBridge service. + if (this.taskBridgeService) { + this.taskBridgeService + .unsubscribeFromTask(this.taskId) + .catch((error) => console.error("Error unsubscribing from task bridge:", error)) + } + // Release any terminals associated with this task. try { // Release any terminals associated with this task. diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index ed8f8a27d1..384de58be7 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -17,7 +17,6 @@ import { type ProviderSettings, type RooCodeSettings, type ProviderSettingsEntry, - type ProviderSettingsWithId, type TelemetryProperties, type TelemetryPropertiesProvider, type CodeActionId, @@ -66,6 +65,7 @@ import { fileExistsAtPath } from "../../utils/fs" import { setTtsEnabled, setTtsSpeed } from "../../utils/tts" import { getWorkspaceGitInfo } from "../../utils/git" import { getWorkspacePath } from "../../utils/path" +import { isRemoteControlEnabled } from "../../utils/remoteControl" import { setPanel } from "../../activate/registerCommands" @@ -111,6 +111,8 @@ export class ClineProvider protected mcpHub?: McpHub // Change from private to protected private marketplaceManager: MarketplaceManager private mdmService?: MdmService + private taskCreationCallback: (task: Task) => void + private taskEventListeners: WeakMap void>> = new WeakMap() public isViewLaunched = false public settingsImportedAt?: number @@ -162,6 +164,40 @@ export class ClineProvider this.marketplaceManager = new MarketplaceManager(this.context, this.customModesManager) + this.taskCreationCallback = (instance: Task) => { + this.emit(RooCodeEventName.TaskCreated, instance) + + // Create named listener functions so we can remove them later. + const onTaskStarted = () => this.emit(RooCodeEventName.TaskStarted, instance.taskId) + const onTaskCompleted = (taskId: string, tokenUsage: any, toolUsage: any) => + this.emit(RooCodeEventName.TaskCompleted, taskId, tokenUsage, toolUsage) + const onTaskAborted = () => this.emit(RooCodeEventName.TaskAborted, instance.taskId) + const onTaskFocused = () => this.emit(RooCodeEventName.TaskFocused, instance.taskId) + const onTaskUnfocused = () => this.emit(RooCodeEventName.TaskUnfocused, instance.taskId) + const onTaskActive = (taskId: string) => this.emit(RooCodeEventName.TaskActive, taskId) + const onTaskIdle = (taskId: string) => this.emit(RooCodeEventName.TaskIdle, taskId) + + // Attach the listeners. + instance.on(RooCodeEventName.TaskStarted, onTaskStarted) + instance.on(RooCodeEventName.TaskCompleted, onTaskCompleted) + instance.on(RooCodeEventName.TaskAborted, onTaskAborted) + instance.on(RooCodeEventName.TaskFocused, onTaskFocused) + instance.on(RooCodeEventName.TaskUnfocused, onTaskUnfocused) + instance.on(RooCodeEventName.TaskActive, onTaskActive) + instance.on(RooCodeEventName.TaskIdle, onTaskIdle) + + // Store the cleanup functions for later removal. + this.taskEventListeners.set(instance, [ + () => instance.off(RooCodeEventName.TaskStarted, onTaskStarted), + () => instance.off(RooCodeEventName.TaskCompleted, onTaskCompleted), + () => instance.off(RooCodeEventName.TaskAborted, onTaskAborted), + () => instance.off(RooCodeEventName.TaskFocused, onTaskFocused), + () => instance.off(RooCodeEventName.TaskUnfocused, onTaskUnfocused), + () => instance.off(RooCodeEventName.TaskActive, onTaskActive), + () => instance.off(RooCodeEventName.TaskIdle, onTaskIdle), + ]) + } + // Initialize Roo Code Cloud profile sync. this.initializeCloudProfileSync().catch((error) => { this.log(`Failed to initialize cloud profile sync: ${error}`) @@ -297,6 +333,14 @@ export class ClineProvider task.emit(RooCodeEventName.TaskUnfocused) + // Remove event listeners before clearing the reference. + const cleanupFunctions = this.taskEventListeners.get(task) + + if (cleanupFunctions) { + cleanupFunctions.forEach((cleanup) => cleanup()) + this.taskEventListeners.delete(task) + } + // Make sure no reference kept, once promises end it will be // garbage collected. task = undefined @@ -654,12 +698,17 @@ export class ClineProvider enableCheckpoints, fuzzyMatchThreshold, experiments, + cloudUserInfo, + remoteControlEnabled, } = await this.getState() if (!ProfileValidator.isProfileAllowed(apiConfiguration, organizationAllowList)) { throw new OrganizationAllowListViolationError(t("common:errors.violated_organization_allowlist")) } + // Determine if TaskBridge should be enabled + const enableTaskBridge = isRemoteControlEnabled(cloudUserInfo, remoteControlEnabled) + const task = new Task({ provider: this, apiConfiguration, @@ -673,7 +722,8 @@ export class ClineProvider rootTask: this.clineStack.length > 0 ? this.clineStack[0] : undefined, parentTask, taskNumber: this.clineStack.length + 1, - onCreated: (instance) => this.emit(RooCodeEventName.TaskCreated, instance), + onCreated: this.taskCreationCallback, + enableTaskBridge, ...options, }) @@ -738,8 +788,13 @@ export class ClineProvider enableCheckpoints, fuzzyMatchThreshold, experiments, + cloudUserInfo, + remoteControlEnabled, } = await this.getState() + // Determine if TaskBridge should be enabled + const enableTaskBridge = isRemoteControlEnabled(cloudUserInfo, remoteControlEnabled) + const task = new Task({ provider: this, apiConfiguration, @@ -752,7 +807,8 @@ export class ClineProvider rootTask: historyItem.rootTask, parentTask: historyItem.parentTask, taskNumber: historyItem.number, - onCreated: (instance) => this.emit(RooCodeEventName.TaskCreated, instance), + onCreated: this.taskCreationCallback, + enableTaskBridge, }) await this.addClineToStack(task) @@ -1631,6 +1687,7 @@ export class ClineProvider includeDiagnosticMessages, maxDiagnosticMessages, includeTaskHistoryInEnhance, + remoteControlEnabled, } = await this.getState() const telemetryKey = process.env.POSTHOG_API_KEY @@ -1758,6 +1815,7 @@ export class ClineProvider includeDiagnosticMessages: includeDiagnosticMessages ?? true, maxDiagnosticMessages: maxDiagnosticMessages ?? 50, includeTaskHistoryInEnhance: includeTaskHistoryInEnhance ?? false, + remoteControlEnabled: remoteControlEnabled ?? false, } } @@ -1945,6 +2003,8 @@ export class ClineProvider maxDiagnosticMessages: stateValues.maxDiagnosticMessages ?? 50, // Add includeTaskHistoryInEnhance setting includeTaskHistoryInEnhance: stateValues.includeTaskHistoryInEnhance ?? false, + // Add remoteControlEnabled setting + remoteControlEnabled: stateValues.remoteControlEnabled ?? false, } } @@ -2057,6 +2117,55 @@ export class ClineProvider return true } + /** + * Handle remote control enabled/disabled state changes + * Manages ExtensionBridgeService and TaskBridgeService lifecycle + */ + public async handleRemoteControlToggle(enabled: boolean): Promise { + const { + CloudService: CloudServiceImport, + ExtensionBridgeService, + TaskBridgeService, + } = await import("@roo-code/cloud") + const userInfo = CloudServiceImport.instance.getUserInfo() + + // Handle ExtensionBridgeService using static method + await ExtensionBridgeService.handleRemoteControlState(userInfo, enabled, this, (message: string) => + this.log(message), + ) + + if (isRemoteControlEnabled(userInfo, enabled)) { + // Set up TaskBridgeService for the currently active task if one exists + const currentTask = this.getCurrentCline() + if (currentTask && !currentTask.taskBridgeService) { + try { + currentTask.taskBridgeService = TaskBridgeService.getInstance() + await currentTask.taskBridgeService.subscribeToTask(currentTask) + this.log(`[TaskBridgeService] Subscribed current task ${currentTask.taskId} to TaskBridge`) + } catch (error) { + const message = `[TaskBridgeService#subscribeToTask] ${error instanceof Error ? error.message : String(error)}` + this.log(message) + console.error(message) + } + } + } else { + // Disconnect TaskBridgeService for all tasks in the stack + for (const task of this.clineStack) { + if (task.taskBridgeService) { + try { + await task.taskBridgeService.unsubscribeFromTask(task.taskId) + task.taskBridgeService = undefined + this.log(`[TaskBridgeService] Unsubscribed task ${task.taskId} from TaskBridge`) + } catch (error) { + const message = `[TaskBridgeService#unsubscribeFromTask] for task ${task.taskId}: ${error instanceof Error ? error.message : String(error)}` + this.log(message) + console.error(message) + } + } + } + } + } + /** * Returns properties to be included in every telemetry event * This method is called by the telemetry service to get context information diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index fdb7e90425..743e3b0c13 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -906,6 +906,11 @@ export const webviewMessageHandler = async ( await updateGlobalState("enableMcpServerCreation", message.bool ?? true) await provider.postStateToWebview() break + case "remoteControlEnabled": + await updateGlobalState("remoteControlEnabled", message.bool ?? false) + await provider.handleRemoteControlToggle(message.bool ?? false) + await provider.postStateToWebview() + break case "refreshAllMcpServers": { const mcpHub = provider.getMcpHub() if (mcpHub) { diff --git a/src/extension.ts b/src/extension.ts index beb69b30b5..ea6ab4e1b4 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -12,7 +12,7 @@ try { console.warn("Failed to load environment variables:", e) } -import { CloudService } from "@roo-code/cloud" +import { CloudService, ExtensionBridgeService } from "@roo-code/cloud" import { TelemetryService, PostHogTelemetryClient } from "@roo-code/telemetry" import "./utils/path" // Necessary to have access to String.prototype.toPosix. @@ -29,6 +29,7 @@ import { CodeIndexManager } from "./services/code-index/manager" import { MdmService } from "./services/mdm/MdmService" import { migrateSettings } from "./utils/migrateSettings" import { autoImportSettings } from "./utils/autoImportSettings" +import { isRemoteControlEnabled } from "./utils/remoteControl" import { API } from "./extension/api" import { @@ -71,37 +72,13 @@ export async function activate(context: vscode.ExtensionContext) { console.warn("Failed to register PostHogTelemetryClient:", error) } - // Create logger for cloud services + // Create logger for cloud services. const cloudLogger = createDualLogger(createOutputChannelLogger(outputChannel)) - // Initialize Roo Code Cloud service. - const cloudService = await CloudService.createInstance(context, cloudLogger) - - try { - if (cloudService.telemetryClient) { - TelemetryService.instance.register(cloudService.telemetryClient) - } - } catch (error) { - outputChannel.appendLine( - `[CloudService] Failed to register TelemetryClient: ${error instanceof Error ? error.message : String(error)}`, - ) - } - - const postStateListener = () => { - ClineProvider.getVisibleInstance()?.postStateToWebview() - } - - cloudService.on("auth-state-changed", postStateListener) - cloudService.on("user-info", postStateListener) - cloudService.on("settings-updated", postStateListener) - - // Add to subscriptions for proper cleanup on deactivate - context.subscriptions.push(cloudService) - // Initialize MDM service const mdmService = await MdmService.createInstance(cloudLogger) - // Initialize i18n for internationalization support + // Initialize i18n for internationalization support. initializeI18n(context.globalState.get("language") ?? formatLanguage(vscode.env.language)) // Initialize terminal shell execution handlers. @@ -126,6 +103,29 @@ export async function activate(context: vscode.ExtensionContext) { ) } + // Initialize Roo Code Cloud service. + const cloudService = await CloudService.createInstance(context, cloudLogger) + + const postStateListener = () => ClineProvider.getVisibleInstance()?.postStateToWebview() + + cloudService.on("auth-state-changed", postStateListener) + cloudService.on("settings-updated", postStateListener) + + cloudService.on("user-info", ({ userInfo }) => { + postStateListener() + + // Check if remote control is enabled in user settings + const remoteControlEnabled = contextProxy.getValue("remoteControlEnabled") + + // Handle ExtensionBridgeService state using static method + ExtensionBridgeService.handleRemoteControlState(userInfo, remoteControlEnabled, provider, (message: string) => + outputChannel.appendLine(message), + ) + }) + + // Add to subscriptions for proper cleanup on deactivate. + context.subscriptions.push(cloudService) + const provider = new ClineProvider(context, outputChannel, "sidebar", contextProxy, codeIndexManager, mdmService) TelemetryService.instance.setProvider(provider) @@ -139,7 +139,7 @@ export async function activate(context: vscode.ExtensionContext) { }), ) - // Auto-import configuration if specified in settings + // Auto-import configuration if specified in settings. try { await autoImportSettings(outputChannel, { providerSettingsManager: provider.providerSettingsManager, @@ -232,6 +232,14 @@ export async function activate(context: vscode.ExtensionContext) { // This method is called when your extension is deactivated. export async function deactivate() { outputChannel.appendLine(`${Package.name} extension deactivated`) + + // Cleanup Extension Bridge service. + const extensionBridgeService = ExtensionBridgeService.getInstance() + + if (extensionBridgeService) { + await extensionBridgeService.disconnect() + } + await McpServerManager.cleanup(extensionContext) TelemetryService.instance.shutdown() TerminalRegistry.cleanup() diff --git a/src/package.json b/src/package.json index aa2110dfd5..d35f6f34dd 100644 --- a/src/package.json +++ b/src/package.json @@ -420,7 +420,7 @@ "@mistralai/mistralai": "^1.3.6", "@modelcontextprotocol/sdk": "^1.9.0", "@qdrant/js-client-rest": "^1.14.0", - "@roo-code/cloud": "^0.4.0", + "@roo-code/cloud": "^0.5.0", "@roo-code/ipc": "workspace:^", "@roo-code/telemetry": "workspace:^", "@roo-code/types": "workspace:^", diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 930edeac73..2313d7d177 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -270,6 +270,7 @@ export type ExtensionState = Pick< | "profileThresholds" | "includeDiagnosticMessages" | "maxDiagnosticMessages" + | "remoteControlEnabled" > & { version: string clineMessages: ClineMessage[] diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index cb8759d851..2d94896bf5 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -130,6 +130,7 @@ export interface WebviewMessage { | "terminalCompressProgressBar" | "mcpEnabled" | "enableMcpServerCreation" + | "remoteControlEnabled" | "searchCommits" | "alwaysApproveResubmit" | "requestDelaySeconds" diff --git a/src/utils/remoteControl.ts b/src/utils/remoteControl.ts new file mode 100644 index 0000000000..f003b522d1 --- /dev/null +++ b/src/utils/remoteControl.ts @@ -0,0 +1,11 @@ +import type { CloudUserInfo } from "@roo-code/types" + +/** + * Determines if remote control features should be enabled + * @param cloudUserInfo - User information from cloud service + * @param remoteControlEnabled - User's remote control setting + * @returns true if remote control should be enabled + */ +export function isRemoteControlEnabled(cloudUserInfo?: CloudUserInfo | null, remoteControlEnabled?: boolean): boolean { + return !!(cloudUserInfo?.id && cloudUserInfo.extensionBridgeEnabled && remoteControlEnabled) +} diff --git a/webview-ui/src/components/account/AccountView.tsx b/webview-ui/src/components/account/AccountView.tsx index e3d1a293a7..e36818cc3a 100644 --- a/webview-ui/src/components/account/AccountView.tsx +++ b/webview-ui/src/components/account/AccountView.tsx @@ -5,8 +5,12 @@ import type { CloudUserInfo } from "@roo-code/types" import { TelemetryEventName } from "@roo-code/types" import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useExtensionState } from "@src/context/ExtensionStateContext" import { vscode } from "@src/utils/vscode" import { telemetryClient } from "@src/utils/TelemetryClient" +import { ToggleSwitch } from "@/components/ui/toggle-switch" + +import { History, PiggyBank, Router, SquareArrowOutUpRightIcon } from "lucide-react" type AccountViewProps = { userInfo: CloudUserInfo | null @@ -17,6 +21,7 @@ type AccountViewProps = { export const AccountView = ({ userInfo, isAuthenticated, cloudApiUrl, onDone }: AccountViewProps) => { const { t } = useAppTranslation() + const { remoteControlEnabled, setRemoteControlEnabled } = useExtensionState() const wasAuthenticatedRef = useRef(false) const rooLogoUri = (window as any).IMAGES_BASE_URI + "/roo-logo.svg" @@ -51,11 +56,17 @@ export const AccountView = ({ userInfo, isAuthenticated, cloudApiUrl, onDone }: vscode.postMessage({ type: "openExternal", url: cloudUrl }) } + const handleRemoteControlToggle = () => { + const newValue = !remoteControlEnabled + setRemoteControlEnabled(newValue) + vscode.postMessage({ type: "remoteControlEnabled", bool: newValue }) + } + return ( -
+

{t("account:title")}

- + {t("settings:common.done")}
@@ -77,13 +88,13 @@ export const AccountView = ({ userInfo, isAuthenticated, cloudApiUrl, onDone }: )}
{userInfo.name && ( -

{userInfo.name}

+

{userInfo.name}

)} {userInfo?.email && ( -

{userInfo?.email}

+

{userInfo?.email}

)} {userInfo?.organizationName && ( -
+
{userInfo.organizationImageUrl && ( )} + + {/* Remote Control Toggle - only show if user has extension bridge enabled */} + {userInfo?.extensionBridgeEnabled && ( +
+
+ + {t("account:remoteControl")} +
+
+ {t("account:remoteControlDescription")} +
+
+
+ )} +
{t("account:visitCloudWebsite")} @@ -125,30 +157,31 @@ export const AccountView = ({ userInfo, isAuthenticated, cloudApiUrl, onDone }:
-

+

{t("account:cloudBenefitsTitle")}

-

- {t("account:cloudBenefitsSubtitle")} -

-
    -
  • - - {t("account:cloudBenefitHistory")} +
      +
    • + + {t("account:cloudBenefitWalkaway")}
    • -
    • - +
    • + {t("account:cloudBenefitSharing")}
    • -
    • - +
    • + + {t("account:cloudBenefitHistory")} +
    • +
    • + {t("account:cloudBenefitMetrics")}
-
- +
+ {t("account:connect")}
diff --git a/webview-ui/src/components/account/__tests__/AccountView.spec.tsx b/webview-ui/src/components/account/__tests__/AccountView.spec.tsx index d6fd3013e6..2af759d615 100644 --- a/webview-ui/src/components/account/__tests__/AccountView.spec.tsx +++ b/webview-ui/src/components/account/__tests__/AccountView.spec.tsx @@ -11,11 +11,17 @@ vi.mock("@src/i18n/TranslationContext", () => ({ "settings:common.done": "Done", "account:signIn": "Connect to Roo Code Cloud", "account:cloudBenefitsTitle": "Connect to Roo Code Cloud", - "account:cloudBenefitsSubtitle": "Sync your prompts and telemetry to enable:", - "account:cloudBenefitHistory": "Online task history", - "account:cloudBenefitSharing": "Sharing and collaboration features", - "account:cloudBenefitMetrics": "Task, token, and cost-based usage metrics", + "account:cloudBenefitWalkaway": "Follow and control tasks from anywhere with Roomote Control", + "account:cloudBenefitSharing": "Share tasks with others", + "account:cloudBenefitHistory": "Access your task history", + "account:cloudBenefitMetrics": "Get a holistic view of your token consumption", "account:logOut": "Log out", + "account:connect": "Connect Now", + "account:visitCloudWebsite": "Visit Roo Code Cloud", + "account:remoteControl": "Roomote Control", + "account:remoteControlDescription": + "Enable following and interacting with tasks in this workspace with Roo Code Cloud", + "account:profilePicture": "Profile picture", } return translations[key] || key }, @@ -36,6 +42,14 @@ vi.mock("@src/utils/TelemetryClient", () => ({ }, })) +// Mock the extension state context +vi.mock("@src/context/ExtensionStateContext", () => ({ + useExtensionState: () => ({ + remoteControlEnabled: false, + setRemoteControlEnabled: vi.fn(), + }), +})) + // Mock window global for images Object.defineProperty(window, "IMAGES_BASE_URI", { value: "/images", @@ -55,13 +69,13 @@ describe("AccountView", () => { // Check that the benefits section is displayed expect(screen.getByRole("heading", { name: "Connect to Roo Code Cloud" })).toBeInTheDocument() - expect(screen.getByText("Sync your prompts and telemetry to enable:")).toBeInTheDocument() - expect(screen.getByText("Online task history")).toBeInTheDocument() - expect(screen.getByText("Sharing and collaboration features")).toBeInTheDocument() - expect(screen.getByText("Task, token, and cost-based usage metrics")).toBeInTheDocument() + expect(screen.getByText("Follow and control tasks from anywhere with Roomote Control")).toBeInTheDocument() + expect(screen.getByText("Share tasks with others")).toBeInTheDocument() + expect(screen.getByText("Access your task history")).toBeInTheDocument() + expect(screen.getByText("Get a holistic view of your token consumption")).toBeInTheDocument() // Check that the connect button is also present - expect(screen.getByText("account:connect")).toBeInTheDocument() + expect(screen.getByText("Connect Now")).toBeInTheDocument() }) it("should not display benefits when user is authenticated", () => { @@ -80,13 +94,60 @@ describe("AccountView", () => { ) // Check that the benefits section is NOT displayed - expect(screen.queryByText("Sync your prompts and telemetry to enable:")).not.toBeInTheDocument() - expect(screen.queryByText("Online task history")).not.toBeInTheDocument() - expect(screen.queryByText("Sharing and collaboration features")).not.toBeInTheDocument() - expect(screen.queryByText("Task, token, and cost-based usage metrics")).not.toBeInTheDocument() + expect( + screen.queryByText("Follow and control tasks from anywhere with Roomote Control"), + ).not.toBeInTheDocument() + expect(screen.queryByText("Share tasks with others")).not.toBeInTheDocument() + expect(screen.queryByText("Access your task history")).not.toBeInTheDocument() + expect(screen.queryByText("Get a holistic view of your token consumption")).not.toBeInTheDocument() // Check that user info is displayed instead expect(screen.getByText("Test User")).toBeInTheDocument() expect(screen.getByText("test@example.com")).toBeInTheDocument() }) + + it("should display remote control toggle when user has extension bridge enabled", () => { + const mockUserInfo = { + name: "Test User", + email: "test@example.com", + extensionBridgeEnabled: true, + } + + render( + {}} + />, + ) + + // Check that the remote control toggle is displayed + expect(screen.getByTestId("remote-control-toggle")).toBeInTheDocument() + expect(screen.getByText("Roomote Control")).toBeInTheDocument() + expect( + screen.getByText("Enable following and interacting with tasks in this workspace with Roo Code Cloud"), + ).toBeInTheDocument() + }) + + it("should not display remote control toggle when user does not have extension bridge enabled", () => { + const mockUserInfo = { + name: "Test User", + email: "test@example.com", + extensionBridgeEnabled: false, + } + + render( + {}} + />, + ) + + // Check that the remote control toggle is NOT displayed + expect(screen.queryByTestId("remote-control-toggle")).not.toBeInTheDocument() + expect(screen.queryByText("Roomote Control")).not.toBeInTheDocument() + }) }) diff --git a/webview-ui/src/components/modes/ModesView.tsx b/webview-ui/src/components/modes/ModesView.tsx index 21c531937f..d470f7a658 100644 --- a/webview-ui/src/components/modes/ModesView.tsx +++ b/webview-ui/src/components/modes/ModesView.tsx @@ -617,7 +617,9 @@ const ModesView = ({ onDone }: ModesViewProps) => { aria-expanded={open} className="justify-between w-full" data-testid="mode-select-trigger"> -
{getCurrentMode()?.name || t("prompts:modes.selectMode")}
+
+ {getCurrentMode()?.name || t("prompts:modes.selectMode")} +
diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index da7ab63358..12f13bdf55 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -97,6 +97,8 @@ export interface ExtensionStateContextType extends ExtensionState { setMcpEnabled: (value: boolean) => void enableMcpServerCreation: boolean setEnableMcpServerCreation: (value: boolean) => void + remoteControlEnabled: boolean + setRemoteControlEnabled: (value: boolean) => void alwaysApproveResubmit?: boolean setAlwaysApproveResubmit: (value: boolean) => void requestDelaySeconds: number @@ -195,6 +197,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode terminalShellIntegrationTimeout: 4000, mcpEnabled: true, enableMcpServerCreation: false, + remoteControlEnabled: false, alwaysApproveResubmit: false, requestDelaySeconds: 5, currentApiConfigName: "default", @@ -408,6 +411,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode profileThresholds: state.profileThresholds ?? {}, alwaysAllowFollowupQuestions, followupAutoApproveTimeoutMs, + remoteControlEnabled: state.remoteControlEnabled ?? false, setExperimentEnabled: (id, enabled) => setState((prevState) => ({ ...prevState, experiments: { ...prevState.experiments, [id]: enabled } })), setApiConfiguration, @@ -454,6 +458,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode setMcpEnabled: (value) => setState((prevState) => ({ ...prevState, mcpEnabled: value })), setEnableMcpServerCreation: (value) => setState((prevState) => ({ ...prevState, enableMcpServerCreation: value })), + setRemoteControlEnabled: (value) => setState((prevState) => ({ ...prevState, remoteControlEnabled: value })), setAlwaysApproveResubmit: (value) => setState((prevState) => ({ ...prevState, alwaysApproveResubmit: value })), setRequestDelaySeconds: (value) => setState((prevState) => ({ ...prevState, requestDelaySeconds: value })), setCurrentApiConfigName: (value) => setState((prevState) => ({ ...prevState, currentApiConfigName: value })), diff --git a/webview-ui/src/i18n/locales/ca/account.json b/webview-ui/src/i18n/locales/ca/account.json index a94a978b87..2804cc8dfa 100644 --- a/webview-ui/src/i18n/locales/ca/account.json +++ b/webview-ui/src/i18n/locales/ca/account.json @@ -10,5 +10,8 @@ "cloudBenefitHistory": "Historial de tasques en línia", "cloudBenefitSharing": "Funcions de compartició i col·laboració", "cloudBenefitMetrics": "Mètriques d'ús basades en tasques, tokens i costos", + "cloudBenefitWalkaway": "Segueix i controla tasques des de qualsevol lloc amb Roomote Control", + "remoteControl": "Roomote Control", + "remoteControlDescription": "Permet seguir i interactuar amb tasques en aquest espai de treball amb Roo Code Cloud", "visitCloudWebsite": "Visita Roo Code Cloud" } diff --git a/webview-ui/src/i18n/locales/de/account.json b/webview-ui/src/i18n/locales/de/account.json index bd4d71eada..6edaf58fff 100644 --- a/webview-ui/src/i18n/locales/de/account.json +++ b/webview-ui/src/i18n/locales/de/account.json @@ -10,5 +10,8 @@ "cloudBenefitHistory": "Online-Aufgabenverlauf", "cloudBenefitSharing": "Freigabe- und Kollaborationsfunktionen", "cloudBenefitMetrics": "Aufgaben-, Token- und kostenbasierte Nutzungsmetriken", + "cloudBenefitWalkaway": "Verfolge und steuere Aufgaben von überall mit Roomote Control", + "remoteControl": "Roomote Control", + "remoteControlDescription": "Ermöglicht das Verfolgen und Interagieren mit Aufgaben in diesem Arbeitsbereich mit Roo Code Cloud", "visitCloudWebsite": "Roo Code Cloud besuchen" } diff --git a/webview-ui/src/i18n/locales/en/account.json b/webview-ui/src/i18n/locales/en/account.json index f900abb297..a73acef432 100644 --- a/webview-ui/src/i18n/locales/en/account.json +++ b/webview-ui/src/i18n/locales/en/account.json @@ -4,11 +4,13 @@ "logOut": "Log out", "testApiAuthentication": "Test API Authentication", "signIn": "Connect to Roo Code Cloud", - "connect": "Connect", + "connect": "Connect Now", "cloudBenefitsTitle": "Connect to Roo Code Cloud", - "cloudBenefitsSubtitle": "Sync your prompts and telemetry to enable:", - "cloudBenefitHistory": "Online task history", - "cloudBenefitSharing": "Sharing and collaboration features", - "cloudBenefitMetrics": "Task, token, and cost-based usage metrics", - "visitCloudWebsite": "Visit Roo Code Cloud" + "cloudBenefitWalkaway": "Follow and control tasks from anywhere with Roomote Control", + "cloudBenefitSharing": "Share tasks with others", + "cloudBenefitHistory": "Access your task history", + "cloudBenefitMetrics": "Get a holistic view of your token consumption", + "visitCloudWebsite": "Visit Roo Code Cloud", + "remoteControl": "Roomote Control", + "remoteControlDescription": "Enable following and interacting with tasks in this workspace with Roo Code Cloud" } diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index a48213110a..b20482d1b2 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -686,7 +686,7 @@ "name": "Enable concurrent file edits", "description": "When enabled, Roo can edit multiple files in a single request. When disabled, Roo must edit files one at a time. Disabling this can help when working with less capable models or when you want more control over file modifications." }, -"PREVENT_FOCUS_DISRUPTION": { + "PREVENT_FOCUS_DISRUPTION": { "name": "Background editing", "description": "Prevent editor focus disruption when enabled. File edits happen in the background without opening diff views or stealing focus. You can continue working uninterrupted while Roo makes changes. Files can be opened without focus to capture diagnostics or kept closed entirely." }, diff --git a/webview-ui/src/i18n/locales/es/account.json b/webview-ui/src/i18n/locales/es/account.json index 2bda10e82f..c8398ae25a 100644 --- a/webview-ui/src/i18n/locales/es/account.json +++ b/webview-ui/src/i18n/locales/es/account.json @@ -10,5 +10,8 @@ "cloudBenefitHistory": "Historial de tareas en línea", "cloudBenefitSharing": "Funciones de compartir y colaboración", "cloudBenefitMetrics": "Métricas de uso basadas en tareas, tokens y costos", + "cloudBenefitWalkaway": "Sigue y controla tareas desde cualquier lugar con Roomote Control", + "remoteControl": "Roomote Control", + "remoteControlDescription": "Permite seguir e interactuar con tareas en este espacio de trabajo con Roo Code Cloud", "visitCloudWebsite": "Visitar Roo Code Cloud" } diff --git a/webview-ui/src/i18n/locales/fr/account.json b/webview-ui/src/i18n/locales/fr/account.json index 1af4483c5c..e50d11af15 100644 --- a/webview-ui/src/i18n/locales/fr/account.json +++ b/webview-ui/src/i18n/locales/fr/account.json @@ -10,5 +10,8 @@ "cloudBenefitHistory": "Historique des tâches en ligne", "cloudBenefitSharing": "Fonctionnalités de partage et collaboration", "cloudBenefitMetrics": "Métriques d'utilisation basées sur les tâches, tokens et coûts", + "cloudBenefitWalkaway": "Suivez et contrôlez les tâches depuis n'importe où avec Roomote Control", + "remoteControl": "Roomote Control", + "remoteControlDescription": "Permet de suivre et d'interagir avec les tâches dans cet espace de travail avec Roo Code Cloud", "visitCloudWebsite": "Visiter Roo Code Cloud" } diff --git a/webview-ui/src/i18n/locales/hi/account.json b/webview-ui/src/i18n/locales/hi/account.json index be6ea00d88..485bc00633 100644 --- a/webview-ui/src/i18n/locales/hi/account.json +++ b/webview-ui/src/i18n/locales/hi/account.json @@ -10,5 +10,8 @@ "cloudBenefitHistory": "ऑनलाइन कार्य इतिहास", "cloudBenefitSharing": "साझाकरण और सहयोग सुविधाएं", "cloudBenefitMetrics": "कार्य, token और लागत आधारित उपयोग मेट्रिक्स", + "cloudBenefitWalkaway": "Roomote Control के साथ कहीं से भी कार्यों को फॉलो और नियंत्रित करें", + "remoteControl": "Roomote Control", + "remoteControlDescription": "Roo Code Cloud के साथ इस वर्कस्पेस में कार्यों को फॉलो और इंटरैक्ट करने की सुविधा दें", "visitCloudWebsite": "Roo Code Cloud पर जाएं" } diff --git a/webview-ui/src/i18n/locales/id/account.json b/webview-ui/src/i18n/locales/id/account.json index 57f3fec0df..a3b6f4b97e 100644 --- a/webview-ui/src/i18n/locales/id/account.json +++ b/webview-ui/src/i18n/locales/id/account.json @@ -10,5 +10,8 @@ "cloudBenefitHistory": "Riwayat tugas online", "cloudBenefitSharing": "Fitur berbagi dan kolaborasi", "cloudBenefitMetrics": "Metrik penggunaan berdasarkan tugas, token, dan biaya", + "cloudBenefitWalkaway": "Ikuti dan kontrol tugas dari mana saja dengan Roomote Control", + "remoteControl": "Roomote Control", + "remoteControlDescription": "Memungkinkan mengikuti dan berinteraksi dengan tugas di workspace ini dengan Roo Code Cloud", "visitCloudWebsite": "Kunjungi Roo Code Cloud" } diff --git a/webview-ui/src/i18n/locales/it/account.json b/webview-ui/src/i18n/locales/it/account.json index fda13f563c..7ffb569407 100644 --- a/webview-ui/src/i18n/locales/it/account.json +++ b/webview-ui/src/i18n/locales/it/account.json @@ -10,5 +10,8 @@ "cloudBenefitHistory": "Cronologia attività online", "cloudBenefitSharing": "Funzionalità di condivisione e collaborazione", "cloudBenefitMetrics": "Metriche di utilizzo basate su attività, token e costi", + "cloudBenefitWalkaway": "Segui e controlla le attività da qualsiasi luogo con Roomote Control", + "remoteControl": "Roomote Control", + "remoteControlDescription": "Abilita il monitoraggio e l'interazione con le attività in questo workspace con Roo Code Cloud", "visitCloudWebsite": "Visita Roo Code Cloud" } diff --git a/webview-ui/src/i18n/locales/ja/account.json b/webview-ui/src/i18n/locales/ja/account.json index b41eaf7895..331d613f9b 100644 --- a/webview-ui/src/i18n/locales/ja/account.json +++ b/webview-ui/src/i18n/locales/ja/account.json @@ -10,5 +10,8 @@ "cloudBenefitHistory": "オンラインタスク履歴", "cloudBenefitSharing": "共有とコラボレーション機能", "cloudBenefitMetrics": "タスク、Token、コストベースの使用メトリクス", + "cloudBenefitWalkaway": "Roomote Controlでどこからでもタスクをフォローし制御", + "remoteControl": "Roomote Control", + "remoteControlDescription": "Roo Code Cloudでこのワークスペースのタスクをフォローし操作することを有効にする", "visitCloudWebsite": "Roo Code Cloudを訪問" } diff --git a/webview-ui/src/i18n/locales/ko/account.json b/webview-ui/src/i18n/locales/ko/account.json index 6ad06d43fa..98b09b6e3d 100644 --- a/webview-ui/src/i18n/locales/ko/account.json +++ b/webview-ui/src/i18n/locales/ko/account.json @@ -10,5 +10,8 @@ "cloudBenefitHistory": "온라인 작업 기록", "cloudBenefitSharing": "공유 및 협업 기능", "cloudBenefitMetrics": "작업, 토큰, 비용 기반 사용 메트릭", + "cloudBenefitWalkaway": "Roomote Control로 어디서나 작업을 팔로우하고 제어하세요", + "remoteControl": "Roomote Control", + "remoteControlDescription": "Roo Code Cloud로 이 워크스페이스의 작업을 팔로우하고 상호작용할 수 있게 합니다", "visitCloudWebsite": "Roo Code Cloud 방문" } diff --git a/webview-ui/src/i18n/locales/nl/account.json b/webview-ui/src/i18n/locales/nl/account.json index 15ceb1865b..94d08b4409 100644 --- a/webview-ui/src/i18n/locales/nl/account.json +++ b/webview-ui/src/i18n/locales/nl/account.json @@ -10,5 +10,8 @@ "cloudBenefitHistory": "Online taakgeschiedenis", "cloudBenefitSharing": "Deel- en samenwerkingsfuncties", "cloudBenefitMetrics": "Taak-, token- en kostengebaseerde gebruiksstatistieken", + "cloudBenefitWalkaway": "Volg en beheer taken van overal met Roomote Control", + "remoteControl": "Roomote Control", + "remoteControlDescription": "Schakel het volgen en interacteren met taken in deze workspace in met Roo Code Cloud", "visitCloudWebsite": "Bezoek Roo Code Cloud" } diff --git a/webview-ui/src/i18n/locales/pl/account.json b/webview-ui/src/i18n/locales/pl/account.json index fdb0e4d894..b25f29b1bb 100644 --- a/webview-ui/src/i18n/locales/pl/account.json +++ b/webview-ui/src/i18n/locales/pl/account.json @@ -10,5 +10,8 @@ "cloudBenefitHistory": "Historia zadań online", "cloudBenefitSharing": "Funkcje udostępniania i współpracy", "cloudBenefitMetrics": "Metryki użycia oparte na zadaniach, tokenach i kosztach", + "cloudBenefitWalkaway": "Śledź i kontroluj zadania z dowolnego miejsca za pomocą Roomote Control", + "remoteControl": "Roomote Control", + "remoteControlDescription": "Umożliwia śledzenie i interakcję z zadaniami w tym obszarze roboczym za pomocą Roo Code Cloud", "visitCloudWebsite": "Odwiedź Roo Code Cloud" } diff --git a/webview-ui/src/i18n/locales/pt-BR/account.json b/webview-ui/src/i18n/locales/pt-BR/account.json index 5492ca7520..5b4f457b99 100644 --- a/webview-ui/src/i18n/locales/pt-BR/account.json +++ b/webview-ui/src/i18n/locales/pt-BR/account.json @@ -10,5 +10,8 @@ "cloudBenefitHistory": "Histórico de tarefas online", "cloudBenefitSharing": "Recursos de compartilhamento e colaboração", "cloudBenefitMetrics": "Métricas de uso baseadas em tarefas, tokens e custos", + "cloudBenefitWalkaway": "Acompanhe e controle tarefas de qualquer lugar com Roomote Control", + "remoteControl": "Roomote Control", + "remoteControlDescription": "Permite acompanhar e interagir com tarefas neste workspace com Roo Code Cloud", "visitCloudWebsite": "Visitar Roo Code Cloud" } diff --git a/webview-ui/src/i18n/locales/ru/account.json b/webview-ui/src/i18n/locales/ru/account.json index 1c8dcf5289..4f4a2de167 100644 --- a/webview-ui/src/i18n/locales/ru/account.json +++ b/webview-ui/src/i18n/locales/ru/account.json @@ -10,5 +10,8 @@ "cloudBenefitHistory": "Онлайн-история задач", "cloudBenefitSharing": "Функции обмена и совместной работы", "cloudBenefitMetrics": "Метрики использования на основе задач, токенов и затрат", + "cloudBenefitWalkaway": "Отслеживайте и управляйте задачами откуда угодно с Roomote Control", + "remoteControl": "Roomote Control", + "remoteControlDescription": "Позволяет отслеживать и взаимодействовать с задачами в этом рабочем пространстве с Roo Code Cloud", "visitCloudWebsite": "Посетить Roo Code Cloud" } diff --git a/webview-ui/src/i18n/locales/tr/account.json b/webview-ui/src/i18n/locales/tr/account.json index a344ce940f..03131e3fb5 100644 --- a/webview-ui/src/i18n/locales/tr/account.json +++ b/webview-ui/src/i18n/locales/tr/account.json @@ -10,5 +10,8 @@ "cloudBenefitHistory": "Çevrimiçi görev geçmişi", "cloudBenefitSharing": "Paylaşım ve işbirliği özellikleri", "cloudBenefitMetrics": "Görev, token ve maliyet tabanlı kullanım metrikleri", + "cloudBenefitWalkaway": "Roomote Control ile görevleri her yerden takip et ve kontrol et", + "remoteControl": "Roomote Control", + "remoteControlDescription": "Bu çalışma alanındaki görevleri Roo Code Cloud ile takip etme ve etkileşim kurma imkanı sağlar", "visitCloudWebsite": "Roo Code Cloud'u ziyaret et" } diff --git a/webview-ui/src/i18n/locales/vi/account.json b/webview-ui/src/i18n/locales/vi/account.json index 0e826b75ad..3224160ba3 100644 --- a/webview-ui/src/i18n/locales/vi/account.json +++ b/webview-ui/src/i18n/locales/vi/account.json @@ -10,5 +10,8 @@ "cloudBenefitHistory": "Lịch sử tác vụ trực tuyến", "cloudBenefitSharing": "Tính năng chia sẻ và cộng tác", "cloudBenefitMetrics": "Số liệu sử dụng dựa trên tác vụ, token và chi phí", + "cloudBenefitWalkaway": "Theo dõi và điều khiển tác vụ từ bất kỳ đâu với Roomote Control", + "remoteControl": "Roomote Control", + "remoteControlDescription": "Cho phép theo dõi và tương tác với các tác vụ trong workspace này với Roo Code Cloud", "visitCloudWebsite": "Truy cập Roo Code Cloud" } diff --git a/webview-ui/src/i18n/locales/zh-CN/account.json b/webview-ui/src/i18n/locales/zh-CN/account.json index 65a4c1d221..9e097472a0 100644 --- a/webview-ui/src/i18n/locales/zh-CN/account.json +++ b/webview-ui/src/i18n/locales/zh-CN/account.json @@ -10,5 +10,8 @@ "cloudBenefitHistory": "在线任务历史", "cloudBenefitSharing": "共享和协作功能", "cloudBenefitMetrics": "基于任务、Token 和成本的使用指标", + "cloudBenefitWalkaway": "使用 Roomote Control 随时随地跟踪和控制任务", + "remoteControl": "Roomote Control", + "remoteControlDescription": "允许通过 Roo Code Cloud 跟踪和操作此工作区中的任务", "visitCloudWebsite": "访问 Roo Code Cloud" } diff --git a/webview-ui/src/i18n/locales/zh-TW/account.json b/webview-ui/src/i18n/locales/zh-TW/account.json index dca8d3231c..edd25dcf18 100644 --- a/webview-ui/src/i18n/locales/zh-TW/account.json +++ b/webview-ui/src/i18n/locales/zh-TW/account.json @@ -10,5 +10,8 @@ "cloudBenefitHistory": "線上工作歷史", "cloudBenefitSharing": "分享和協作功能", "cloudBenefitMetrics": "基於工作、Token 和成本的使用指標", + "cloudBenefitWalkaway": "使用 Roomote Control 隨時隨地追蹤和控制工作", + "remoteControl": "Roomote Control", + "remoteControlDescription": "允許透過 Roo Code Cloud 追蹤和操作此工作區中的工作", "visitCloudWebsite": "造訪 Roo Code Cloud" }