From 0500894b34e7d6650635ef836eb6eb0577d0a390 Mon Sep 17 00:00:00 2001 From: "roomote[bot]" <219738659+roomote[bot]@users.noreply.github.com> Date: Mon, 21 Jul 2025 19:37:42 -0400 Subject: [PATCH 1/6] fix: sort symlinked rules files by symlink names, not target names (#5903) * fix: sort symlinked rules files alphabetically - Add alphabetical sorting to readTextFilesFromDirectory function - Sort by basename of filename (case-insensitive) for consistent order - Fixes issue where symlinked rules were read in random order - Add test case to verify alphabetical sorting behavior Fixes #4131 * chore: remove solution-indicating comment per PR feedback * fix: sort symlinks by their symlink names, not target names - Modified readTextFilesFromDirectory to store both original symlink path and resolved target path - Updated resolveDirectoryEntry and resolveSymLink to track both paths - Sort files by original path (symlink name) but read content from resolved path - Added test to verify symlinks are sorted by their names, not their target names - This ensures consistent alphabetical ordering when using symlinks in rules directories --------- Co-authored-by: Roo Code --- .../__tests__/custom-instructions.spec.ts | 151 ++++++++++++++++++ .../prompts/sections/custom-instructions.ts | 55 ++++--- 2 files changed, 188 insertions(+), 18 deletions(-) diff --git a/src/core/prompts/sections/__tests__/custom-instructions.spec.ts b/src/core/prompts/sections/__tests__/custom-instructions.spec.ts index 9c8e003143..f6fb8ea1f4 100644 --- a/src/core/prompts/sections/__tests__/custom-instructions.spec.ts +++ b/src/core/prompts/sections/__tests__/custom-instructions.spec.ts @@ -1033,6 +1033,157 @@ describe("Rules directory reading", () => { expect(result).toContain("content of file3") }) + it("should return files in alphabetical order by filename", async () => { + // Simulate .roo/rules directory exists + statMock.mockResolvedValueOnce({ + isDirectory: vi.fn().mockReturnValue(true), + } as any) + + // Simulate listing files in non-alphabetical order to test sorting + readdirMock.mockResolvedValueOnce([ + { name: "zebra.txt", isFile: () => true, parentPath: "/fake/path/.roo/rules" }, + { name: "alpha.txt", isFile: () => true, parentPath: "/fake/path/.roo/rules" }, + { name: "Beta.txt", isFile: () => true, parentPath: "/fake/path/.roo/rules" }, // Test case-insensitive sorting + ] as any) + + statMock.mockImplementation((path) => { + return Promise.resolve({ + isFile: vi.fn().mockReturnValue(true), + }) as any + }) + + readFileMock.mockImplementation((filePath: PathLike) => { + const pathStr = filePath.toString() + const normalizedPath = pathStr.replace(/\\/g, "/") + if (normalizedPath === "/fake/path/.roo/rules/zebra.txt") { + return Promise.resolve("zebra content") + } + if (normalizedPath === "/fake/path/.roo/rules/alpha.txt") { + return Promise.resolve("alpha content") + } + if (normalizedPath === "/fake/path/.roo/rules/Beta.txt") { + return Promise.resolve("beta content") + } + return Promise.reject({ code: "ENOENT" }) + }) + + const result = await loadRuleFiles("/fake/path") + + // Files should appear in alphabetical order: alpha.txt, Beta.txt, zebra.txt + const alphaIndex = result.indexOf("alpha content") + const betaIndex = result.indexOf("beta content") + const zebraIndex = result.indexOf("zebra content") + + expect(alphaIndex).toBeLessThan(betaIndex) + expect(betaIndex).toBeLessThan(zebraIndex) + + // Verify the expected file paths are in the result + const expectedAlphaPath = + process.platform === "win32" ? "\\fake\\path\\.roo\\rules\\alpha.txt" : "/fake/path/.roo/rules/alpha.txt" + const expectedBetaPath = + process.platform === "win32" ? "\\fake\\path\\.roo\\rules\\Beta.txt" : "/fake/path/.roo/rules/Beta.txt" + const expectedZebraPath = + process.platform === "win32" ? "\\fake\\path\\.roo\\rules\\zebra.txt" : "/fake/path/.roo/rules/zebra.txt" + + expect(result).toContain(`# Rules from ${expectedAlphaPath}:`) + expect(result).toContain(`# Rules from ${expectedBetaPath}:`) + expect(result).toContain(`# Rules from ${expectedZebraPath}:`) + }) + + it("should sort symlinks by their symlink names, not target names", async () => { + // Reset mocks + statMock.mockReset() + readdirMock.mockReset() + readlinkMock.mockReset() + readFileMock.mockReset() + + // First call: check if .roo/rules directory exists + statMock.mockResolvedValueOnce({ + isDirectory: vi.fn().mockReturnValue(true), + } as any) + + // Simulate listing files with symlinks that point to files with different names + readdirMock.mockResolvedValueOnce([ + { + name: "01-first.link", + isFile: () => false, + isSymbolicLink: () => true, + parentPath: "/fake/path/.roo/rules", + }, + { + name: "02-second.link", + isFile: () => false, + isSymbolicLink: () => true, + parentPath: "/fake/path/.roo/rules", + }, + { + name: "03-third.link", + isFile: () => false, + isSymbolicLink: () => true, + parentPath: "/fake/path/.roo/rules", + }, + ] as any) + + // Mock readlink to return target paths that would sort differently than symlink names + readlinkMock + .mockResolvedValueOnce("../../targets/zzz-last.txt") // 01-first.link -> zzz-last.txt + .mockResolvedValueOnce("../../targets/aaa-first.txt") // 02-second.link -> aaa-first.txt + .mockResolvedValueOnce("../../targets/mmm-middle.txt") // 03-third.link -> mmm-middle.txt + + // Set up stat mock for the remaining calls + statMock.mockImplementation((path) => { + const normalizedPath = path.toString().replace(/\\/g, "/") + // Target files exist and are files + if (normalizedPath.endsWith(".txt")) { + return Promise.resolve({ + isFile: vi.fn().mockReturnValue(true), + isDirectory: vi.fn().mockReturnValue(false), + } as any) + } + return Promise.resolve({ + isFile: vi.fn().mockReturnValue(false), + isDirectory: vi.fn().mockReturnValue(false), + } as any) + }) + + readFileMock.mockImplementation((filePath: PathLike) => { + const pathStr = filePath.toString() + const normalizedPath = pathStr.replace(/\\/g, "/") + if (normalizedPath.endsWith("zzz-last.txt")) { + return Promise.resolve("content from zzz-last.txt") + } + if (normalizedPath.endsWith("aaa-first.txt")) { + return Promise.resolve("content from aaa-first.txt") + } + if (normalizedPath.endsWith("mmm-middle.txt")) { + return Promise.resolve("content from mmm-middle.txt") + } + return Promise.reject({ code: "ENOENT" }) + }) + + const result = await loadRuleFiles("/fake/path") + + // Content should appear in order of symlink names (01-first, 02-second, 03-third) + // NOT in order of target names (aaa-first, mmm-middle, zzz-last) + const firstIndex = result.indexOf("content from zzz-last.txt") // from 01-first.link + const secondIndex = result.indexOf("content from aaa-first.txt") // from 02-second.link + const thirdIndex = result.indexOf("content from mmm-middle.txt") // from 03-third.link + + // All content should be found + expect(firstIndex).toBeGreaterThan(-1) + expect(secondIndex).toBeGreaterThan(-1) + expect(thirdIndex).toBeGreaterThan(-1) + + // And they should be in the order of symlink names, not target names + expect(firstIndex).toBeLessThan(secondIndex) + expect(secondIndex).toBeLessThan(thirdIndex) + + // Verify the target paths are shown (not symlink paths) + expect(result).toContain("zzz-last.txt") + expect(result).toContain("aaa-first.txt") + expect(result).toContain("mmm-middle.txt") + }) + it("should handle empty file list gracefully", async () => { // Simulate .roo/rules directory exists statMock.mockResolvedValueOnce({ diff --git a/src/core/prompts/sections/custom-instructions.ts b/src/core/prompts/sections/custom-instructions.ts index 3c8558a57f..71613ff94a 100644 --- a/src/core/prompts/sections/custom-instructions.ts +++ b/src/core/prompts/sections/custom-instructions.ts @@ -44,7 +44,7 @@ const MAX_DEPTH = 5 async function resolveDirectoryEntry( entry: Dirent, dirPath: string, - filePaths: string[], + fileInfo: Array<{ originalPath: string; resolvedPath: string }>, depth: number, ): Promise { // Avoid cyclic symlinks @@ -54,44 +54,49 @@ async function resolveDirectoryEntry( const fullPath = path.resolve(entry.parentPath || dirPath, entry.name) if (entry.isFile()) { - // Regular file - filePaths.push(fullPath) + // Regular file - both original and resolved paths are the same + fileInfo.push({ originalPath: fullPath, resolvedPath: fullPath }) } else if (entry.isSymbolicLink()) { // Await the resolution of the symbolic link - await resolveSymLink(fullPath, filePaths, depth + 1) + await resolveSymLink(fullPath, fileInfo, depth + 1) } } /** * Recursively resolve a symbolic link and collect file paths */ -async function resolveSymLink(fullPath: string, filePaths: string[], depth: number): Promise { +async function resolveSymLink( + symlinkPath: string, + fileInfo: Array<{ originalPath: string; resolvedPath: string }>, + depth: number, +): Promise { // Avoid cyclic symlinks if (depth > MAX_DEPTH) { return } try { // Get the symlink target - const linkTarget = await fs.readlink(fullPath) + const linkTarget = await fs.readlink(symlinkPath) // Resolve the target path (relative to the symlink location) - const resolvedTarget = path.resolve(path.dirname(fullPath), linkTarget) + const resolvedTarget = path.resolve(path.dirname(symlinkPath), linkTarget) // Check if the target is a file const stats = await fs.stat(resolvedTarget) if (stats.isFile()) { - filePaths.push(resolvedTarget) + // For symlinks to files, store the symlink path as original and target as resolved + fileInfo.push({ originalPath: symlinkPath, resolvedPath: resolvedTarget }) } else if (stats.isDirectory()) { const anotherEntries = await fs.readdir(resolvedTarget, { withFileTypes: true, recursive: true }) // Collect promises for recursive calls within the directory const directoryPromises: Promise[] = [] for (const anotherEntry of anotherEntries) { - directoryPromises.push(resolveDirectoryEntry(anotherEntry, resolvedTarget, filePaths, depth + 1)) + directoryPromises.push(resolveDirectoryEntry(anotherEntry, resolvedTarget, fileInfo, depth + 1)) } // Wait for all entries in the resolved directory to be processed await Promise.all(directoryPromises) } else if (stats.isSymbolicLink()) { // Handle nested symlinks by awaiting the recursive call - await resolveSymLink(resolvedTarget, filePaths, depth + 1) + await resolveSymLink(resolvedTarget, fileInfo, depth + 1) } } catch (err) { // Skip invalid symlinks @@ -106,29 +111,31 @@ async function readTextFilesFromDirectory(dirPath: string): Promise = [] // Collect promises for the initial resolution calls const initialPromises: Promise[] = [] for (const entry of entries) { - initialPromises.push(resolveDirectoryEntry(entry, dirPath, filePaths, 0)) + initialPromises.push(resolveDirectoryEntry(entry, dirPath, fileInfo, 0)) } // Wait for all asynchronous operations (including recursive ones) to complete await Promise.all(initialPromises) const fileContents = await Promise.all( - filePaths.map(async (file) => { + fileInfo.map(async ({ originalPath, resolvedPath }) => { try { // Check if it's a file (not a directory) - const stats = await fs.stat(file) + const stats = await fs.stat(resolvedPath) if (stats.isFile()) { // Filter out cache files and system files that shouldn't be in rules - if (!shouldIncludeRuleFile(file)) { + if (!shouldIncludeRuleFile(resolvedPath)) { return null } - const content = await safeReadFile(file) - return { filename: file, content } + const content = await safeReadFile(resolvedPath) + // Use resolvedPath for display to maintain existing behavior + return { filename: resolvedPath, content, sortKey: originalPath } } return null } catch (err) { @@ -138,7 +145,19 @@ async function readTextFilesFromDirectory(dirPath: string): Promise item !== null) + const filteredFiles = fileContents.filter( + (item): item is { filename: string; content: string; sortKey: string } => item !== null, + ) + + // Sort files alphabetically by the original filename (case-insensitive) to ensure consistent order + // For symlinks, this will use the symlink name, not the target name + return filteredFiles + .sort((a, b) => { + const filenameA = path.basename(a.sortKey).toLowerCase() + const filenameB = path.basename(b.sortKey).toLowerCase() + return filenameA.localeCompare(filenameB) + }) + .map(({ filename, content }) => ({ filename, content })) } catch (err) { return [] } From 8334f0869ff8f576d299e065d2cc729d2746ecab Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Mon, 21 Jul 2025 22:26:39 -0400 Subject: [PATCH 2/6] Update the max_tokens fallback logic in the sliding window (#5993) --- .../sliding-window/__tests__/sliding-window.spec.ts | 12 ++++++------ src/core/sliding-window/index.ts | 3 ++- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/core/sliding-window/__tests__/sliding-window.spec.ts b/src/core/sliding-window/__tests__/sliding-window.spec.ts index 3bda5351d4..393d50307e 100644 --- a/src/core/sliding-window/__tests__/sliding-window.spec.ts +++ b/src/core/sliding-window/__tests__/sliding-window.spec.ts @@ -1103,9 +1103,9 @@ describe("Sliding Window", () => { expect(result2.prevContextTokens).toBe(50001) }) - it("should use 20% of context window as buffer when maxTokens is undefined", async () => { + it("should use ANTHROPIC_DEFAULT_MAX_TOKENS as buffer when maxTokens is undefined", async () => { const modelInfo = createModelInfo(100000, undefined) - // Max tokens = 100000 - (100000 * 0.2) = 80000 + // Max tokens = 100000 - ANTHROPIC_DEFAULT_MAX_TOKENS = 100000 - 8192 = 91808 // Create messages with very small content in the last one to avoid token overflow const messagesWithSmallContent = [ @@ -1117,7 +1117,7 @@ describe("Sliding Window", () => { // Below max tokens and buffer - no truncation const result1 = await truncateConversationIfNeeded({ messages: messagesWithSmallContent, - totalTokens: 69999, // Well below threshold + dynamic buffer + totalTokens: 81807, // Well below threshold + dynamic buffer (91808 - 10000 = 81808) contextWindow: modelInfo.contextWindow, maxTokens: modelInfo.maxTokens, apiHandler: mockApiHandler, @@ -1132,13 +1132,13 @@ describe("Sliding Window", () => { messages: messagesWithSmallContent, summary: "", cost: 0, - prevContextTokens: 69999, + prevContextTokens: 81807, }) // Above max tokens - truncate const result2 = await truncateConversationIfNeeded({ messages: messagesWithSmallContent, - totalTokens: 80001, // Above threshold + totalTokens: 81809, // Above threshold (81808) contextWindow: modelInfo.contextWindow, maxTokens: modelInfo.maxTokens, apiHandler: mockApiHandler, @@ -1153,7 +1153,7 @@ describe("Sliding Window", () => { expect(result2.messages.length).toBe(3) // Truncated with 0.5 fraction expect(result2.summary).toBe("") expect(result2.cost).toBe(0) - expect(result2.prevContextTokens).toBe(80001) + expect(result2.prevContextTokens).toBe(81809) }) it("should handle small context windows appropriately", async () => { diff --git a/src/core/sliding-window/index.ts b/src/core/sliding-window/index.ts index ae26f51a52..1e518c9a56 100644 --- a/src/core/sliding-window/index.ts +++ b/src/core/sliding-window/index.ts @@ -5,6 +5,7 @@ import { TelemetryService } from "@roo-code/telemetry" import { ApiHandler } from "../../api" import { MAX_CONDENSE_THRESHOLD, MIN_CONDENSE_THRESHOLD, summarizeConversation, SummarizeResponse } from "../condense" import { ApiMessage } from "../task-persistence/apiMessages" +import { ANTHROPIC_DEFAULT_MAX_TOKENS } from "@roo-code/types" /** * Default percentage of the context window to use as a buffer when deciding when to truncate @@ -105,7 +106,7 @@ export async function truncateConversationIfNeeded({ let error: string | undefined let cost = 0 // Calculate the maximum tokens reserved for response - const reservedTokens = maxTokens || contextWindow * 0.2 + const reservedTokens = maxTokens || ANTHROPIC_DEFAULT_MAX_TOKENS // Estimate tokens for the last message (which is always a user message) const lastMessage = messages[messages.length - 1] From 7e34fbcb0c1a808745b3ac0a2d933b55e5979e1c Mon Sep 17 00:00:00 2001 From: Daniel <57051444+daniel-lxs@users.noreply.github.com> Date: Mon, 21 Jul 2025 21:27:20 -0500 Subject: [PATCH 3/6] fix: add bedrock to ANTHROPIC_STYLE_PROVIDERS and restore vertex Claude model checking (#6019) --- .../src/__tests__/provider-settings.test.ts | 34 ++++++------------- packages/types/src/provider-settings.ts | 11 ++---- 2 files changed, 14 insertions(+), 31 deletions(-) diff --git a/packages/types/src/__tests__/provider-settings.test.ts b/packages/types/src/__tests__/provider-settings.test.ts index 87c5bbcc1c..8277320289 100644 --- a/packages/types/src/__tests__/provider-settings.test.ts +++ b/packages/types/src/__tests__/provider-settings.test.ts @@ -12,6 +12,12 @@ describe("getApiProtocol", () => { expect(getApiProtocol("claude-code")).toBe("anthropic") expect(getApiProtocol("claude-code", "some-model")).toBe("anthropic") }) + + it("should return 'anthropic' for bedrock provider", () => { + expect(getApiProtocol("bedrock")).toBe("anthropic") + expect(getApiProtocol("bedrock", "gpt-4")).toBe("anthropic") + expect(getApiProtocol("bedrock", "claude-3-opus")).toBe("anthropic") + }) }) describe("Vertex provider with Claude models", () => { @@ -27,25 +33,14 @@ describe("getApiProtocol", () => { expect(getApiProtocol("vertex", "gemini-pro")).toBe("openai") expect(getApiProtocol("vertex", "llama-2")).toBe("openai") }) - }) - describe("Bedrock provider with Claude models", () => { - it("should return 'anthropic' for bedrock provider with claude models", () => { - expect(getApiProtocol("bedrock", "claude-3-opus")).toBe("anthropic") - expect(getApiProtocol("bedrock", "Claude-3-Sonnet")).toBe("anthropic") - expect(getApiProtocol("bedrock", "CLAUDE-instant")).toBe("anthropic") - expect(getApiProtocol("bedrock", "anthropic.claude-v2")).toBe("anthropic") - }) - - it("should return 'openai' for bedrock provider with non-claude models", () => { - expect(getApiProtocol("bedrock", "gpt-4")).toBe("openai") - expect(getApiProtocol("bedrock", "titan-text")).toBe("openai") - expect(getApiProtocol("bedrock", "llama-2")).toBe("openai") + it("should return 'openai' for vertex provider without model", () => { + expect(getApiProtocol("vertex")).toBe("openai") }) }) - describe("Other providers with Claude models", () => { - it("should return 'openai' for non-vertex/bedrock providers with claude models", () => { + describe("Other providers", () => { + it("should return 'openai' for non-anthropic providers regardless of model", () => { expect(getApiProtocol("openrouter", "claude-3-opus")).toBe("openai") expect(getApiProtocol("openai", "claude-3-sonnet")).toBe("openai") expect(getApiProtocol("litellm", "claude-instant")).toBe("openai") @@ -59,20 +54,13 @@ describe("getApiProtocol", () => { expect(getApiProtocol(undefined, "claude-3-opus")).toBe("openai") }) - it("should return 'openai' when model is undefined", () => { - expect(getApiProtocol("openai")).toBe("openai") - expect(getApiProtocol("vertex")).toBe("openai") - expect(getApiProtocol("bedrock")).toBe("openai") - }) - it("should handle empty strings", () => { expect(getApiProtocol("vertex", "")).toBe("openai") - expect(getApiProtocol("bedrock", "")).toBe("openai") }) it("should be case-insensitive for claude detection", () => { expect(getApiProtocol("vertex", "CLAUDE-3-OPUS")).toBe("anthropic") - expect(getApiProtocol("bedrock", "claude-3-opus")).toBe("anthropic") + expect(getApiProtocol("vertex", "claude-3-opus")).toBe("anthropic") expect(getApiProtocol("vertex", "ClAuDe-InStAnT")).toBe("anthropic") }) }) diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index be74ae6bb4..511e803cbd 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -301,7 +301,7 @@ export const getModelId = (settings: ProviderSettings): string | undefined => { } // Providers that use Anthropic-style API protocol -export const ANTHROPIC_STYLE_PROVIDERS: ProviderName[] = ["anthropic", "claude-code"] +export const ANTHROPIC_STYLE_PROVIDERS: ProviderName[] = ["anthropic", "claude-code", "bedrock"] // Helper function to determine API protocol for a provider and model export const getApiProtocol = (provider: ProviderName | undefined, modelId?: string): "anthropic" | "openai" => { @@ -310,13 +310,8 @@ export const getApiProtocol = (provider: ProviderName | undefined, modelId?: str return "anthropic" } - // For vertex and bedrock providers, check if the model ID contains "claude" (case-insensitive) - if ( - provider && - (provider === "vertex" || provider === "bedrock") && - modelId && - modelId.toLowerCase().includes("claude") - ) { + // For vertex provider, check if the model ID contains "claude" (case-insensitive) + if (provider && provider === "vertex" && modelId && modelId.toLowerCase().includes("claude")) { return "anthropic" } From b1bc085aa610be19265868bc1657e193884bcd3c Mon Sep 17 00:00:00 2001 From: "roomote[bot]" <219738659+roomote[bot]@users.noreply.github.com> Date: Mon, 21 Jul 2025 23:27:28 -0400 Subject: [PATCH 4/6] Add todo list tool enable checkbox to provider advanced settings (#6032) Co-authored-by: Roo Code Co-authored-by: Daniel Riccio Co-authored-by: Matt Rubens --- packages/types/src/provider-settings.ts | 1 + src/core/config/ProviderSettingsManager.ts | 21 +++++ .../__tests__/ProviderSettingsManager.spec.ts | 43 +++++++++ .../architect-mode-prompt.snap | 2 + .../mcp-server-creation-disabled.snap | 2 + .../mcp-server-creation-enabled.snap | 2 + .../partial-reads-enabled.snap | 2 + .../consistent-system-prompt.snap | 2 + .../with-computer-use-support.snap | 2 + .../with-diff-enabled-false.snap | 2 + .../system-prompt/with-diff-enabled-true.snap | 2 + .../with-diff-enabled-undefined.snap | 2 + .../with-different-viewport-size.snap | 2 + .../system-prompt/with-mcp-hub-provided.snap | 2 + .../system-prompt/with-undefined-mcp-hub.snap | 2 + .../prompts/__tests__/system-prompt.spec.ts | 90 ++++++++++++++++++- .../prompts/sections/custom-instructions.ts | 2 +- src/core/prompts/system.ts | 4 +- src/core/prompts/tools/index.ts | 5 ++ src/shared/modes.ts | 2 +- .../src/components/settings/ApiOptions.tsx | 5 ++ .../settings/TodoListSettingsControl.tsx | 35 ++++++++ .../settings/__tests__/ApiOptions.spec.tsx | 26 ++++++ .../TodoListSettingsControl.spec.tsx | 77 ++++++++++++++++ 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 + 42 files changed, 402 insertions(+), 5 deletions(-) create mode 100644 webview-ui/src/components/settings/TodoListSettingsControl.tsx create mode 100644 webview-ui/src/components/settings/__tests__/TodoListSettingsControl.spec.tsx diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 511e803cbd..7df1b27db7 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -61,6 +61,7 @@ export const DEFAULT_CONSECUTIVE_MISTAKE_LIMIT = 3 const baseProviderSettingsSchema = z.object({ includeMaxTokens: z.boolean().optional(), diffEnabled: z.boolean().optional(), + todoListEnabled: z.boolean().optional(), fuzzyMatchThreshold: z.number().optional(), modelTemperature: z.number().nullish(), rateLimitSeconds: z.number().optional(), diff --git a/src/core/config/ProviderSettingsManager.ts b/src/core/config/ProviderSettingsManager.ts index 7823a3040a..350c8136f2 100644 --- a/src/core/config/ProviderSettingsManager.ts +++ b/src/core/config/ProviderSettingsManager.ts @@ -28,6 +28,7 @@ export const providerProfilesSchema = z.object({ diffSettingsMigrated: z.boolean().optional(), openAiHeadersMigrated: z.boolean().optional(), consecutiveMistakeLimitMigrated: z.boolean().optional(), + todoListEnabledMigrated: z.boolean().optional(), }) .optional(), }) @@ -51,6 +52,7 @@ export class ProviderSettingsManager { diffSettingsMigrated: true, // Mark as migrated on fresh installs openAiHeadersMigrated: true, // Mark as migrated on fresh installs consecutiveMistakeLimitMigrated: true, // Mark as migrated on fresh installs + todoListEnabledMigrated: true, // Mark as migrated on fresh installs }, } @@ -117,6 +119,7 @@ export class ProviderSettingsManager { diffSettingsMigrated: false, openAiHeadersMigrated: false, consecutiveMistakeLimitMigrated: false, + todoListEnabledMigrated: false, } // Initialize with default values isDirty = true } @@ -145,6 +148,12 @@ export class ProviderSettingsManager { isDirty = true } + if (!providerProfiles.migrations.todoListEnabledMigrated) { + await this.migrateTodoListEnabled(providerProfiles) + providerProfiles.migrations.todoListEnabledMigrated = true + isDirty = true + } + if (isDirty) { await this.store(providerProfiles) } @@ -250,6 +259,18 @@ export class ProviderSettingsManager { } } + private async migrateTodoListEnabled(providerProfiles: ProviderProfiles) { + try { + for (const [_name, apiConfig] of Object.entries(providerProfiles.apiConfigs)) { + if (apiConfig.todoListEnabled === undefined) { + apiConfig.todoListEnabled = true + } + } + } catch (error) { + console.error(`[MigrateTodoListEnabled] Failed to migrate todo list enabled setting:`, error) + } + } + /** * List all available configs with metadata. */ diff --git a/src/core/config/__tests__/ProviderSettingsManager.spec.ts b/src/core/config/__tests__/ProviderSettingsManager.spec.ts index 6d24c63101..e52c1974b6 100644 --- a/src/core/config/__tests__/ProviderSettingsManager.spec.ts +++ b/src/core/config/__tests__/ProviderSettingsManager.spec.ts @@ -67,6 +67,7 @@ describe("ProviderSettingsManager", () => { diffSettingsMigrated: true, openAiHeadersMigrated: true, consecutiveMistakeLimitMigrated: true, + todoListEnabledMigrated: true, }, }), ) @@ -186,6 +187,48 @@ describe("ProviderSettingsManager", () => { expect(storedConfig.migrations.consecutiveMistakeLimitMigrated).toEqual(true) }) + it("should call migrateTodoListEnabled if it has not done so already", async () => { + mockSecrets.get.mockResolvedValue( + JSON.stringify({ + currentApiConfigName: "default", + apiConfigs: { + default: { + config: {}, + id: "default", + todoListEnabled: undefined, + }, + test: { + apiProvider: "anthropic", + todoListEnabled: undefined, + }, + existing: { + apiProvider: "anthropic", + // this should not really be possible, unless someone has loaded a hand edited config, + // but we don't overwrite so we'll check that + todoListEnabled: false, + }, + }, + migrations: { + rateLimitSecondsMigrated: true, + diffSettingsMigrated: true, + openAiHeadersMigrated: true, + consecutiveMistakeLimitMigrated: true, + todoListEnabledMigrated: false, + }, + }), + ) + + await providerSettingsManager.initialize() + + // Get the last call to store, which should contain the migrated config + const calls = mockSecrets.store.mock.calls + const storedConfig = JSON.parse(calls[calls.length - 1][1]) + expect(storedConfig.apiConfigs.default.todoListEnabled).toEqual(true) + expect(storedConfig.apiConfigs.test.todoListEnabled).toEqual(true) + expect(storedConfig.apiConfigs.existing.todoListEnabled).toEqual(false) + expect(storedConfig.migrations.todoListEnabledMigrated).toEqual(true) + }) + it("should throw error if secrets storage fails", async () => { mockSecrets.get.mockRejectedValue(new Error("Storage failed")) diff --git a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/architect-mode-prompt.snap b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/architect-mode-prompt.snap index e80857d354..273e43d20b 100644 --- a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/architect-mode-prompt.snap +++ b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/architect-mode-prompt.snap @@ -555,6 +555,8 @@ Mode-specific Instructions: - Focused on a single, well-defined outcome - Clear enough that another mode could execute it independently + **Note:** If the `update_todo_list` tool is not available, write the plan to a markdown file (e.g., `plan.md` or `todo.md`) instead. + 4. As you gather more information or discover new requirements, update the todo list to reflect the current understanding of what needs to be accomplished. 5. Ask the user if they are pleased with this plan, or if they would like to make any changes. Think of this as a brainstorming session where you can discuss the task and refine the todo list. diff --git a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-disabled.snap b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-disabled.snap index e80857d354..273e43d20b 100644 --- a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-disabled.snap +++ b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-disabled.snap @@ -555,6 +555,8 @@ Mode-specific Instructions: - Focused on a single, well-defined outcome - Clear enough that another mode could execute it independently + **Note:** If the `update_todo_list` tool is not available, write the plan to a markdown file (e.g., `plan.md` or `todo.md`) instead. + 4. As you gather more information or discover new requirements, update the todo list to reflect the current understanding of what needs to be accomplished. 5. Ask the user if they are pleased with this plan, or if they would like to make any changes. Think of this as a brainstorming session where you can discuss the task and refine the todo list. diff --git a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-enabled.snap b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-enabled.snap index 4dce0f264a..8e1b90a1cf 100644 --- a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-enabled.snap +++ b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-enabled.snap @@ -623,6 +623,8 @@ Mode-specific Instructions: - Focused on a single, well-defined outcome - Clear enough that another mode could execute it independently + **Note:** If the `update_todo_list` tool is not available, write the plan to a markdown file (e.g., `plan.md` or `todo.md`) instead. + 4. As you gather more information or discover new requirements, update the todo list to reflect the current understanding of what needs to be accomplished. 5. Ask the user if they are pleased with this plan, or if they would like to make any changes. Think of this as a brainstorming session where you can discuss the task and refine the todo list. diff --git a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/partial-reads-enabled.snap b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/partial-reads-enabled.snap index 0d172d756b..7ee1ab207f 100644 --- a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/partial-reads-enabled.snap +++ b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/partial-reads-enabled.snap @@ -560,6 +560,8 @@ Mode-specific Instructions: - Focused on a single, well-defined outcome - Clear enough that another mode could execute it independently + **Note:** If the `update_todo_list` tool is not available, write the plan to a markdown file (e.g., `plan.md` or `todo.md`) instead. + 4. As you gather more information or discover new requirements, update the todo list to reflect the current understanding of what needs to be accomplished. 5. Ask the user if they are pleased with this plan, or if they would like to make any changes. Think of this as a brainstorming session where you can discuss the task and refine the todo list. diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/consistent-system-prompt.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/consistent-system-prompt.snap index e80857d354..273e43d20b 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/consistent-system-prompt.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/consistent-system-prompt.snap @@ -555,6 +555,8 @@ Mode-specific Instructions: - Focused on a single, well-defined outcome - Clear enough that another mode could execute it independently + **Note:** If the `update_todo_list` tool is not available, write the plan to a markdown file (e.g., `plan.md` or `todo.md`) instead. + 4. As you gather more information or discover new requirements, update the todo list to reflect the current understanding of what needs to be accomplished. 5. Ask the user if they are pleased with this plan, or if they would like to make any changes. Think of this as a brainstorming session where you can discuss the task and refine the todo list. diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-computer-use-support.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-computer-use-support.snap index 576bf0ddd5..e23dc220b4 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-computer-use-support.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-computer-use-support.snap @@ -611,6 +611,8 @@ Mode-specific Instructions: - Focused on a single, well-defined outcome - Clear enough that another mode could execute it independently + **Note:** If the `update_todo_list` tool is not available, write the plan to a markdown file (e.g., `plan.md` or `todo.md`) instead. + 4. As you gather more information or discover new requirements, update the todo list to reflect the current understanding of what needs to be accomplished. 5. Ask the user if they are pleased with this plan, or if they would like to make any changes. Think of this as a brainstorming session where you can discuss the task and refine the todo list. diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-false.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-false.snap index e80857d354..273e43d20b 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-false.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-false.snap @@ -555,6 +555,8 @@ Mode-specific Instructions: - Focused on a single, well-defined outcome - Clear enough that another mode could execute it independently + **Note:** If the `update_todo_list` tool is not available, write the plan to a markdown file (e.g., `plan.md` or `todo.md`) instead. + 4. As you gather more information or discover new requirements, update the todo list to reflect the current understanding of what needs to be accomplished. 5. Ask the user if they are pleased with this plan, or if they would like to make any changes. Think of this as a brainstorming session where you can discuss the task and refine the todo list. diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-true.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-true.snap index 6fc7ad69ce..ad90a58850 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-true.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-true.snap @@ -643,6 +643,8 @@ Mode-specific Instructions: - Focused on a single, well-defined outcome - Clear enough that another mode could execute it independently + **Note:** If the `update_todo_list` tool is not available, write the plan to a markdown file (e.g., `plan.md` or `todo.md`) instead. + 4. As you gather more information or discover new requirements, update the todo list to reflect the current understanding of what needs to be accomplished. 5. Ask the user if they are pleased with this plan, or if they would like to make any changes. Think of this as a brainstorming session where you can discuss the task and refine the todo list. diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-undefined.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-undefined.snap index e80857d354..273e43d20b 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-undefined.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-undefined.snap @@ -555,6 +555,8 @@ Mode-specific Instructions: - Focused on a single, well-defined outcome - Clear enough that another mode could execute it independently + **Note:** If the `update_todo_list` tool is not available, write the plan to a markdown file (e.g., `plan.md` or `todo.md`) instead. + 4. As you gather more information or discover new requirements, update the todo list to reflect the current understanding of what needs to be accomplished. 5. Ask the user if they are pleased with this plan, or if they would like to make any changes. Think of this as a brainstorming session where you can discuss the task and refine the todo list. diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-different-viewport-size.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-different-viewport-size.snap index 6ccccedefe..005813848f 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-different-viewport-size.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-different-viewport-size.snap @@ -611,6 +611,8 @@ Mode-specific Instructions: - Focused on a single, well-defined outcome - Clear enough that another mode could execute it independently + **Note:** If the `update_todo_list` tool is not available, write the plan to a markdown file (e.g., `plan.md` or `todo.md`) instead. + 4. As you gather more information or discover new requirements, update the todo list to reflect the current understanding of what needs to be accomplished. 5. Ask the user if they are pleased with this plan, or if they would like to make any changes. Think of this as a brainstorming session where you can discuss the task and refine the todo list. diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-mcp-hub-provided.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-mcp-hub-provided.snap index 4dce0f264a..8e1b90a1cf 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-mcp-hub-provided.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-mcp-hub-provided.snap @@ -623,6 +623,8 @@ Mode-specific Instructions: - Focused on a single, well-defined outcome - Clear enough that another mode could execute it independently + **Note:** If the `update_todo_list` tool is not available, write the plan to a markdown file (e.g., `plan.md` or `todo.md`) instead. + 4. As you gather more information or discover new requirements, update the todo list to reflect the current understanding of what needs to be accomplished. 5. Ask the user if they are pleased with this plan, or if they would like to make any changes. Think of this as a brainstorming session where you can discuss the task and refine the todo list. diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-undefined-mcp-hub.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-undefined-mcp-hub.snap index e80857d354..273e43d20b 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-undefined-mcp-hub.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-undefined-mcp-hub.snap @@ -555,6 +555,8 @@ Mode-specific Instructions: - Focused on a single, well-defined outcome - Clear enough that another mode could execute it independently + **Note:** If the `update_todo_list` tool is not available, write the plan to a markdown file (e.g., `plan.md` or `todo.md`) instead. + 4. As you gather more information or discover new requirements, update the todo list to reflect the current understanding of what needs to be accomplished. 5. Ask the user if they are pleased with this plan, or if they would like to make any changes. Think of this as a brainstorming session where you can discuss the task and refine the todo list. diff --git a/src/core/prompts/__tests__/system-prompt.spec.ts b/src/core/prompts/__tests__/system-prompt.spec.ts index 175f3ba265..7589790efe 100644 --- a/src/core/prompts/__tests__/system-prompt.spec.ts +++ b/src/core/prompts/__tests__/system-prompt.spec.ts @@ -79,7 +79,7 @@ __setMockImplementation( globalCustomInstructions: string, cwd: string, mode: string, - options?: { language?: string }, + options?: { language?: string; rooIgnoreInstructions?: string; settings?: Record }, ) => { const sections = [] @@ -575,6 +575,94 @@ describe("SYSTEM_PROMPT", () => { expect(prompt.indexOf(modes[0].roleDefinition)).toBeLessThan(prompt.indexOf("TOOL USE")) }) + it("should exclude update_todo_list tool when todoListEnabled is false", async () => { + const settings = { + todoListEnabled: false, + } + + const prompt = await SYSTEM_PROMPT( + mockContext, + "/test/path", + false, // supportsComputerUse + undefined, // mcpHub + undefined, // diffStrategy + undefined, // browserViewportSize + defaultModeSlug, // mode + undefined, // customModePrompts + undefined, // customModes + undefined, // globalCustomInstructions + undefined, // diffEnabled + experiments, + true, // enableMcpServerCreation + undefined, // language + undefined, // rooIgnoreInstructions + undefined, // partialReadsEnabled + settings, // settings + ) + + // Should not contain the tool description + expect(prompt).not.toContain("## update_todo_list") + // Mode instructions will still reference the tool with a fallback to markdown + }) + + it("should include update_todo_list tool when todoListEnabled is true", async () => { + const settings = { + todoListEnabled: true, + } + + const prompt = await SYSTEM_PROMPT( + mockContext, + "/test/path", + false, // supportsComputerUse + undefined, // mcpHub + undefined, // diffStrategy + undefined, // browserViewportSize + defaultModeSlug, // mode + undefined, // customModePrompts + undefined, // customModes + undefined, // globalCustomInstructions + undefined, // diffEnabled + experiments, + true, // enableMcpServerCreation + undefined, // language + undefined, // rooIgnoreInstructions + undefined, // partialReadsEnabled + settings, // settings + ) + + expect(prompt).toContain("update_todo_list") + expect(prompt).toContain("## update_todo_list") + }) + + it("should include update_todo_list tool when todoListEnabled is undefined", async () => { + const settings = { + // todoListEnabled not set + } + + const prompt = await SYSTEM_PROMPT( + mockContext, + "/test/path", + false, // supportsComputerUse + undefined, // mcpHub + undefined, // diffStrategy + undefined, // browserViewportSize + defaultModeSlug, // mode + undefined, // customModePrompts + undefined, // customModes + undefined, // globalCustomInstructions + undefined, // diffEnabled + experiments, + true, // enableMcpServerCreation + undefined, // language + undefined, // rooIgnoreInstructions + undefined, // partialReadsEnabled + settings, // settings + ) + + expect(prompt).toContain("update_todo_list") + expect(prompt).toContain("## update_todo_list") + }) + afterAll(() => { vi.restoreAllMocks() }) diff --git a/src/core/prompts/sections/custom-instructions.ts b/src/core/prompts/sections/custom-instructions.ts index 71613ff94a..a78882cc90 100644 --- a/src/core/prompts/sections/custom-instructions.ts +++ b/src/core/prompts/sections/custom-instructions.ts @@ -219,7 +219,7 @@ export async function addCustomInstructions( globalCustomInstructions: string, cwd: string, mode: string, - options: { language?: string; rooIgnoreInstructions?: string } = {}, + options: { language?: string; rooIgnoreInstructions?: string; settings?: Record } = {}, ): Promise { const sections = [] diff --git a/src/core/prompts/system.ts b/src/core/prompts/system.ts index 92653cafd2..ea4f43823e 100644 --- a/src/core/prompts/system.ts +++ b/src/core/prompts/system.ts @@ -119,7 +119,7 @@ ${getSystemInfoSection(cwd)} ${getObjectiveSection(codeIndexManager, experiments)} -${await addCustomInstructions(baseInstructions, globalCustomInstructions || "", cwd, mode, { language: language ?? formatLanguage(vscode.env.language), rooIgnoreInstructions })}` +${await addCustomInstructions(baseInstructions, globalCustomInstructions || "", cwd, mode, { language: language ?? formatLanguage(vscode.env.language), rooIgnoreInstructions, settings })}` return basePrompt } @@ -177,7 +177,7 @@ export const SYSTEM_PROMPT = async ( globalCustomInstructions || "", cwd, mode, - { language: language ?? formatLanguage(vscode.env.language), rooIgnoreInstructions }, + { language: language ?? formatLanguage(vscode.env.language), rooIgnoreInstructions, settings }, ) // For file-based prompts, don't include the tool sections diff --git a/src/core/prompts/tools/index.ts b/src/core/prompts/tools/index.ts index 3fd5a636a4..9f4af7f312 100644 --- a/src/core/prompts/tools/index.ts +++ b/src/core/prompts/tools/index.ts @@ -109,6 +109,11 @@ export function getToolDescriptionsForMode( tools.delete("codebase_search") } + // Conditionally exclude update_todo_list if disabled in settings + if (settings?.todoListEnabled === false) { + tools.delete("update_todo_list") + } + // Map tool descriptions for allowed tools const descriptions = Array.from(tools).map((toolName) => { const descriptionFn = toolDescriptionMap[toolName] diff --git a/src/shared/modes.ts b/src/shared/modes.ts index 9c790236eb..168f4ff867 100644 --- a/src/shared/modes.ts +++ b/src/shared/modes.ts @@ -72,7 +72,7 @@ export const modes: readonly ModeConfig[] = [ description: "Plan and design before implementation", groups: ["read", ["edit", { fileRegex: "\\.md$", description: "Markdown files only" }], "browser", "mcp"], customInstructions: - "1. Do some information gathering (using provided tools) to get more context about the task.\n\n2. You should also ask the user clarifying questions to get a better understanding of the task.\n\n3. Once you've gained more context about the user's request, break down the task into clear, actionable steps and create a todo list using the `update_todo_list` tool. Each todo item should be:\n - Specific and actionable\n - Listed in logical execution order\n - Focused on a single, well-defined outcome\n - Clear enough that another mode could execute it independently\n\n4. As you gather more information or discover new requirements, update the todo list to reflect the current understanding of what needs to be accomplished.\n\n5. Ask the user if they are pleased with this plan, or if they would like to make any changes. Think of this as a brainstorming session where you can discuss the task and refine the todo list.\n\n6. Include Mermaid diagrams if they help clarify complex workflows or system architecture. Please avoid using double quotes (\"\") and parentheses () inside square brackets ([]) in Mermaid diagrams, as this can cause parsing errors.\n\n7. Use the switch_mode tool to request that the user switch to another mode to implement the solution.\n\n**IMPORTANT: Focus on creating clear, actionable todo lists rather than lengthy markdown documents. Use the todo list as your primary planning tool to track and organize the work that needs to be done.**", + "1. Do some information gathering (using provided tools) to get more context about the task.\n\n2. You should also ask the user clarifying questions to get a better understanding of the task.\n\n3. Once you've gained more context about the user's request, break down the task into clear, actionable steps and create a todo list using the `update_todo_list` tool. Each todo item should be:\n - Specific and actionable\n - Listed in logical execution order\n - Focused on a single, well-defined outcome\n - Clear enough that another mode could execute it independently\n\n **Note:** If the `update_todo_list` tool is not available, write the plan to a markdown file (e.g., `plan.md` or `todo.md`) instead.\n\n4. As you gather more information or discover new requirements, update the todo list to reflect the current understanding of what needs to be accomplished.\n\n5. Ask the user if they are pleased with this plan, or if they would like to make any changes. Think of this as a brainstorming session where you can discuss the task and refine the todo list.\n\n6. Include Mermaid diagrams if they help clarify complex workflows or system architecture. Please avoid using double quotes (\"\") and parentheses () inside square brackets ([]) in Mermaid diagrams, as this can cause parsing errors.\n\n7. Use the switch_mode tool to request that the user switch to another mode to implement the solution.\n\n**IMPORTANT: Focus on creating clear, actionable todo lists rather than lengthy markdown documents. Use the todo list as your primary planning tool to track and organize the work that needs to be done.**", }, { slug: "code", diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 06994b16b9..681268a787 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -78,6 +78,7 @@ import { ModelInfoView } from "./ModelInfoView" import { ApiErrorMessage } from "./ApiErrorMessage" import { ThinkingBudget } from "./ThinkingBudget" import { DiffSettingsControl } from "./DiffSettingsControl" +import { TodoListSettingsControl } from "./TodoListSettingsControl" import { TemperatureControl } from "./TemperatureControl" import { RateLimitSecondsControl } from "./RateLimitSecondsControl" import { ConsecutiveMistakeLimitControl } from "./ConsecutiveMistakeLimitControl" @@ -564,6 +565,10 @@ const ApiOptions = ({ {t("settings:advancedSettings.title")} + setApiConfigurationField(field, value)} + /> void +} + +export const TodoListSettingsControl: React.FC = ({ + todoListEnabled = true, + onChange, +}) => { + const { t } = useAppTranslation() + + const handleTodoListEnabledChange = useCallback( + (e: any) => { + onChange("todoListEnabled", e.target.checked) + }, + [onChange], + ) + + return ( +
+
+ + {t("settings:advanced.todoList.label")} + +
+ {t("settings:advanced.todoList.description")} +
+
+
+ ) +} diff --git a/webview-ui/src/components/settings/__tests__/ApiOptions.spec.tsx b/webview-ui/src/components/settings/__tests__/ApiOptions.spec.tsx index bf02840429..7b7f9b33e4 100644 --- a/webview-ui/src/components/settings/__tests__/ApiOptions.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/ApiOptions.spec.tsx @@ -21,6 +21,16 @@ vi.mock("@vscode/webview-ui-toolkit/react", () => ({ VSCodeRadio: ({ value, checked }: any) => , VSCodeRadioGroup: ({ children }: any) =>
{children}
, VSCodeButton: ({ children }: any) =>
{children}
, + VSCodeCheckbox: ({ children, checked, onChange }: any) => ( + + ), })) // Mock other components @@ -173,6 +183,22 @@ vi.mock("../DiffSettingsControl", () => ({ ), })) +// Mock TodoListSettingsControl for tests +vi.mock("../TodoListSettingsControl", () => ({ + TodoListSettingsControl: ({ todoListEnabled, onChange }: any) => ( +
+ +
+ ), +})) + // Mock ThinkingBudget component vi.mock("../ThinkingBudget", () => ({ ThinkingBudget: ({ modelInfo }: any) => { diff --git a/webview-ui/src/components/settings/__tests__/TodoListSettingsControl.spec.tsx b/webview-ui/src/components/settings/__tests__/TodoListSettingsControl.spec.tsx new file mode 100644 index 0000000000..432b2c9c61 --- /dev/null +++ b/webview-ui/src/components/settings/__tests__/TodoListSettingsControl.spec.tsx @@ -0,0 +1,77 @@ +import React from "react" +import { render, screen, fireEvent } from "@testing-library/react" +import { describe, it, expect, vi } from "vitest" +import { TodoListSettingsControl } from "../TodoListSettingsControl" + +// Mock the translation hook +vi.mock("@/i18n/TranslationContext", () => ({ + useAppTranslation: () => ({ + t: (key: string) => { + const translations: Record = { + "settings:advanced.todoList.label": "Enable todo list tool", + "settings:advanced.todoList.description": + "When enabled, Roo can create and manage todo lists to track task progress. This helps organize complex tasks into manageable steps.", + } + return translations[key] || key + }, + }), +})) + +// Mock VSCodeCheckbox +vi.mock("@vscode/webview-ui-toolkit/react", () => ({ + VSCodeCheckbox: ({ children, onChange, checked, ...props }: any) => ( + + ), +})) + +describe("TodoListSettingsControl", () => { + it("renders with default props", () => { + const onChange = vi.fn() + render() + + const checkbox = screen.getByRole("checkbox") + const label = screen.getByText("Enable todo list tool") + const description = screen.getByText(/When enabled, Roo can create and manage todo lists/) + + expect(checkbox).toBeInTheDocument() + expect(checkbox).toBeChecked() // Default is true + expect(label).toBeInTheDocument() + expect(description).toBeInTheDocument() + }) + + it("renders with todoListEnabled set to false", () => { + const onChange = vi.fn() + render() + + const checkbox = screen.getByRole("checkbox") + expect(checkbox).not.toBeChecked() + }) + + it("calls onChange when checkbox is clicked", () => { + const onChange = vi.fn() + render() + + const checkbox = screen.getByRole("checkbox") + fireEvent.click(checkbox) + + expect(onChange).toHaveBeenCalledWith("todoListEnabled", false) + }) + + it("toggles from unchecked to checked", () => { + const onChange = vi.fn() + render() + + const checkbox = screen.getByRole("checkbox") + fireEvent.click(checkbox) + + expect(onChange).toHaveBeenCalledWith("todoListEnabled", true) + }) +}) diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index eaa83b1b0d..8f270ee4ce 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -577,6 +577,10 @@ "label": "Precisió de coincidència", "description": "Aquest control lliscant controla amb quina precisió han de coincidir les seccions de codi en aplicar diffs. Valors més baixos permeten coincidències més flexibles però augmenten el risc de reemplaçaments incorrectes. Utilitzeu valors per sota del 100% amb extrema precaució." } + }, + "todoList": { + "label": "Habilitar eina de llista de tasques", + "description": "Quan està habilitat, Roo pot crear i gestionar llistes de tasques per fer el seguiment del progrés de les tasques. Això ajuda a organitzar tasques complexes en passos manejables." } }, "experimental": { diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index 7f7c2c22b7..73a4ede3d8 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -577,6 +577,10 @@ "label": "Übereinstimmungspräzision", "description": "Dieser Schieberegler steuert, wie genau Codeabschnitte bei der Anwendung von Diffs übereinstimmen müssen. Niedrigere Werte ermöglichen eine flexiblere Übereinstimmung, erhöhen aber das Risiko falscher Ersetzungen. Verwenden Sie Werte unter 100 % mit äußerster Vorsicht." } + }, + "todoList": { + "label": "Todo-Listen-Tool aktivieren", + "description": "Wenn aktiviert, kann Roo Todo-Listen erstellen und verwalten, um den Aufgabenfortschritt zu verfolgen. Dies hilft, komplexe Aufgaben in überschaubare Schritte zu organisieren." } }, "experimental": { diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 7e3c2e3fcc..457b4dbda9 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -577,6 +577,10 @@ "label": "Match precision", "description": "This slider controls how precisely code sections must match when applying diffs. Lower values allow more flexible matching but increase the risk of incorrect replacements. Use values below 100% with extreme caution." } + }, + "todoList": { + "label": "Enable todo list tool", + "description": "When enabled, Roo can create and manage todo lists to track task progress. This helps organize complex tasks into manageable steps." } }, "experimental": { diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index f00c2c9b42..d217e66226 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -577,6 +577,10 @@ "label": "Precisión de coincidencia", "description": "Este control deslizante controla cuán precisamente deben coincidir las secciones de código al aplicar diffs. Valores más bajos permiten coincidencias más flexibles pero aumentan el riesgo de reemplazos incorrectos. Use valores por debajo del 100% con extrema precaución." } + }, + "todoList": { + "label": "Habilitar herramienta de lista de tareas", + "description": "Cuando está habilitado, Roo puede crear y gestionar listas de tareas para hacer seguimiento del progreso. Esto ayuda a organizar tareas complejas en pasos manejables." } }, "experimental": { diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 02dfac3552..f7f9a2683a 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -577,6 +577,10 @@ "label": "Précision de correspondance", "description": "Ce curseur contrôle la précision avec laquelle les sections de code doivent correspondre lors de l'application des diffs. Des valeurs plus basses permettent des correspondances plus flexibles mais augmentent le risque de remplacements incorrects. Utilisez des valeurs inférieures à 100 % avec une extrême prudence." } + }, + "todoList": { + "label": "Activer l'outil de liste de tâches", + "description": "Lorsqu'activé, Roo peut créer et gérer des listes de tâches pour suivre la progression. Cela aide à organiser les tâches complexes en étapes gérables." } }, "experimental": { diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 2b7fd03d56..6a3679e090 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -577,6 +577,10 @@ "label": "मिलान सटीकता", "description": "यह स्लाइडर नियंत्रित करता है कि diffs लागू करते समय कोड अनुभागों को कितनी सटीकता से मेल खाना चाहिए। निम्न मान अधिक लचीले मिलान की अनुमति देते हैं लेकिन गलत प्रतिस्थापन का जोखिम बढ़ाते हैं। 100% से नीचे के मानों का उपयोग अत्यधिक सावधानी के साथ करें।" } + }, + "todoList": { + "label": "टूडू सूची टूल सक्षम करें", + "description": "जब सक्षम हो, तो Roo कार्य प्रगति को ट्रैक करने के लिए टूडू सूचियाँ बना और प्रबंधित कर सकता है। यह जटिल कार्यों को प्रबंधनीय चरणों में व्यवस्थित करने में मदद करता है।" } }, "experimental": { diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index c6ba2728a7..09d7e8254a 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -581,6 +581,10 @@ "label": "Presisi pencocokan", "description": "Slider ini mengontrol seberapa tepat bagian kode harus cocok saat menerapkan diff. Nilai yang lebih rendah memungkinkan pencocokan yang lebih fleksibel tetapi meningkatkan risiko penggantian yang salah. Gunakan nilai di bawah 100% dengan sangat hati-hati." } + }, + "todoList": { + "label": "Aktifkan alat daftar tugas", + "description": "Saat diaktifkan, Roo dapat membuat dan mengelola daftar tugas untuk melacak kemajuan tugas. Ini membantu mengatur tugas kompleks menjadi langkah-langkah yang dapat dikelola." } }, "experimental": { diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index 26c776d60d..c2836f2c71 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -577,6 +577,10 @@ "label": "Precisione corrispondenza", "description": "Questo cursore controlla quanto precisamente le sezioni di codice devono corrispondere quando si applicano i diff. Valori più bassi consentono corrispondenze più flessibili ma aumentano il rischio di sostituzioni errate. Usa valori inferiori al 100% con estrema cautela." } + }, + "todoList": { + "label": "Abilita strumento lista di cose da fare", + "description": "Quando abilitato, Roo può creare e gestire liste di cose da fare per tracciare il progresso delle attività. Questo aiuta a organizzare attività complesse in passaggi gestibili." } }, "experimental": { diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index 9eb4328c12..01233ef505 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -577,6 +577,10 @@ "label": "マッチ精度", "description": "このスライダーは、diffを適用する際にコードセクションがどれだけ正確に一致する必要があるかを制御します。低い値はより柔軟なマッチングを可能にしますが、誤った置換のリスクが高まります。100%未満の値は細心の注意を払って使用してください。" } + }, + "todoList": { + "label": "ToDoリストツールを有効にする", + "description": "有効にすると、Rooはタスクの進捗を追跡するためのToDoリストを作成・管理できます。これにより、複雑なタスクを管理しやすいステップに整理できます。" } }, "experimental": { diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index 18b054bbb8..806b11f022 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -577,6 +577,10 @@ "label": "일치 정확도", "description": "이 슬라이더는 diff를 적용할 때 코드 섹션이 얼마나 정확하게 일치해야 하는지 제어합니다. 낮은 값은 더 유연한 일치를 허용하지만 잘못된 교체 위험이 증가합니다. 100% 미만의 값은 극도로 주의해서 사용하세요." } + }, + "todoList": { + "label": "할 일 목록 도구 활성화", + "description": "활성화하면 Roo가 작업 진행 상황을 추적하기 위한 할 일 목록을 만들고 관리할 수 있습니다. 이는 복잡한 작업을 관리 가능한 단계로 구성하는 데 도움이 됩니다." } }, "experimental": { diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index 73ecf87d34..28edfdfd52 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -577,6 +577,10 @@ "label": "Matchnauwkeurigheid", "description": "Deze schuifregelaar bepaalt hoe nauwkeurig codeblokken moeten overeenkomen bij het toepassen van diffs. Lagere waarden laten flexibelere matching toe maar verhogen het risico op verkeerde vervangingen. Gebruik waarden onder 100% met uiterste voorzichtigheid." } + }, + "todoList": { + "label": "Takenlijst-tool inschakelen", + "description": "Wanneer ingeschakeld, kan Roo takenlijsten maken en beheren om de voortgang van taken bij te houden. Dit helpt complexe taken te organiseren in beheersbare stappen." } }, "experimental": { diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index b0fb8efac3..d034830c1a 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -577,6 +577,10 @@ "label": "Precyzja dopasowania", "description": "Ten suwak kontroluje, jak dokładnie sekcje kodu muszą pasować podczas stosowania różnic. Niższe wartości umożliwiają bardziej elastyczne dopasowywanie, ale zwiększają ryzyko nieprawidłowych zamian. Używaj wartości poniżej 100% z najwyższą ostrożnością." } + }, + "todoList": { + "label": "Włącz narzędzie listy zadań", + "description": "Po włączeniu Roo może tworzyć i zarządzać listami zadań do śledzenia postępu zadań. Pomaga to organizować złożone zadania w łatwe do zarządzania kroki." } }, "experimental": { diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index 4167ade974..140042c773 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -577,6 +577,10 @@ "label": "Precisão de correspondência", "description": "Este controle deslizante controla quão precisamente as seções de código devem corresponder ao aplicar diffs. Valores mais baixos permitem correspondências mais flexíveis, mas aumentam o risco de substituições incorretas. Use valores abaixo de 100% com extrema cautela." } + }, + "todoList": { + "label": "Habilitar ferramenta de lista de tarefas", + "description": "Quando habilitado, o Roo pode criar e gerenciar listas de tarefas para acompanhar o progresso das tarefas. Isso ajuda a organizar tarefas complexas em etapas gerenciáveis." } }, "experimental": { diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index c07bc1d98a..895655fbdf 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -577,6 +577,10 @@ "label": "Точность совпадения", "description": "Этот ползунок управляет точностью совпадения секций кода при применении диффов. Меньшие значения позволяют более гибкое совпадение, но увеличивают риск неверной замены. Используйте значения ниже 100% с осторожностью." } + }, + "todoList": { + "label": "Включить инструмент списка задач", + "description": "При включении Roo может создавать и управлять списками задач для отслеживания прогресса. Это помогает организовать сложные задачи в управляемые шаги." } }, "experimental": { diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index ae6fad364d..06878be920 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -577,6 +577,10 @@ "label": "Eşleşme hassasiyeti", "description": "Bu kaydırıcı, diff'ler uygulanırken kod bölümlerinin ne kadar hassas bir şekilde eşleşmesi gerektiğini kontrol eder. Daha düşük değerler daha esnek eşleşmeye izin verir ancak yanlış değiştirme riskini artırır. %100'ün altındaki değerleri son derece dikkatli kullanın." } + }, + "todoList": { + "label": "Yapılacaklar listesi aracını etkinleştir", + "description": "Etkinleştirildiğinde, Roo görev ilerlemesini takip etmek için yapılacaklar listeleri oluşturabilir ve yönetebilir. Bu, karmaşık görevleri yönetilebilir adımlara organize etmeye yardımcı olur." } }, "experimental": { diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index 6505d7df0e..c83b7a6642 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -577,6 +577,10 @@ "label": "Độ chính xác khớp", "description": "Thanh trượt này kiểm soát mức độ chính xác các phần mã phải khớp khi áp dụng diff. Giá trị thấp hơn cho phép khớp linh hoạt hơn nhưng tăng nguy cơ thay thế không chính xác. Sử dụng giá trị dưới 100% với sự thận trọng cao." } + }, + "todoList": { + "label": "Bật công cụ danh sách việc cần làm", + "description": "Khi được bật, Roo có thể tạo và quản lý danh sách việc cần làm để theo dõi tiến độ công việc. Điều này giúp tổ chức các tác vụ phức tạp thành các bước có thể quản lý được." } }, "experimental": { diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index bf3eea0294..11c469bf77 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -577,6 +577,10 @@ "label": "匹配精度", "description": "控制代码匹配的精确程度。数值越低匹配越宽松(容错率高但风险大),建议保持100%以确保安全。" } + }, + "todoList": { + "label": "启用任务清单工具", + "description": "启用后,Roo 可以创建和管理任务清单来跟踪任务进度。这有助于将复杂任务组织成可管理的步骤。" } }, "experimental": { diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index e2a896dce4..dab70b88b1 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -577,6 +577,10 @@ "label": "比對精確度", "description": "此滑桿控制套用差異時程式碼區段的比對精確度。較低的數值允許更彈性的比對,但也會增加錯誤取代的風險。使用低於 100% 的數值時請特別謹慎。" } + }, + "todoList": { + "label": "啟用待辦事項清單工具", + "description": "啟用後,Roo 可以建立和管理待辦事項清單來追蹤任務進度。這有助於將複雜任務組織成可管理的步驟。" } }, "experimental": { From df6c57d2930f32f511ad92f3fdbe98df21e5dfda Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Tue, 22 Jul 2025 00:37:24 -0400 Subject: [PATCH 5/6] feat: add moonshot provider (#6046) Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> Co-authored-by: CellenLee <99465814+CellenLee@users.noreply.github.com> --- packages/types/src/global-settings.ts | 1 + packages/types/src/provider-settings.ts | 10 + packages/types/src/providers/index.ts | 1 + packages/types/src/providers/moonshot.ts | 22 ++ src/api/index.ts | 4 + src/api/providers/__tests__/moonshot.spec.ts | 297 ++++++++++++++++++ src/api/providers/index.ts | 1 + src/api/providers/moonshot.ts | 39 +++ .../__tests__/checkExistApiConfig.spec.ts | 1 + .../src/components/settings/ApiOptions.tsx | 7 + .../src/components/settings/constants.ts | 3 + .../settings/providers/Moonshot.tsx | 73 +++++ .../components/settings/providers/index.ts | 1 + .../components/ui/hooks/useSelectedModel.ts | 8 + webview-ui/src/i18n/locales/ca/settings.json | 3 + webview-ui/src/i18n/locales/de/settings.json | 3 + webview-ui/src/i18n/locales/en/settings.json | 3 + webview-ui/src/i18n/locales/es/settings.json | 3 + webview-ui/src/i18n/locales/fr/settings.json | 3 + webview-ui/src/i18n/locales/hi/settings.json | 3 + webview-ui/src/i18n/locales/id/settings.json | 3 + webview-ui/src/i18n/locales/it/settings.json | 3 + webview-ui/src/i18n/locales/ja/settings.json | 3 + webview-ui/src/i18n/locales/ko/settings.json | 3 + webview-ui/src/i18n/locales/nl/settings.json | 3 + webview-ui/src/i18n/locales/pl/settings.json | 3 + .../src/i18n/locales/pt-BR/settings.json | 3 + webview-ui/src/i18n/locales/ru/settings.json | 3 + webview-ui/src/i18n/locales/tr/settings.json | 3 + webview-ui/src/i18n/locales/vi/settings.json | 3 + .../src/i18n/locales/zh-CN/settings.json | 3 + .../src/i18n/locales/zh-TW/settings.json | 3 + 32 files changed, 522 insertions(+) create mode 100644 packages/types/src/providers/moonshot.ts create mode 100644 src/api/providers/__tests__/moonshot.spec.ts create mode 100644 src/api/providers/moonshot.ts create mode 100644 webview-ui/src/components/settings/providers/Moonshot.tsx diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index a30550dce1..30521f2c68 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -159,6 +159,7 @@ export const SECRET_STATE_KEYS = [ "geminiApiKey", "openAiNativeApiKey", "deepSeekApiKey", + "moonshotApiKey", "mistralApiKey", "unboundApiKey", "requestyApiKey", diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 7df1b27db7..884337767f 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -22,6 +22,7 @@ export const providerNames = [ "gemini-cli", "openai-native", "mistral", + "moonshot", "deepseek", "unbound", "requesty", @@ -187,6 +188,13 @@ const deepSeekSchema = apiModelIdProviderModelSchema.extend({ deepSeekApiKey: z.string().optional(), }) +const moonshotSchema = apiModelIdProviderModelSchema.extend({ + moonshotBaseUrl: z + .union([z.literal("https://api.moonshot.ai/v1"), z.literal("https://api.moonshot.cn/v1")]) + .optional(), + moonshotApiKey: z.string().optional(), +}) + const unboundSchema = baseProviderSettingsSchema.extend({ unboundApiKey: z.string().optional(), unboundModelId: z.string().optional(), @@ -241,6 +249,7 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv openAiNativeSchema.merge(z.object({ apiProvider: z.literal("openai-native") })), mistralSchema.merge(z.object({ apiProvider: z.literal("mistral") })), deepSeekSchema.merge(z.object({ apiProvider: z.literal("deepseek") })), + moonshotSchema.merge(z.object({ apiProvider: z.literal("moonshot") })), unboundSchema.merge(z.object({ apiProvider: z.literal("unbound") })), requestySchema.merge(z.object({ apiProvider: z.literal("requesty") })), humanRelaySchema.merge(z.object({ apiProvider: z.literal("human-relay") })), @@ -269,6 +278,7 @@ export const providerSettingsSchema = z.object({ ...openAiNativeSchema.shape, ...mistralSchema.shape, ...deepSeekSchema.shape, + ...moonshotSchema.shape, ...unboundSchema.shape, ...requestySchema.shape, ...humanRelaySchema.shape, diff --git a/packages/types/src/providers/index.ts b/packages/types/src/providers/index.ts index 267401bd91..e4e506b8a7 100644 --- a/packages/types/src/providers/index.ts +++ b/packages/types/src/providers/index.ts @@ -9,6 +9,7 @@ export * from "./groq.js" export * from "./lite-llm.js" export * from "./lm-studio.js" export * from "./mistral.js" +export * from "./moonshot.js" export * from "./ollama.js" export * from "./openai.js" export * from "./openrouter.js" diff --git a/packages/types/src/providers/moonshot.ts b/packages/types/src/providers/moonshot.ts new file mode 100644 index 0000000000..d7e542077d --- /dev/null +++ b/packages/types/src/providers/moonshot.ts @@ -0,0 +1,22 @@ +import type { ModelInfo } from "../model.js" + +// https://platform.moonshot.ai/ +export type MoonshotModelId = keyof typeof moonshotModels + +export const moonshotDefaultModelId: MoonshotModelId = "kimi-k2-0711-preview" + +export const moonshotModels = { + "kimi-k2-0711-preview": { + maxTokens: 32_000, + contextWindow: 131_072, + supportsImages: false, + supportsPromptCache: true, + inputPrice: 0.6, // $0.60 per million tokens (cache miss) + outputPrice: 2.5, // $2.50 per million tokens + cacheWritesPrice: 0, // $0 per million tokens (cache miss) + cacheReadsPrice: 0.15, // $0.15 per million tokens (cache hit) + description: `Kimi K2 is a state-of-the-art mixture-of-experts (MoE) language model with 32 billion activated parameters and 1 trillion total parameters.`, + }, +} as const satisfies Record + +export const MOONSHOT_DEFAULT_TEMPERATURE = 0.6 diff --git a/src/api/index.ts b/src/api/index.ts index 960209f714..4598a711b2 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -17,6 +17,7 @@ import { GeminiHandler, OpenAiNativeHandler, DeepSeekHandler, + MoonshotHandler, MistralHandler, VsCodeLmHandler, UnboundHandler, @@ -89,6 +90,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler { return new OpenAiNativeHandler(options) case "deepseek": return new DeepSeekHandler(options) + case "moonshot": + return new MoonshotHandler(options) case "vscode-lm": return new VsCodeLmHandler(options) case "mistral": @@ -110,6 +113,7 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler { case "litellm": return new LiteLLMHandler(options) default: + apiProvider satisfies "gemini-cli" | undefined return new AnthropicHandler(options) } } diff --git a/src/api/providers/__tests__/moonshot.spec.ts b/src/api/providers/__tests__/moonshot.spec.ts new file mode 100644 index 0000000000..586dd5598e --- /dev/null +++ b/src/api/providers/__tests__/moonshot.spec.ts @@ -0,0 +1,297 @@ +// Mocks must come first, before imports +const mockCreate = vi.fn() +vi.mock("openai", () => { + return { + __esModule: true, + default: vi.fn().mockImplementation(() => ({ + chat: { + completions: { + create: mockCreate.mockImplementation(async (options) => { + if (!options.stream) { + return { + id: "test-completion", + choices: [ + { + message: { role: "assistant", content: "Test response", refusal: null }, + finish_reason: "stop", + index: 0, + }, + ], + usage: { + prompt_tokens: 10, + completion_tokens: 5, + total_tokens: 15, + cached_tokens: 2, + }, + } + } + + // Return async iterator for streaming + return { + [Symbol.asyncIterator]: async function* () { + yield { + choices: [ + { + delta: { content: "Test response" }, + index: 0, + }, + ], + usage: null, + } + yield { + choices: [ + { + delta: {}, + index: 0, + }, + ], + usage: { + prompt_tokens: 10, + completion_tokens: 5, + total_tokens: 15, + cached_tokens: 2, + }, + } + }, + } + }), + }, + }, + })), + } +}) + +import OpenAI from "openai" +import type { Anthropic } from "@anthropic-ai/sdk" + +import { moonshotDefaultModelId } from "@roo-code/types" + +import type { ApiHandlerOptions } from "../../../shared/api" + +import { MoonshotHandler } from "../moonshot" + +describe("MoonshotHandler", () => { + let handler: MoonshotHandler + let mockOptions: ApiHandlerOptions + + beforeEach(() => { + mockOptions = { + moonshotApiKey: "test-api-key", + apiModelId: "moonshot-chat", + moonshotBaseUrl: "https://api.moonshot.ai/v1", + } + handler = new MoonshotHandler(mockOptions) + vi.clearAllMocks() + }) + + describe("constructor", () => { + it("should initialize with provided options", () => { + expect(handler).toBeInstanceOf(MoonshotHandler) + expect(handler.getModel().id).toBe(mockOptions.apiModelId) + }) + + it.skip("should throw error if API key is missing", () => { + expect(() => { + new MoonshotHandler({ + ...mockOptions, + moonshotApiKey: undefined, + }) + }).toThrow("Moonshot API key is required") + }) + + it("should use default model ID if not provided", () => { + const handlerWithoutModel = new MoonshotHandler({ + ...mockOptions, + apiModelId: undefined, + }) + expect(handlerWithoutModel.getModel().id).toBe(moonshotDefaultModelId) + }) + + it("should use default base URL if not provided", () => { + const handlerWithoutBaseUrl = new MoonshotHandler({ + ...mockOptions, + moonshotBaseUrl: undefined, + }) + expect(handlerWithoutBaseUrl).toBeInstanceOf(MoonshotHandler) + // The base URL is passed to OpenAI client internally + expect(OpenAI).toHaveBeenCalledWith( + expect.objectContaining({ + baseURL: "https://api.moonshot.ai/v1", + }), + ) + }) + + it("should use chinese base URL if provided", () => { + const customBaseUrl = "https://api.moonshot.cn/v1" + const handlerWithCustomUrl = new MoonshotHandler({ + ...mockOptions, + moonshotBaseUrl: customBaseUrl, + }) + expect(handlerWithCustomUrl).toBeInstanceOf(MoonshotHandler) + // The custom base URL is passed to OpenAI client + expect(OpenAI).toHaveBeenCalledWith( + expect.objectContaining({ + baseURL: customBaseUrl, + }), + ) + }) + + it("should set includeMaxTokens to true", () => { + // Create a new handler and verify OpenAI client was called with includeMaxTokens + const _handler = new MoonshotHandler(mockOptions) + expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ apiKey: mockOptions.moonshotApiKey })) + }) + }) + + describe("getModel", () => { + it("should return model info for valid model ID", () => { + const model = handler.getModel() + expect(model.id).toBe(mockOptions.apiModelId) + expect(model.info).toBeDefined() + expect(model.info.maxTokens).toBe(32_000) + expect(model.info.contextWindow).toBe(131_072) + expect(model.info.supportsImages).toBe(false) + expect(model.info.supportsPromptCache).toBe(true) // Should be true now + }) + + it("should return provided model ID with default model info if model does not exist", () => { + const handlerWithInvalidModel = new MoonshotHandler({ + ...mockOptions, + apiModelId: "invalid-model", + }) + const model = handlerWithInvalidModel.getModel() + expect(model.id).toBe("invalid-model") // Returns provided ID + expect(model.info).toBeDefined() + // With the current implementation, it's the same object reference when using default model info + expect(model.info).toBe(handler.getModel().info) + // Should have the same base properties + expect(model.info.contextWindow).toBe(handler.getModel().info.contextWindow) + // And should have supportsPromptCache set to true + expect(model.info.supportsPromptCache).toBe(true) + }) + + it("should return default model if no model ID is provided", () => { + const handlerWithoutModel = new MoonshotHandler({ + ...mockOptions, + apiModelId: undefined, + }) + const model = handlerWithoutModel.getModel() + expect(model.id).toBe(moonshotDefaultModelId) + expect(model.info).toBeDefined() + expect(model.info.supportsPromptCache).toBe(true) + }) + + it("should include model parameters from getModelParams", () => { + const model = handler.getModel() + expect(model).toHaveProperty("temperature") + expect(model).toHaveProperty("maxTokens") + }) + }) + + describe("createMessage", () => { + const systemPrompt = "You are a helpful assistant." + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { + type: "text" as const, + text: "Hello!", + }, + ], + }, + ] + + it("should handle streaming responses", async () => { + const stream = handler.createMessage(systemPrompt, messages) + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + expect(chunks.length).toBeGreaterThan(0) + const textChunks = chunks.filter((chunk) => chunk.type === "text") + expect(textChunks).toHaveLength(1) + expect(textChunks[0].text).toBe("Test response") + }) + + it("should include usage information", async () => { + const stream = handler.createMessage(systemPrompt, messages) + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + const usageChunks = chunks.filter((chunk) => chunk.type === "usage") + expect(usageChunks.length).toBeGreaterThan(0) + expect(usageChunks[0].inputTokens).toBe(10) + expect(usageChunks[0].outputTokens).toBe(5) + }) + + it("should include cache metrics in usage information", async () => { + const stream = handler.createMessage(systemPrompt, messages) + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + const usageChunks = chunks.filter((chunk) => chunk.type === "usage") + expect(usageChunks.length).toBeGreaterThan(0) + expect(usageChunks[0].cacheWriteTokens).toBe(0) + expect(usageChunks[0].cacheReadTokens).toBe(2) + }) + }) + + describe("processUsageMetrics", () => { + it("should correctly process usage metrics including cache information", () => { + // We need to access the protected method, so we'll create a test subclass + class TestMoonshotHandler extends MoonshotHandler { + public testProcessUsageMetrics(usage: any) { + return this.processUsageMetrics(usage) + } + } + + const testHandler = new TestMoonshotHandler(mockOptions) + + const usage = { + prompt_tokens: 100, + completion_tokens: 50, + total_tokens: 150, + cached_tokens: 20, + } + + const result = testHandler.testProcessUsageMetrics(usage) + + expect(result.type).toBe("usage") + expect(result.inputTokens).toBe(100) + expect(result.outputTokens).toBe(50) + expect(result.cacheWriteTokens).toBe(0) + expect(result.cacheReadTokens).toBe(20) + }) + + it("should handle missing cache metrics gracefully", () => { + class TestMoonshotHandler extends MoonshotHandler { + public testProcessUsageMetrics(usage: any) { + return this.processUsageMetrics(usage) + } + } + + const testHandler = new TestMoonshotHandler(mockOptions) + + const usage = { + prompt_tokens: 100, + completion_tokens: 50, + total_tokens: 150, + // No cached_tokens + } + + const result = testHandler.testProcessUsageMetrics(usage) + + expect(result.type).toBe("usage") + expect(result.inputTokens).toBe(100) + expect(result.outputTokens).toBe(50) + expect(result.cacheWriteTokens).toBe(0) + expect(result.cacheReadTokens).toBeUndefined() + }) + }) +}) diff --git a/src/api/providers/index.ts b/src/api/providers/index.ts index 93df5c58ff..89d4c203ad 100644 --- a/src/api/providers/index.ts +++ b/src/api/providers/index.ts @@ -4,6 +4,7 @@ export { AwsBedrockHandler } from "./bedrock" export { ChutesHandler } from "./chutes" export { ClaudeCodeHandler } from "./claude-code" export { DeepSeekHandler } from "./deepseek" +export { MoonshotHandler } from "./moonshot" export { FakeAIHandler } from "./fake-ai" export { GeminiHandler } from "./gemini" export { GlamaHandler } from "./glama" diff --git a/src/api/providers/moonshot.ts b/src/api/providers/moonshot.ts new file mode 100644 index 0000000000..f04b369d1c --- /dev/null +++ b/src/api/providers/moonshot.ts @@ -0,0 +1,39 @@ +import { moonshotModels, moonshotDefaultModelId } from "@roo-code/types" + +import type { ApiHandlerOptions } from "../../shared/api" + +import type { ApiStreamUsageChunk } from "../transform/stream" +import { getModelParams } from "../transform/model-params" + +import { OpenAiHandler } from "./openai" + +export class MoonshotHandler extends OpenAiHandler { + constructor(options: ApiHandlerOptions) { + super({ + ...options, + openAiApiKey: options.moonshotApiKey ?? "not-provided", + openAiModelId: options.apiModelId ?? moonshotDefaultModelId, + openAiBaseUrl: options.moonshotBaseUrl ?? "https://api.moonshot.ai/v1", + openAiStreamingEnabled: true, + includeMaxTokens: true, + }) + } + + override getModel() { + const id = this.options.apiModelId ?? moonshotDefaultModelId + const info = moonshotModels[id as keyof typeof moonshotModels] || moonshotModels[moonshotDefaultModelId] + const params = getModelParams({ format: "openai", modelId: id, model: info, settings: this.options }) + return { id, info, ...params } + } + + // Override to handle Moonshot's usage metrics, including caching. + protected override processUsageMetrics(usage: any): ApiStreamUsageChunk { + return { + type: "usage", + inputTokens: usage?.prompt_tokens || 0, + outputTokens: usage?.completion_tokens || 0, + cacheWriteTokens: 0, + cacheReadTokens: usage?.cached_tokens, + } + } +} diff --git a/src/shared/__tests__/checkExistApiConfig.spec.ts b/src/shared/__tests__/checkExistApiConfig.spec.ts index 7bc9e1d576..7696f00cc0 100644 --- a/src/shared/__tests__/checkExistApiConfig.spec.ts +++ b/src/shared/__tests__/checkExistApiConfig.spec.ts @@ -53,6 +53,7 @@ describe("checkExistKey", () => { geminiApiKey: undefined, openAiNativeApiKey: undefined, deepSeekApiKey: undefined, + moonshotApiKey: undefined, mistralApiKey: undefined, vsCodeLmModelSelector: undefined, requestyApiKey: undefined, diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 681268a787..6c6c621956 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -18,6 +18,7 @@ import { claudeCodeDefaultModelId, geminiDefaultModelId, deepSeekDefaultModelId, + moonshotDefaultModelId, mistralDefaultModelId, xaiDefaultModelId, groqDefaultModelId, @@ -61,6 +62,7 @@ import { LMStudio, LiteLLM, Mistral, + Moonshot, Ollama, OpenAI, OpenAICompatible, @@ -287,6 +289,7 @@ const ApiOptions = ({ "openai-native": { field: "apiModelId", default: openAiNativeDefaultModelId }, gemini: { field: "apiModelId", default: geminiDefaultModelId }, deepseek: { field: "apiModelId", default: deepSeekDefaultModelId }, + moonshot: { field: "apiModelId", default: moonshotDefaultModelId }, mistral: { field: "apiModelId", default: mistralDefaultModelId }, xai: { field: "apiModelId", default: xaiDefaultModelId }, groq: { field: "apiModelId", default: groqDefaultModelId }, @@ -464,6 +467,10 @@ const ApiOptions = ({ )} + {selectedProvider === "moonshot" && ( + + )} + {selectedProvider === "vscode-lm" && ( )} diff --git a/webview-ui/src/components/settings/constants.ts b/webview-ui/src/components/settings/constants.ts index bbee8f990d..1140e4c0bc 100644 --- a/webview-ui/src/components/settings/constants.ts +++ b/webview-ui/src/components/settings/constants.ts @@ -5,6 +5,7 @@ import { bedrockModels, claudeCodeModels, deepSeekModels, + moonshotModels, geminiModels, mistralModels, openAiNativeModels, @@ -19,6 +20,7 @@ export const MODELS_BY_PROVIDER: Partial void +} + +export const Moonshot = ({ apiConfiguration, setApiConfigurationField }: MoonshotProps) => { + 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.moonshot.ai + + + api.moonshot.cn + + +
+
+ + + +
+ {t("settings:providers.apiKeyStorageNotice")} +
+ {!apiConfiguration?.moonshotApiKey && ( + + {t("settings:providers.getMoonshotApiKey")} + + )} +
+ + ) +} diff --git a/webview-ui/src/components/settings/providers/index.ts b/webview-ui/src/components/settings/providers/index.ts index b195607430..54974f7200 100644 --- a/webview-ui/src/components/settings/providers/index.ts +++ b/webview-ui/src/components/settings/providers/index.ts @@ -8,6 +8,7 @@ export { Glama } from "./Glama" export { Groq } from "./Groq" export { LMStudio } from "./LMStudio" export { Mistral } from "./Mistral" +export { Moonshot } from "./Moonshot" export { Ollama } from "./Ollama" export { OpenAI } from "./OpenAI" export { OpenAICompatible } from "./OpenAICompatible" diff --git a/webview-ui/src/components/ui/hooks/useSelectedModel.ts b/webview-ui/src/components/ui/hooks/useSelectedModel.ts index 40c1ff2431..928ebb42f4 100644 --- a/webview-ui/src/components/ui/hooks/useSelectedModel.ts +++ b/webview-ui/src/components/ui/hooks/useSelectedModel.ts @@ -8,6 +8,8 @@ import { bedrockModels, deepSeekDefaultModelId, deepSeekModels, + moonshotDefaultModelId, + moonshotModels, geminiDefaultModelId, geminiModels, mistralDefaultModelId, @@ -162,6 +164,11 @@ function getSelectedModel({ const info = deepSeekModels[id as keyof typeof deepSeekModels] return { id, info } } + case "moonshot": { + const id = apiConfiguration.apiModelId ?? moonshotDefaultModelId + const info = moonshotModels[id as keyof typeof moonshotModels] + return { id, info } + } case "openai-native": { const id = apiConfiguration.apiModelId ?? openAiNativeDefaultModelId const info = openAiNativeModels[id as keyof typeof openAiNativeModels] @@ -211,6 +218,7 @@ function getSelectedModel({ // case "human-relay": // case "fake-ai": default: { + provider satisfies "anthropic" | "gemini-cli" | "human-relay" | "fake-ai" const id = apiConfiguration.apiModelId ?? anthropicDefaultModelId const info = anthropicModels[id as keyof typeof anthropicModels] return { id, info } diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index 8f270ee4ce..cf17a6f919 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -253,6 +253,9 @@ "getChutesApiKey": "Obtenir clau API de Chutes", "deepSeekApiKey": "Clau API de DeepSeek", "getDeepSeekApiKey": "Obtenir clau API de DeepSeek", + "moonshotApiKey": "Clau API de Moonshot", + "getMoonshotApiKey": "Obtenir clau API de Moonshot", + "moonshotBaseUrl": "Punt d'entrada de Moonshot", "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 73a4ede3d8..20ffaec525 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -253,6 +253,9 @@ "getChutesApiKey": "Chutes API-Schlüssel erhalten", "deepSeekApiKey": "DeepSeek API-Schlüssel", "getDeepSeekApiKey": "DeepSeek API-Schlüssel erhalten", + "moonshotApiKey": "Moonshot API-Schlüssel", + "getMoonshotApiKey": "Moonshot API-Schlüssel erhalten", + "moonshotBaseUrl": "Moonshot-Einstiegspunkt", "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 457b4dbda9..4a826bddab 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -253,6 +253,9 @@ "getChutesApiKey": "Get Chutes API Key", "deepSeekApiKey": "DeepSeek API Key", "getDeepSeekApiKey": "Get DeepSeek API Key", + "moonshotApiKey": "Moonshot API Key", + "getMoonshotApiKey": "Get Moonshot API Key", + "moonshotBaseUrl": "Moonshot Entrypoint", "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 d217e66226..4c4f24bb0f 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -253,6 +253,9 @@ "getChutesApiKey": "Obtener clave API de Chutes", "deepSeekApiKey": "Clave API de DeepSeek", "getDeepSeekApiKey": "Obtener clave API de DeepSeek", + "moonshotApiKey": "Clave API de Moonshot", + "getMoonshotApiKey": "Obtener clave API de Moonshot", + "moonshotBaseUrl": "Punto de entrada de Moonshot", "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 f7f9a2683a..6e7a964694 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -253,6 +253,9 @@ "getChutesApiKey": "Obtenir la clé API Chutes", "deepSeekApiKey": "Clé API DeepSeek", "getDeepSeekApiKey": "Obtenir la clé API DeepSeek", + "moonshotApiKey": "Clé API Moonshot", + "getMoonshotApiKey": "Obtenir la clé API Moonshot", + "moonshotBaseUrl": "Point d'entrée Moonshot", "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 6a3679e090..ca255ca44e 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -253,6 +253,9 @@ "getChutesApiKey": "Chutes API कुंजी प्राप्त करें", "deepSeekApiKey": "DeepSeek API कुंजी", "getDeepSeekApiKey": "DeepSeek API कुंजी प्राप्त करें", + "moonshotApiKey": "Moonshot API कुंजी", + "getMoonshotApiKey": "Moonshot API कुंजी प्राप्त करें", + "moonshotBaseUrl": "Moonshot प्रवेश बिंदु", "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 09d7e8254a..0bff5ef4a1 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -257,6 +257,9 @@ "getChutesApiKey": "Dapatkan Chutes API Key", "deepSeekApiKey": "DeepSeek API Key", "getDeepSeekApiKey": "Dapatkan DeepSeek API Key", + "moonshotApiKey": "Kunci API Moonshot", + "getMoonshotApiKey": "Dapatkan Kunci API Moonshot", + "moonshotBaseUrl": "Titik Masuk Moonshot", "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 c2836f2c71..e5cd37a110 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -253,6 +253,9 @@ "getChutesApiKey": "Ottieni chiave API Chutes", "deepSeekApiKey": "Chiave API DeepSeek", "getDeepSeekApiKey": "Ottieni chiave API DeepSeek", + "moonshotApiKey": "Chiave API Moonshot", + "getMoonshotApiKey": "Ottieni chiave API Moonshot", + "moonshotBaseUrl": "Punto di ingresso Moonshot", "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 01233ef505..e32fac776a 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -253,6 +253,9 @@ "getChutesApiKey": "Chutes APIキーを取得", "deepSeekApiKey": "DeepSeek APIキー", "getDeepSeekApiKey": "DeepSeek APIキーを取得", + "moonshotApiKey": "Moonshot APIキー", + "getMoonshotApiKey": "Moonshot APIキーを取得", + "moonshotBaseUrl": "Moonshot エントリーポイント", "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 806b11f022..f48860b0ae 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -253,6 +253,9 @@ "getChutesApiKey": "Chutes API 키 받기", "deepSeekApiKey": "DeepSeek API 키", "getDeepSeekApiKey": "DeepSeek API 키 받기", + "moonshotApiKey": "Moonshot API 키", + "getMoonshotApiKey": "Moonshot API 키 받기", + "moonshotBaseUrl": "Moonshot 엔트리포인트", "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 28edfdfd52..bf6e65c995 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -253,6 +253,9 @@ "getChutesApiKey": "Chutes API-sleutel ophalen", "deepSeekApiKey": "DeepSeek API-sleutel", "getDeepSeekApiKey": "DeepSeek API-sleutel ophalen", + "moonshotApiKey": "Moonshot API-sleutel", + "getMoonshotApiKey": "Moonshot API-sleutel ophalen", + "moonshotBaseUrl": "Moonshot-ingangspunt", "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 d034830c1a..d42e4f51a9 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -253,6 +253,9 @@ "getChutesApiKey": "Uzyskaj klucz API Chutes", "deepSeekApiKey": "Klucz API DeepSeek", "getDeepSeekApiKey": "Uzyskaj klucz API DeepSeek", + "moonshotApiKey": "Klucz API Moonshot", + "getMoonshotApiKey": "Uzyskaj klucz API Moonshot", + "moonshotBaseUrl": "Punkt wejścia Moonshot", "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 140042c773..924542cf09 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -253,6 +253,9 @@ "getChutesApiKey": "Obter chave de API Chutes", "deepSeekApiKey": "Chave de API DeepSeek", "getDeepSeekApiKey": "Obter chave de API DeepSeek", + "moonshotApiKey": "Chave de API Moonshot", + "getMoonshotApiKey": "Obter chave de API Moonshot", + "moonshotBaseUrl": "Ponto de entrada Moonshot", "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 895655fbdf..cf719b0976 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -253,6 +253,9 @@ "getChutesApiKey": "Получить Chutes API-ключ", "deepSeekApiKey": "DeepSeek API-ключ", "getDeepSeekApiKey": "Получить DeepSeek API-ключ", + "moonshotApiKey": "Moonshot API-ключ", + "getMoonshotApiKey": "Получить Moonshot API-ключ", + "moonshotBaseUrl": "Точка входа Moonshot", "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 06878be920..fc8fc9c677 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -253,6 +253,9 @@ "getChutesApiKey": "Chutes API Anahtarı Al", "deepSeekApiKey": "DeepSeek API Anahtarı", "getDeepSeekApiKey": "DeepSeek API Anahtarı Al", + "moonshotApiKey": "Moonshot API Anahtarı", + "getMoonshotApiKey": "Moonshot API Anahtarı Al", + "moonshotBaseUrl": "Moonshot Giriş Noktası", "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 c83b7a6642..7d3e2803ad 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -253,6 +253,9 @@ "getChutesApiKey": "Lấy khóa API Chutes", "deepSeekApiKey": "Khóa API DeepSeek", "getDeepSeekApiKey": "Lấy khóa API DeepSeek", + "moonshotApiKey": "Khóa API Moonshot", + "getMoonshotApiKey": "Lấy khóa API Moonshot", + "moonshotBaseUrl": "Điểm vào Moonshot", "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 11c469bf77..eae71ac706 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -253,6 +253,9 @@ "getChutesApiKey": "获取 Chutes API 密钥", "deepSeekApiKey": "DeepSeek API 密钥", "getDeepSeekApiKey": "获取 DeepSeek API 密钥", + "moonshotApiKey": "Moonshot API 密钥", + "getMoonshotApiKey": "获取 Moonshot API 密钥", + "moonshotBaseUrl": "Moonshot 服务站点", "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 dab70b88b1..420ece916e 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -253,6 +253,9 @@ "getChutesApiKey": "取得 Chutes API 金鑰", "deepSeekApiKey": "DeepSeek API 金鑰", "getDeepSeekApiKey": "取得 DeepSeek API 金鑰", + "moonshotApiKey": "Moonshot API 金鑰", + "getMoonshotApiKey": "取得 Moonshot API 金鑰", + "moonshotBaseUrl": "Moonshot 服務站點", "geminiApiKey": "Gemini API 金鑰", "getGroqApiKey": "取得 Groq API 金鑰", "groqApiKey": "Groq API 金鑰", From e78d9541ca7cf72415bbc521165b9853576bae01 Mon Sep 17 00:00:00 2001 From: John Richmond <5629+jr@users.noreply.github.com> Date: Tue, 22 Jul 2025 07:16:15 -0700 Subject: [PATCH 6/6] Bugfix: Cloud: be more specific about session error codes (#6051) InvalidClientTokenError indicates an unrecoverable state for the session, so we need to be more exact about triggering it. A recent Clerk outage resulted in a lot of 429 responses which should really cause inactive-session, not a full clear to logged-out. --- packages/cloud/src/auth/WebAuthService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cloud/src/auth/WebAuthService.ts b/packages/cloud/src/auth/WebAuthService.ts index d14cbe67d8..82d3122426 100644 --- a/packages/cloud/src/auth/WebAuthService.ts +++ b/packages/cloud/src/auth/WebAuthService.ts @@ -494,7 +494,7 @@ export class WebAuthService extends EventEmitter implements A signal: AbortSignal.timeout(10000), }) - if (response.status >= 400 && response.status < 500) { + if (response.status === 401 || response.status === 404) { throw new InvalidClientTokenError() } else if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`)