From bcb8c81916bdc69982d872e009c9783d461c14e2 Mon Sep 17 00:00:00 2001 From: Hannes Rudolph Date: Sat, 14 Feb 2026 16:40:07 -0700 Subject: [PATCH] Reapply Batch 2: 9 minor-conflict non-AI-SDK cherry-picks (#11474) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: correct Bedrock model ID for Claude Opus 4.6 (#11232) Remove the :0 suffix from the Claude Opus 4.6 model ID to match the correct AWS Bedrock model identifier. The model ID was "anthropic.claude-opus-4-6-v1:0" but should be "anthropic.claude-opus-4-6-v1" per AWS Bedrock documentation. Fixes #11231 Co-authored-by: Roo Code * fix: guard against empty-string baseURL in provider constructors (#11233) When the 'custom base URL' checkbox is unchecked in the UI, the setting is set to '' (empty string). Providers that passed this directly to their SDK constructors caused 'Failed to parse URL' errors because the SDK treated '' as a valid but broken base URL override. - gemini.ts: use || undefined (was passing raw option) - openai-native.ts: use || undefined (was passing raw option) - openai.ts: change ?? to || for fallback default - deepseek.ts: change ?? to || for fallback default - moonshot.ts: change ?? to || for fallback default Adds test coverage for Gemini and OpenAI Native constructors verifying empty-string baseURL is coerced to undefined. * fix: make defaultTemperature required in getModelParams to prevent silent temperature overrides (#11218) * fix: DeepSeek temperature defaulting to 0 instead of 0.3 Pass defaultTemperature: DEEP_SEEK_DEFAULT_TEMPERATURE to getModelParams() in DeepSeekHandler.getModel() to ensure the correct default temperature (0.3) is used when no user configuration is provided. Closes #11194 * refactor: make defaultTemperature required in getModelParams Make the defaultTemperature parameter required in getModelParams() instead of defaulting to 0. This prevents providers with their own non-zero default temperature (like DeepSeek's 0.3) from being silently overridden by the implicit 0 default. Every provider now explicitly declares its temperature default, making the temperature resolution chain clear: user setting → model default → provider default --------- Co-authored-by: Roo Code Co-authored-by: daniel-lxs * feat: batch consecutive tool calls in chat UI with shared utility (#11245) * feat: group consecutive list_files tool calls into single UI block Consolidate consecutive listFilesTopLevel/listFilesRecursive ask messages into a single 'Roo wants to view multiple directories' block, matching the existing read_file batching pattern. * chore: add missing translation keys for all locales * refactor: consolidate duplicate listFiles batch-handling blocks in ChatRow Merge the separate listFilesTopLevel and listFilesRecursive case blocks into a single combined case with shared batch-detection logic, selecting the icon and translation key based on the tool type. This removes the duplicated isBatchDirRequest check and BatchListFilesPermission render. * feat: batch consecutive file-edit tool calls into single UI block Add edit-file batching in ChatView groupedMessages that consolidates consecutive editedExistingFile, appliedDiff, newFileCreated, insertContent, and searchAndReplace asks into a single BatchDiffApproval block. Move batchDiffs detection in ChatRow above the switch statement so it applies to any file-edit tool type. * refactor: extract batchConsecutive utility, fix batch UI issues - Extract generic batchConsecutive() utility from 3 identical while-loops - Fix React key collisions in BatchListFilesPermission, BatchFilePermission, BatchDiffApproval - Normalize language prop to "shellsession" (was "shell-session" for top-level) - Remove unused _batchedMessages property from synthetic messages - Remove dead didViewMultipleDirectories i18n key from all 18 locale files - Add batch button text for listFilesTopLevel/listFilesRecursive - Add batchConsecutive utility tests (6 cases) * fix: audit improvements for batch tool-call UI - Make batchConsecutive() generic instead of ClineMessage-specific - Add batch-aware button text for edit-file batches ("Save All"/"Deny All") - Add dedicated list-batch/edit-batch i18n keys (stop reusing read-batch) - Add JSON.parse defense-in-depth in all three synthesizers - Fix mixed list_files batch icon to default to FolderTree - Add 6 missing test cases (all-match, immutability, spy, single-dir) * chore: minor type cleanup (out-of-scope housekeeping) - Trim unused recursive/isOutsideWorkspace from DirPermissionItem interface - Remove 4 pre-existing `as any` casts in ChatView.tsx: - window cast → precise inline type - checkpoint bracket access → removed unnecessary casts - condensing message → `as ClineMessage` - debounce cancel → `.clear()` (correct API) - Update BatchListFilesPermission test data to match trimmed interface * i18n: add list-batch and edit-batch translations for all locales * feat: add IPC query handlers for commands, modes, and models (#11279) Add GetCommands, GetModes, and GetModels to the IPC protocol so external clients can fetch slash commands, available modes, and Roo provider models without going through the internal webview message channel. Co-authored-by: Claude Opus 4.6 * feat: add lock toggle to pin API config across all modes in workspace (#11295) * feat: add lock toggle to pin API config across all modes in workspace Add a lock/unlock toggle inside the API config selector popover (next to the settings gear) that, when enabled, applies the selected API configuration to all modes in the current workspace. - Add lockApiConfigAcrossModes to ExtensionState and WebviewMessage types - Store setting in workspaceState (per-workspace, not global) - When locked, activateProviderProfile sets config for all modes - Lock icon in ApiConfigSelector popover bottom bar next to gear - Full i18n: English + 17 locale translations (all mention workspace scope) - 9 new tests: 2 ClineProvider, 2 handler, 5 UI (77 total pass) * refactor: replace write-fan-out with read-time override for lock API config The original lock implementation used setModeConfig() fan-out to write the locked config to ALL modes globally. Since the lock flag lives in workspace- scoped workspaceState but modeApiConfigs are in global secrets, this caused cross-workspace data destruction. Replaced with read-time guards: - handleModeSwitch: early return when lock is on (skip per-mode config load) - createTaskWithHistoryItem: skip mode-based config restoration under lock - activateProviderProfile: removed fan-out block - lockApiConfigAcrossModes handler: simplified to flag + state post only - Fixed pre-existing workspaceState mock gap in ClineProvider.spec.ts and ClineProvider.sticky-profile.spec.ts * fix: validate Gemini thinkingLevel against model capabilities and handle empty streams (#11303) * fix: validate Gemini thinkingLevel against model capabilities and handle empty streams getGeminiReasoning() now validates the selected effort against the model's supportsReasoningEffort array before sending it as thinkingLevel. When a stale settings value (e.g. 'medium' from a different model) is not in the supported set, it falls back to the model's default reasoningEffort. GeminiHandler.createMessage() now tracks whether any text content was yielded during streaming and handles NoOutputGeneratedError gracefully instead of surfacing the cryptic 'No output generated' error. * fix: guard thinkingLevel fallback against 'none' effort and add i18n TODO The array validation fallback in getGeminiReasoning() now only triggers when the selected effort IS a valid Gemini thinking level but not in the model's supported set. Values like 'none' (explicit no-reasoning signal) are no longer overridden by the model default. Also adds a TODO for moving the empty-stream message to i18n. * fix: track tool_call_start in hasContent to avoid false empty-stream warning Tool-only responses (no text) are valid content. Without this, agentic tool-call responses would incorrectly trigger the empty response warning message. * chore(cli): prepare release v0.0.53 (#11425) * feat: add GLM-5 model support to Z.ai provider (#11440) * chore: regenerate pnpm-lock.yaml * fix: resolve type errors and remove AI SDK test contamination * docs: update progress.txt with rebuilt Batch 2 status --------- Co-authored-by: roomote[bot] <219738659+roomote[bot]@users.noreply.github.com> Co-authored-by: Roo Code Co-authored-by: daniel-lxs Co-authored-by: Chris Estreich Co-authored-by: Claude Opus 4.6 --- apps/cli/CHANGELOG.md | 23 ++ apps/cli/package.json | 2 +- packages/types/src/events.ts | 37 ++ packages/types/src/ipc.ts | 12 + packages/types/src/providers/bedrock.ts | 6 +- packages/types/src/providers/zai.ts | 30 ++ packages/types/src/vscode-extension-host.ts | 8 + pnpm-lock.yaml | 125 +++--- progress.txt | 35 ++ src/api/providers/__tests__/deepseek.spec.ts | 16 +- .../providers/__tests__/openai-native.spec.ts | 23 ++ src/api/providers/anthropic-vertex.ts | 8 +- src/api/providers/anthropic.ts | 1 + src/api/providers/deepinfra.ts | 1 + src/api/providers/deepseek.ts | 10 +- src/api/providers/doubao.ts | 8 +- src/api/providers/moonshot.ts | 10 +- src/api/providers/openai-native.ts | 2 +- src/api/providers/openai.ts | 10 +- src/api/providers/requesty.ts | 1 + src/api/providers/unbound.ts | 1 + src/api/providers/vertex.ts | 8 +- src/api/providers/xai.ts | 8 +- src/api/providers/zai.ts | 4 +- .../transform/__tests__/model-params.spec.ts | 10 +- src/api/transform/__tests__/reasoning.spec.ts | 123 ++++++ src/api/transform/model-params.ts | 4 +- src/api/transform/reasoning.ts | 14 +- src/core/webview/ClineProvider.ts | 13 +- .../ClineProvider.apiHandlerRebuild.spec.ts | 5 + .../ClineProvider.lockApiConfig.spec.ts | 372 ++++++++++++++++++ .../webview/__tests__/ClineProvider.spec.ts | 30 ++ .../ClineProvider.sticky-mode.spec.ts | 5 + .../ClineProvider.sticky-profile.spec.ts | 5 + .../ClineProvider.taskHistory.spec.ts | 5 + ...ebviewMessageHandler.lockApiConfig.spec.ts | 68 ++++ src/core/webview/webviewMessageHandler.ts | 8 + src/extension/api.ts | 60 ++- .../src/components/chat/ApiConfigSelector.tsx | 14 + .../src/components/chat/BatchDiffApproval.tsx | 4 +- .../components/chat/BatchFilePermission.tsx | 4 +- .../chat/BatchListFilesPermission.tsx | 45 +++ .../src/components/chat/ChatTextArea.tsx | 8 + webview-ui/src/components/chat/ChatView.tsx | 210 +++++++--- .../chat/__tests__/ApiConfigSelector.spec.tsx | 2 + .../BatchListFilesPermission.spec.tsx | 103 +++++ .../ChatTextArea.lockApiConfig.spec.tsx | 156 ++++++++ .../src/context/ExtensionStateContext.tsx | 1 + webview-ui/src/i18n/locales/ca/chat.json | 21 +- webview-ui/src/i18n/locales/de/chat.json | 21 +- webview-ui/src/i18n/locales/en/chat.json | 19 + webview-ui/src/i18n/locales/es/chat.json | 21 +- webview-ui/src/i18n/locales/fr/chat.json | 21 +- webview-ui/src/i18n/locales/hi/chat.json | 21 +- webview-ui/src/i18n/locales/id/chat.json | 21 +- webview-ui/src/i18n/locales/it/chat.json | 21 +- webview-ui/src/i18n/locales/ja/chat.json | 21 +- webview-ui/src/i18n/locales/ko/chat.json | 21 +- webview-ui/src/i18n/locales/nl/chat.json | 25 +- webview-ui/src/i18n/locales/pl/chat.json | 25 +- webview-ui/src/i18n/locales/pt-BR/chat.json | 21 +- webview-ui/src/i18n/locales/ru/chat.json | 21 +- webview-ui/src/i18n/locales/tr/chat.json | 21 +- webview-ui/src/i18n/locales/vi/chat.json | 21 +- webview-ui/src/i18n/locales/zh-CN/chat.json | 21 +- webview-ui/src/i18n/locales/zh-TW/chat.json | 25 +- .../utils/__tests__/batchConsecutive.spec.ts | 116 ++++++ webview-ui/src/utils/batchConsecutive.ts | 38 ++ 68 files changed, 2004 insertions(+), 196 deletions(-) create mode 100644 progress.txt create mode 100644 src/core/webview/__tests__/ClineProvider.lockApiConfig.spec.ts create mode 100644 src/core/webview/__tests__/webviewMessageHandler.lockApiConfig.spec.ts create mode 100644 webview-ui/src/components/chat/BatchListFilesPermission.tsx create mode 100644 webview-ui/src/components/chat/__tests__/BatchListFilesPermission.spec.tsx create mode 100644 webview-ui/src/components/chat/__tests__/ChatTextArea.lockApiConfig.spec.tsx create mode 100644 webview-ui/src/utils/__tests__/batchConsecutive.spec.ts create mode 100644 webview-ui/src/utils/batchConsecutive.ts diff --git a/apps/cli/CHANGELOG.md b/apps/cli/CHANGELOG.md index e328a927bb..ae12d4591b 100644 --- a/apps/cli/CHANGELOG.md +++ b/apps/cli/CHANGELOG.md @@ -5,6 +5,29 @@ All notable changes to the `@roo-code/cli` package will be documented in this fi The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.0.53] - 2026-02-12 + +### Changed + +- **Auto-Approve by Default**: The CLI now auto-approves all actions (tools, commands, browser, MCP) by default. Followup questions auto-select the first suggestion after a 60-second timeout. +- **New `--require-approval` Flag**: Replaced `-y`/`--yes`/`--dangerously-skip-permissions` flags with a new `-a, --require-approval` flag for users who want manual approval prompts before actions execute. + +### Fixed + +- Spamming the escape key to cancel a running task no longer crashes the cli. + +## [0.0.52] - 2026-02-09 + +### Added + +- **Linux Support**: Added support for `linux-arm64`. + +## [0.0.51] - 2026-02-06 + +### Changed + +- **Default Model Update**: Changed the default model from Opus 4.5 to Opus 4.6 for improved performance and capabilities + ## [0.0.50] - 2026-02-05 ### Added diff --git a/apps/cli/package.json b/apps/cli/package.json index 9d3014bb6c..028d024e81 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,6 +1,6 @@ { "name": "@roo-code/cli", - "version": "0.0.50", + "version": "0.0.53", "description": "Roo Code CLI - Run the Roo Code agent from the command line", "private": true, "type": "module", diff --git a/packages/types/src/events.ts b/packages/types/src/events.ts index d4a05f8e3e..54267d67e4 100644 --- a/packages/types/src/events.ts +++ b/packages/types/src/events.ts @@ -1,6 +1,7 @@ import { z } from "zod" import { clineMessageSchema, queuedMessageSchema, tokenUsageSchema } from "./message.js" +import { modelInfoSchema } from "./model.js" import { toolNamesSchema, toolUsageSchema } from "./tool.js" /** @@ -45,6 +46,11 @@ export enum RooCodeEventName { ModeChanged = "modeChanged", ProviderProfileChanged = "providerProfileChanged", + // Query Responses + CommandsResponse = "commandsResponse", + ModesResponse = "modesResponse", + ModelsResponse = "modelsResponse", + // Evals EvalPass = "evalPass", EvalFail = "evalFail", @@ -108,6 +114,20 @@ export const rooCodeEventsSchema = z.object({ [RooCodeEventName.ModeChanged]: z.tuple([z.string()]), [RooCodeEventName.ProviderProfileChanged]: z.tuple([z.object({ name: z.string(), provider: z.string() })]), + + [RooCodeEventName.CommandsResponse]: z.tuple([ + z.array( + z.object({ + name: z.string(), + source: z.enum(["global", "project", "built-in"]), + filePath: z.string().optional(), + description: z.string().optional(), + argumentHint: z.string().optional(), + }), + ), + ]), + [RooCodeEventName.ModesResponse]: z.tuple([z.array(z.object({ slug: z.string(), name: z.string() }))]), + [RooCodeEventName.ModelsResponse]: z.tuple([z.record(z.string(), modelInfoSchema)]), }) export type RooCodeEvents = z.infer @@ -237,6 +257,23 @@ export const taskEventSchema = z.discriminatedUnion("eventName", [ taskId: z.number().optional(), }), + // Query Responses + z.object({ + eventName: z.literal(RooCodeEventName.CommandsResponse), + payload: rooCodeEventsSchema.shape[RooCodeEventName.CommandsResponse], + taskId: z.number().optional(), + }), + z.object({ + eventName: z.literal(RooCodeEventName.ModesResponse), + payload: rooCodeEventsSchema.shape[RooCodeEventName.ModesResponse], + taskId: z.number().optional(), + }), + z.object({ + eventName: z.literal(RooCodeEventName.ModelsResponse), + payload: rooCodeEventsSchema.shape[RooCodeEventName.ModelsResponse], + taskId: z.number().optional(), + }), + // Evals z.object({ eventName: z.literal(RooCodeEventName.EvalPass), diff --git a/packages/types/src/ipc.ts b/packages/types/src/ipc.ts index 9f6d2de04d..90a1478a4d 100644 --- a/packages/types/src/ipc.ts +++ b/packages/types/src/ipc.ts @@ -46,6 +46,9 @@ export enum TaskCommandName { CloseTask = "CloseTask", ResumeTask = "ResumeTask", SendMessage = "SendMessage", + GetCommands = "GetCommands", + GetModes = "GetModes", + GetModels = "GetModels", } /** @@ -79,6 +82,15 @@ export const taskCommandSchema = z.discriminatedUnion("commandName", [ images: z.array(z.string()).optional(), }), }), + z.object({ + commandName: z.literal(TaskCommandName.GetCommands), + }), + z.object({ + commandName: z.literal(TaskCommandName.GetModes), + }), + z.object({ + commandName: z.literal(TaskCommandName.GetModels), + }), ]) export type TaskCommand = z.infer diff --git a/packages/types/src/providers/bedrock.ts b/packages/types/src/providers/bedrock.ts index 69d6493357..008961b301 100644 --- a/packages/types/src/providers/bedrock.ts +++ b/packages/types/src/providers/bedrock.ts @@ -119,7 +119,7 @@ export const bedrockModels = { maxCachePoints: 4, cachableFields: ["system", "messages", "tools"], }, - "anthropic.claude-opus-4-6-v1:0": { + "anthropic.claude-opus-4-6-v1": { maxTokens: 8192, contextWindow: 200_000, // Default 200K, extendable to 1M with beta flag 'context-1m-2025-08-07' supportsImages: true, @@ -499,7 +499,7 @@ export const BEDROCK_REGIONS = [ export const BEDROCK_1M_CONTEXT_MODEL_IDS = [ "anthropic.claude-sonnet-4-20250514-v1:0", "anthropic.claude-sonnet-4-5-20250929-v1:0", - "anthropic.claude-opus-4-6-v1:0", + "anthropic.claude-opus-4-6-v1", ] as const // Amazon Bedrock models that support Global Inference profiles @@ -514,7 +514,7 @@ export const BEDROCK_GLOBAL_INFERENCE_MODEL_IDS = [ "anthropic.claude-sonnet-4-5-20250929-v1:0", "anthropic.claude-haiku-4-5-20251001-v1:0", "anthropic.claude-opus-4-5-20251101-v1:0", - "anthropic.claude-opus-4-6-v1:0", + "anthropic.claude-opus-4-6-v1", ] as const // Amazon Bedrock Service Tier types diff --git a/packages/types/src/providers/zai.ts b/packages/types/src/providers/zai.ts index 41a6a808ca..69f90f232a 100644 --- a/packages/types/src/providers/zai.ts +++ b/packages/types/src/providers/zai.ts @@ -120,6 +120,21 @@ export const internationalZAiModels = { description: "GLM-4.7 is Zhipu's latest model with built-in thinking capabilities enabled by default. It provides enhanced reasoning for complex tasks while maintaining fast response times.", }, + "glm-5": { + maxTokens: 16_384, + contextWindow: 202_752, + supportsImages: false, + supportsPromptCache: true, + supportsReasoningEffort: ["disable", "medium"], + reasoningEffort: "medium", + preserveReasoning: true, + inputPrice: 0.6, + outputPrice: 2.2, + cacheWritesPrice: 0, + cacheReadsPrice: 0.11, + description: + "GLM-5 is Zhipu's next-generation model with a 202k context window and built-in thinking capabilities. It delivers state-of-the-art reasoning, coding, and agentic performance.", + }, "glm-4.7-flash": { maxTokens: 16_384, contextWindow: 200_000, @@ -281,6 +296,21 @@ export const mainlandZAiModels = { description: "GLM-4.7 is Zhipu's latest model with built-in thinking capabilities enabled by default. It provides enhanced reasoning for complex tasks while maintaining fast response times.", }, + "glm-5": { + maxTokens: 16_384, + contextWindow: 202_752, + supportsImages: false, + supportsPromptCache: true, + supportsReasoningEffort: ["disable", "medium"], + reasoningEffort: "medium", + preserveReasoning: true, + inputPrice: 0.29, + outputPrice: 1.14, + cacheWritesPrice: 0, + cacheReadsPrice: 0.057, + description: + "GLM-5 is Zhipu's next-generation model with a 202k context window and built-in thinking capabilities. It delivers state-of-the-art reasoning, coding, and agentic performance.", + }, "glm-4.7-flash": { maxTokens: 16_384, contextWindow: 204_800, diff --git a/packages/types/src/vscode-extension-host.ts b/packages/types/src/vscode-extension-host.ts index c9f7a3a923..fcabae2388 100644 --- a/packages/types/src/vscode-extension-host.ts +++ b/packages/types/src/vscode-extension-host.ts @@ -333,6 +333,7 @@ export type ExtensionState = Pick< | "showWorktreesInHomeScreen" | "disabledTools" > & { + lockApiConfigAcrossModes?: boolean version: string clineMessages: ClineMessage[] currentTaskItem?: HistoryItem @@ -529,6 +530,7 @@ export interface WebviewMessage { | "searchFiles" | "toggleApiConfigPin" | "hasOpenedModeSelector" + | "lockApiConfigAcrossModes" | "clearCloudAuthSkipModel" | "cloudButtonClicked" | "rooCloudSignIn" @@ -833,6 +835,12 @@ export interface ClineSayTool { startLine?: number }> }> + batchDirs?: Array<{ + path: string + recursive: boolean + isOutsideWorkspace?: boolean + key: string + }> question?: string imageData?: string // Base64 encoded image data for generated images // Properties for runSlashCommand tool diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 49e547e469..d202a0456d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -995,10 +995,10 @@ importers: devDependencies: '@ai-sdk/openai-compatible': specifier: ^1.0.0 - version: 1.0.31(zod@3.25.76) + version: 1.0.11(zod@3.25.76) '@openrouter/ai-sdk-provider': specifier: ^2.0.4 - version: 2.1.1(ai@6.0.57(zod@3.25.76))(zod@3.25.76) + version: 2.1.1(ai@6.0.77(zod@3.25.76))(zod@3.25.76) '@roo-code/build': specifier: workspace:^ version: link:../packages/build @@ -1073,7 +1073,7 @@ importers: version: 3.3.2 ai: specifier: ^6.0.0 - version: 6.0.57(zod@3.25.76) + version: 6.0.77(zod@3.25.76) esbuild-wasm: specifier: ^0.25.0 version: 0.25.12 @@ -1390,36 +1390,36 @@ packages: '@adobe/css-tools@4.4.2': resolution: {integrity: sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A==} - '@ai-sdk/gateway@3.0.25': - resolution: {integrity: sha512-j0AQeA7hOVqwImykQlganf/Euj3uEXf0h3G0O4qKTDpEwE+EZGIPnVimCWht5W91lAetPZSfavDyvfpuPDd2PQ==} + '@ai-sdk/gateway@3.0.39': + resolution: {integrity: sha512-SeCZBAdDNbWpVUXiYgOAqis22p5MEYfrjRw0hiBa5hM+7sDGYQpMinUjkM8kbPXMkY+AhKLrHleBl+SuqpzlgA==} engines: {node: '>=18'} peerDependencies: zod: 3.25.76 - '@ai-sdk/openai-compatible@1.0.31': - resolution: {integrity: sha512-znBvaVHM0M6yWNerIEy3hR+O8ZK2sPcE7e2cxfb6kYLEX3k//JH5VDnRnajseVofg7LXtTCFFdjsB7WLf1BdeQ==} + '@ai-sdk/openai-compatible@1.0.11': + resolution: {integrity: sha512-eRD6dZviy31KYz4YvxAR/c6UEYx3p4pCiWZeDdYdAHj0rn8xZlGVxtQRs1qynhz6IYGOo4aLBf9zVW5w0tI/Uw==} engines: {node: '>=18'} peerDependencies: zod: 3.25.76 - '@ai-sdk/provider-utils@3.0.20': - resolution: {integrity: sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ==} + '@ai-sdk/provider-utils@3.0.5': + resolution: {integrity: sha512-HliwB/yzufw3iwczbFVE2Fiwf1XqROB/I6ng8EKUsPM5+2wnIa8f4VbljZcDx+grhFrPV+PnRZH7zBqi8WZM7Q==} engines: {node: '>=18'} peerDependencies: zod: 3.25.76 - '@ai-sdk/provider-utils@4.0.10': - resolution: {integrity: sha512-VeDAiCH+ZK8Xs4hb9Cw7pHlujWNL52RKe8TExOkrw6Ir1AmfajBZTb9XUdKOZO08RwQElIKA8+Ltm+Gqfo8djQ==} + '@ai-sdk/provider-utils@4.0.14': + resolution: {integrity: sha512-7bzKd9lgiDeXM7O4U4nQ8iTxguAOkg8LZGD9AfDVZYjO5cKYRwBPwVjboFcVrxncRHu0tYxZtXZtiLKpG4pEng==} engines: {node: '>=18'} peerDependencies: zod: 3.25.76 - '@ai-sdk/provider@2.0.1': - resolution: {integrity: sha512-KCUwswvsC5VsW2PWFqF8eJgSCu5Ysj7m1TxiHTVA6g7k360bk0RNQENT8KTMAYEs+8fWPD3Uu4dEmzGHc+jGng==} + '@ai-sdk/provider@2.0.0': + resolution: {integrity: sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==} engines: {node: '>=18'} - '@ai-sdk/provider@3.0.5': - resolution: {integrity: sha512-2Xmoq6DBJqmSl80U6V9z5jJSJP7ehaJJQMy2iFUqTay06wdCqTnPVBBQbtEL8RCChenL+q5DC5H5WzU3vV3v8w==} + '@ai-sdk/provider@3.0.8': + resolution: {integrity: sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ==} engines: {node: '>=18'} '@alcalzone/ansi-tokenize@0.2.3': @@ -1573,14 +1573,6 @@ packages: resolution: {integrity: sha512-/inmPnjZE0ZBE16zaCowAvouSx05FJ7p6BQYuzlJ8vxEU0sS0Hf8fvhuiRnN9V9eDUPIBY+/5EjbMWygXL4wlQ==} engines: {node: '>=18.0.0'} - '@aws-sdk/types@3.804.0': - resolution: {integrity: sha512-A9qnsy9zQ8G89vrPPlNG9d1d8QcKRGqJKqwyGgS0dclJpwy6d1EWgQLIolKPl6vcFpLoe6avLOLxr+h8ur5wpg==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/types@3.840.0': - resolution: {integrity: sha512-xliuHaUFZxEx1NSXeLLZ9Dyu6+EJVQKEoD+yM+zqUo3YDZ7medKJWY6fIOKiPX/N7XbLdBYwajb15Q7IL8KkeA==} - engines: {node: '>=18.0.0'} - '@aws-sdk/types@3.922.0': resolution: {integrity: sha512-eLA6XjVobAUAMivvM7DBL79mnHyrm+32TkXNWZua5mnxF+6kQCfblKKJvxMZLGosO53/Ex46ogim8IY5Nbqv2w==} engines: {node: '>=18.0.0'} @@ -3938,10 +3930,6 @@ packages: resolution: {integrity: sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==} engines: {node: '>=16.0.0'} - '@smithy/types@4.3.1': - resolution: {integrity: sha512-UqKOQBL2x6+HWl3P+3QqFD4ncKq0I8Nuz9QItGv5WuKuMHuuwlhvqcZCoXGfc+P1QmfJE7VieykoYYmrOoFJxA==} - engines: {node: '>=18.0.0'} - '@smithy/types@4.8.1': resolution: {integrity: sha512-N0Zn0OT1zc+NA+UVfkYqQzviRh5ucWwO7mBV3TmHHprMnfcJNfhlPicDkBHi0ewbh+y3evR6cNAW0Raxvb01NA==} engines: {node: '>=18.0.0'} @@ -4840,8 +4828,8 @@ packages: resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} engines: {node: '>= 8.0.0'} - ai@6.0.57: - resolution: {integrity: sha512-5wYcMQmOaNU71wGv4XX1db3zvn4uLjLbTKIo6cQZPWOJElA0882XI7Eawx6TCd5jbjOvKMIP+KLWbpVomAFT2g==} + ai@6.0.77: + resolution: {integrity: sha512-tyyhrRpCRFVlivdNIFLK8cexSBB2jwTqO0z1qJQagk+UxZ+MW8h5V8xsvvb+xdKDY482Y8KAm0mr7TDnPKvvlw==} engines: {node: '>=18'} peerDependencies: zod: 3.25.76 @@ -6436,10 +6424,6 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} - eventsource-parser@3.0.2: - resolution: {integrity: sha512-6RxOBZ/cYgd8usLwsEl+EC09Au/9BcmCKYF2/xbml6DNczf7nv0MQb+7BA2F+li6//I+28VNlQR37XfQtcAJuA==} - engines: {node: '>=18.0.0'} - eventsource-parser@3.0.6: resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} engines: {node: '>=18.0.0'} @@ -10992,38 +10976,39 @@ snapshots: '@adobe/css-tools@4.4.2': {} - '@ai-sdk/gateway@3.0.25(zod@3.25.76)': + '@ai-sdk/gateway@3.0.39(zod@3.25.76)': dependencies: - '@ai-sdk/provider': 3.0.5 - '@ai-sdk/provider-utils': 4.0.10(zod@3.25.76) + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.14(zod@3.25.76) '@vercel/oidc': 3.1.0 zod: 3.25.76 - '@ai-sdk/openai-compatible@1.0.31(zod@3.25.76)': + '@ai-sdk/openai-compatible@1.0.11(zod@3.25.76)': dependencies: - '@ai-sdk/provider': 2.0.1 - '@ai-sdk/provider-utils': 3.0.20(zod@3.25.76) + '@ai-sdk/provider': 2.0.0 + '@ai-sdk/provider-utils': 3.0.5(zod@3.25.76) zod: 3.25.76 - '@ai-sdk/provider-utils@3.0.20(zod@3.25.76)': + '@ai-sdk/provider-utils@3.0.5(zod@3.25.76)': dependencies: - '@ai-sdk/provider': 2.0.1 + '@ai-sdk/provider': 2.0.0 + '@standard-schema/spec': 1.1.0 + eventsource-parser: 3.0.6 + zod: 3.25.76 + zod-to-json-schema: 3.24.5(zod@3.25.76) + + '@ai-sdk/provider-utils@4.0.14(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 3.0.8 '@standard-schema/spec': 1.1.0 eventsource-parser: 3.0.6 zod: 3.25.76 - '@ai-sdk/provider-utils@4.0.10(zod@3.25.76)': - dependencies: - '@ai-sdk/provider': 3.0.5 - '@standard-schema/spec': 1.1.0 - eventsource-parser: 3.0.6 - zod: 3.25.76 - - '@ai-sdk/provider@2.0.1': + '@ai-sdk/provider@2.0.0': dependencies: json-schema: 0.4.0 - '@ai-sdk/provider@3.0.5': + '@ai-sdk/provider@3.0.8': dependencies: json-schema: 0.4.0 @@ -11094,7 +11079,7 @@ snapshots: '@aws-crypto/crc32@3.0.0': dependencies: '@aws-crypto/util': 3.0.0 - '@aws-sdk/types': 3.840.0 + '@aws-sdk/types': 3.922.0 tslib: 1.14.1 '@aws-crypto/crc32@5.2.0': @@ -11116,7 +11101,7 @@ snapshots: '@aws-crypto/sha256-js@4.0.0': dependencies: '@aws-crypto/util': 4.0.0 - '@aws-sdk/types': 3.804.0 + '@aws-sdk/types': 3.922.0 tslib: 1.14.1 '@aws-crypto/sha256-js@5.2.0': @@ -11131,13 +11116,13 @@ snapshots: '@aws-crypto/util@3.0.0': dependencies: - '@aws-sdk/types': 3.840.0 + '@aws-sdk/types': 3.922.0 '@aws-sdk/util-utf8-browser': 3.259.0 tslib: 1.14.1 '@aws-crypto/util@4.0.0': dependencies: - '@aws-sdk/types': 3.840.0 + '@aws-sdk/types': 3.922.0 '@aws-sdk/util-utf8-browser': 3.259.0 tslib: 1.14.1 @@ -11548,16 +11533,6 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/types@3.804.0': - dependencies: - '@smithy/types': 4.3.1 - tslib: 2.8.1 - - '@aws-sdk/types@3.840.0': - dependencies: - '@smithy/types': 4.3.1 - tslib: 2.8.1 - '@aws-sdk/types@3.922.0': dependencies: '@smithy/types': 4.8.1 @@ -12739,9 +12714,9 @@ snapshots: '@open-draft/until@2.1.0': {} - '@openrouter/ai-sdk-provider@2.1.1(ai@6.0.57(zod@3.25.76))(zod@3.25.76)': + '@openrouter/ai-sdk-provider@2.1.1(ai@6.0.77(zod@3.25.76))(zod@3.25.76)': dependencies: - ai: 6.0.57(zod@3.25.76) + ai: 6.0.77(zod@3.25.76) zod: 3.25.76 '@opentelemetry/api-logs@0.208.0': @@ -14106,10 +14081,6 @@ snapshots: dependencies: tslib: 2.8.1 - '@smithy/types@4.3.1': - dependencies: - tslib: 2.8.1 - '@smithy/types@4.8.1': dependencies: tslib: 2.8.1 @@ -15003,7 +14974,7 @@ snapshots: sirv: 3.0.1 tinyglobby: 0.2.14 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.57)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.2.1)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) '@vitest/utils@3.2.4': dependencies: @@ -15153,11 +15124,11 @@ snapshots: dependencies: humanize-ms: 1.2.1 - ai@6.0.57(zod@3.25.76): + ai@6.0.77(zod@3.25.76): dependencies: - '@ai-sdk/gateway': 3.0.25(zod@3.25.76) - '@ai-sdk/provider': 3.0.5 - '@ai-sdk/provider-utils': 4.0.10(zod@3.25.76) + '@ai-sdk/gateway': 3.0.39(zod@3.25.76) + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.14(zod@3.25.76) '@opentelemetry/api': 1.9.0 zod: 3.25.76 @@ -16850,13 +16821,11 @@ snapshots: events@3.3.0: {} - eventsource-parser@3.0.2: {} - eventsource-parser@3.0.6: {} eventsource@3.0.7: dependencies: - eventsource-parser: 3.0.2 + eventsource-parser: 3.0.6 exceljs@4.4.0: dependencies: diff --git a/progress.txt b/progress.txt new file mode 100644 index 0000000000..48c73e5d86 --- /dev/null +++ b/progress.txt @@ -0,0 +1,35 @@ +# Reapply Progress — Batch 2 (reapply/batch-2-minor-conflicts) + +## Status: ✅ READY FOR FORCE PUSH + +## Summary +Batch 2 branch has been rebuilt from scratch on top of origin/main. + +## Changes from Previous Attempt +- **3 delegation PRs removed**: #11379, #11418, #11422 (contained AI SDK contamination) +- Branch rebuilt with clean cherry-picks only + +## Cherry-Picked PRs (9 total) +1. fix: correct Bedrock model ID for Claude Opus 4.6 (#11232) +2. fix: guard against empty-string baseURL (#11233) +3. fix: make defaultTemperature required (#11218) +4. feat: batch consecutive tool calls (#11245) +5. feat: add IPC query handlers (#11279) +6. feat: add lock toggle to pin API config (#11295) +7. fix: validate Gemini thinkingLevel (#11303) +8. chore(cli): prepare release v0.0.53 (#11425) +9. feat: add GLM-5 model support to Z.ai provider (#11440) + +## Post-Cherry-Pick Fixes +- **AI SDK contamination cleaned**: Removed 3 AI SDK tests + import from gemini.spec.ts +- **Type errors fixed**: Added missing `defaultTemperature` to vertex.ts and xai.ts +- **pnpm-lock.yaml regenerated**: Clean lockfile matching current dependencies + +## Verification Results (2026-02-14) +- **Backend tests**: 375 files passed, 5372 tests (4 files skipped, 48 tests skipped) +- **Webview-ui tests**: 120 files passed, 1250 tests (8 tests skipped) +- **TypeScript check**: 14/14 packages clean (all cached) +- **AI SDK contamination check**: CLEAN — no traces of `from "ai"`, `rooMessage`, `@ai-sdk` +- **rooMessage.ts file check**: CLEAN — no such file exists + +## Branch ready for force push to origin/reapply/batch-2-minor-conflicts diff --git a/src/api/providers/__tests__/deepseek.spec.ts b/src/api/providers/__tests__/deepseek.spec.ts index 1aac662d9a..cbbc61ad4d 100644 --- a/src/api/providers/__tests__/deepseek.spec.ts +++ b/src/api/providers/__tests__/deepseek.spec.ts @@ -122,7 +122,7 @@ vi.mock("openai", () => { import OpenAI from "openai" import type { Anthropic } from "@anthropic-ai/sdk" -import { deepSeekDefaultModelId, type ModelInfo } from "@roo-code/types" +import { deepSeekDefaultModelId, DEEP_SEEK_DEFAULT_TEMPERATURE, type ModelInfo } from "@roo-code/types" import type { ApiHandlerOptions } from "../../../shared/api" @@ -279,6 +279,20 @@ describe("DeepSeekHandler", () => { expect(model).toHaveProperty("temperature") expect(model).toHaveProperty("maxTokens") }) + + it("should use DEEP_SEEK_DEFAULT_TEMPERATURE as the default temperature", () => { + const model = handler.getModel() + expect(model.temperature).toBe(DEEP_SEEK_DEFAULT_TEMPERATURE) + }) + + it("should respect user-provided temperature over DEEP_SEEK_DEFAULT_TEMPERATURE", () => { + const handlerWithTemp = new DeepSeekHandler({ + ...mockOptions, + modelTemperature: 0.9, + }) + const model = handlerWithTemp.getModel() + expect(model.temperature).toBe(0.9) + }) }) describe("createMessage", () => { diff --git a/src/api/providers/__tests__/openai-native.spec.ts b/src/api/providers/__tests__/openai-native.spec.ts index 86bb0e9721..ac50e6b0a1 100644 --- a/src/api/providers/__tests__/openai-native.spec.ts +++ b/src/api/providers/__tests__/openai-native.spec.ts @@ -11,6 +11,7 @@ vitest.mock("@roo-code/telemetry", () => ({ })) import { Anthropic } from "@anthropic-ai/sdk" +import OpenAI from "openai" import { ApiProviderError } from "@roo-code/types" @@ -76,6 +77,28 @@ describe("OpenAiNativeHandler", () => { }) expect(handlerWithoutKey).toBeInstanceOf(OpenAiNativeHandler) }) + + it("should pass undefined baseURL when openAiNativeBaseUrl is empty string", () => { + ;(OpenAI as unknown as ReturnType).mockClear() + new OpenAiNativeHandler({ + apiModelId: "gpt-4.1", + openAiNativeApiKey: "test-key", + openAiNativeBaseUrl: "", + }) + expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ baseURL: undefined })) + }) + + it("should pass custom baseURL when openAiNativeBaseUrl is a valid URL", () => { + ;(OpenAI as unknown as ReturnType).mockClear() + new OpenAiNativeHandler({ + apiModelId: "gpt-4.1", + openAiNativeApiKey: "test-key", + openAiNativeBaseUrl: "https://custom-openai.example.com/v1", + }) + expect(OpenAI).toHaveBeenCalledWith( + expect.objectContaining({ baseURL: "https://custom-openai.example.com/v1" }), + ) + }) }) describe("createMessage", () => { diff --git a/src/api/providers/anthropic-vertex.ts b/src/api/providers/anthropic-vertex.ts index 63daf8a3aa..3ed5dd45cc 100644 --- a/src/api/providers/anthropic-vertex.ts +++ b/src/api/providers/anthropic-vertex.ts @@ -231,7 +231,13 @@ export class AnthropicVertexHandler extends BaseProvider implements SingleComple } } - const params = getModelParams({ format: "anthropic", modelId: id, model: info, settings: this.options }) + const params = getModelParams({ + format: "anthropic", + modelId: id, + model: info, + settings: this.options, + defaultTemperature: 0, + }) // Build betas array for request headers const betas: string[] = [] diff --git a/src/api/providers/anthropic.ts b/src/api/providers/anthropic.ts index fc6cc048c7..b2b158f095 100644 --- a/src/api/providers/anthropic.ts +++ b/src/api/providers/anthropic.ts @@ -358,6 +358,7 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa modelId: id, model: info, settings: this.options, + defaultTemperature: 0, }) // The `:thinking` suffix indicates that the model is a "Hybrid" diff --git a/src/api/providers/deepinfra.ts b/src/api/providers/deepinfra.ts index e5b10e4e44..3dc2068372 100644 --- a/src/api/providers/deepinfra.ts +++ b/src/api/providers/deepinfra.ts @@ -47,6 +47,7 @@ export class DeepInfraHandler extends RouterProvider implements SingleCompletion modelId: id, model: info, settings: this.options, + defaultTemperature: 0, }) return { id, info, ...params } diff --git a/src/api/providers/deepseek.ts b/src/api/providers/deepseek.ts index 17ce6e0db7..84cd557de0 100644 --- a/src/api/providers/deepseek.ts +++ b/src/api/providers/deepseek.ts @@ -28,7 +28,7 @@ export class DeepSeekHandler extends OpenAiHandler { ...options, openAiApiKey: options.deepSeekApiKey ?? "not-provided", openAiModelId: options.apiModelId ?? deepSeekDefaultModelId, - openAiBaseUrl: options.deepSeekBaseUrl ?? "https://api.deepseek.com", + openAiBaseUrl: options.deepSeekBaseUrl || "https://api.deepseek.com", openAiStreamingEnabled: true, includeMaxTokens: true, }) @@ -37,7 +37,13 @@ export class DeepSeekHandler extends OpenAiHandler { override getModel() { const id = this.options.apiModelId ?? deepSeekDefaultModelId const info = deepSeekModels[id as keyof typeof deepSeekModels] || deepSeekModels[deepSeekDefaultModelId] - const params = getModelParams({ format: "openai", modelId: id, model: info, settings: this.options }) + const params = getModelParams({ + format: "openai", + modelId: id, + model: info, + settings: this.options, + defaultTemperature: DEEP_SEEK_DEFAULT_TEMPERATURE, + }) return { id, info, ...params } } diff --git a/src/api/providers/doubao.ts b/src/api/providers/doubao.ts index a1337ed558..6490e42208 100644 --- a/src/api/providers/doubao.ts +++ b/src/api/providers/doubao.ts @@ -64,7 +64,13 @@ export class DoubaoHandler extends OpenAiHandler { override getModel() { const id = this.options.apiModelId ?? doubaoDefaultModelId const info = doubaoModels[id as keyof typeof doubaoModels] || doubaoModels[doubaoDefaultModelId] - const params = getModelParams({ format: "openai", modelId: id, model: info, settings: this.options }) + const params = getModelParams({ + format: "openai", + modelId: id, + model: info, + settings: this.options, + defaultTemperature: 0, + }) return { id, info, ...params } } diff --git a/src/api/providers/moonshot.ts b/src/api/providers/moonshot.ts index f7a849cc02..3e90e48f7a 100644 --- a/src/api/providers/moonshot.ts +++ b/src/api/providers/moonshot.ts @@ -15,7 +15,7 @@ export class MoonshotHandler extends OpenAICompatibleHandler { const config: OpenAICompatibleConfig = { providerName: "moonshot", - baseURL: options.moonshotBaseUrl ?? "https://api.moonshot.ai/v1", + baseURL: options.moonshotBaseUrl || "https://api.moonshot.ai/v1", apiKey: options.moonshotApiKey ?? "not-provided", modelId, modelInfo, @@ -29,7 +29,13 @@ export class MoonshotHandler extends OpenAICompatibleHandler { 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 }) + const params = getModelParams({ + format: "openai", + modelId: id, + model: info, + settings: this.options, + defaultTemperature: 0, + }) return { id, info, ...params } } diff --git a/src/api/providers/openai-native.ts b/src/api/providers/openai-native.ts index abf1a562c7..d7c60c5daf 100644 --- a/src/api/providers/openai-native.ts +++ b/src/api/providers/openai-native.ts @@ -87,7 +87,7 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio // Include originator, session_id, and User-Agent headers for API tracking and debugging const userAgent = `roo-code/${Package.version} (${os.platform()} ${os.release()}; ${os.arch()}) node/${process.version.slice(1)}` this.client = new OpenAI({ - baseURL: this.options.openAiNativeBaseUrl, + baseURL: this.options.openAiNativeBaseUrl || undefined, apiKey, defaultHeaders: { originator: "roo-code", diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts index 87589b9396..33b29abcaf 100644 --- a/src/api/providers/openai.ts +++ b/src/api/providers/openai.ts @@ -37,7 +37,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl super() this.options = options - const baseURL = this.options.openAiBaseUrl ?? "https://api.openai.com/v1" + const baseURL = this.options.openAiBaseUrl || "https://api.openai.com/v1" const apiKey = this.options.openAiApiKey ?? "not-provided" const isAzureAiInference = this._isAzureAiInference(this.options.openAiBaseUrl) const urlHost = this._getUrlHost(this.options.openAiBaseUrl) @@ -282,7 +282,13 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl override getModel() { const id = this.options.openAiModelId ?? "" const info: ModelInfo = this.options.openAiCustomModelInfo ?? openAiModelInfoSaneDefaults - const params = getModelParams({ format: "openai", modelId: id, model: info, settings: this.options }) + const params = getModelParams({ + format: "openai", + modelId: id, + model: info, + settings: this.options, + defaultTemperature: 0, + }) return { id, info, ...params } } diff --git a/src/api/providers/requesty.ts b/src/api/providers/requesty.ts index c3b5accbc3..b241c347b0 100644 --- a/src/api/providers/requesty.ts +++ b/src/api/providers/requesty.ts @@ -89,6 +89,7 @@ export class RequestyHandler extends BaseProvider implements SingleCompletionHan modelId: id, model: info, settings: this.options, + defaultTemperature: 0, }) return { id, info, ...params } diff --git a/src/api/providers/unbound.ts b/src/api/providers/unbound.ts index 76dd60d976..ba144f6e1b 100644 --- a/src/api/providers/unbound.ts +++ b/src/api/providers/unbound.ts @@ -70,6 +70,7 @@ export class UnboundHandler extends RouterProvider implements SingleCompletionHa modelId: id, model: info, settings: this.options, + defaultTemperature: 0, }) return { id, info, ...params } diff --git a/src/api/providers/vertex.ts b/src/api/providers/vertex.ts index 2c077d97b7..f470b88e9b 100644 --- a/src/api/providers/vertex.ts +++ b/src/api/providers/vertex.ts @@ -16,7 +16,13 @@ export class VertexHandler extends GeminiHandler implements SingleCompletionHand const modelId = this.options.apiModelId let id = modelId && modelId in vertexModels ? (modelId as VertexModelId) : vertexDefaultModelId const info: ModelInfo = vertexModels[id] - const params = getModelParams({ format: "gemini", modelId: id, model: info, settings: this.options }) + const params = getModelParams({ + format: "gemini", + modelId: id, + model: info, + settings: this.options, + defaultTemperature: info.defaultTemperature ?? 1, + }) // The `:thinking` suffix indicates that the model is a "Hybrid" // reasoning model and that reasoning is required to be enabled. diff --git a/src/api/providers/xai.ts b/src/api/providers/xai.ts index 8df9cc66ec..8b973d41c4 100644 --- a/src/api/providers/xai.ts +++ b/src/api/providers/xai.ts @@ -43,7 +43,13 @@ export class XAIHandler extends BaseProvider implements SingleCompletionHandler : xaiDefaultModelId const info = xaiModels[id] - const params = getModelParams({ format: "openai", modelId: id, model: info, settings: this.options }) + const params = getModelParams({ + format: "openai", + modelId: id, + model: info, + settings: this.options, + defaultTemperature: XAI_DEFAULT_TEMPERATURE, + }) return { id, info, ...params } } diff --git a/src/api/providers/zai.ts b/src/api/providers/zai.ts index a2e3740c56..74e5ea8137 100644 --- a/src/api/providers/zai.ts +++ b/src/api/providers/zai.ts @@ -52,8 +52,8 @@ export class ZAiHandler extends BaseOpenAiCompatibleProvider { ) { const { id: modelId, info } = this.getModel() - // Check if this is a GLM-4.7 model with thinking support - const isThinkingModel = modelId === "glm-4.7" && Array.isArray(info.supportsReasoningEffort) + // Check if this is a model with thinking support (e.g. GLM-4.7, GLM-5) + const isThinkingModel = Array.isArray(info.supportsReasoningEffort) if (isThinkingModel) { // For GLM-4.7, thinking is ON by default in the API. diff --git a/src/api/transform/__tests__/model-params.spec.ts b/src/api/transform/__tests__/model-params.spec.ts index 75b5c50c59..a50f1291be 100644 --- a/src/api/transform/__tests__/model-params.spec.ts +++ b/src/api/transform/__tests__/model-params.spec.ts @@ -17,16 +17,19 @@ describe("getModelParams", () => { const anthropicParams = { modelId: "test", format: "anthropic" as const, + defaultTemperature: 0, } const openaiParams = { modelId: "test", format: "openai" as const, + defaultTemperature: 0, } const openrouterParams = { modelId: "test", format: "openrouter" as const, + defaultTemperature: 0, } describe("Basic functionality", () => { @@ -48,11 +51,12 @@ describe("getModelParams", () => { }) }) - it("should use default temperature of 0 when no defaultTemperature is provided", () => { + it("should use the provided defaultTemperature when no user or model temperature is set", () => { const result = getModelParams({ ...anthropicParams, settings: {}, model: baseModel, + defaultTemperature: 0, }) expect(result.temperature).toBe(0) @@ -193,6 +197,7 @@ describe("getModelParams", () => { format: "openrouter" as const, settings: {}, model: baseModel, + defaultTemperature: 0, }) expect(result.maxTokens).toBe(ANTHROPIC_DEFAULT_MAX_TOKENS) @@ -214,6 +219,7 @@ describe("getModelParams", () => { format: "openrouter" as const, settings: {}, model: baseModel, + defaultTemperature: 0, }) expect(result.maxTokens).toBeUndefined() @@ -374,6 +380,7 @@ describe("getModelParams", () => { format: "gemini" as const, settings: { modelMaxTokens: 2000, modelMaxThinkingTokens: 50 }, model, + defaultTemperature: 0, }), ).toEqual({ format: "gemini", @@ -400,6 +407,7 @@ describe("getModelParams", () => { format: "openrouter" as const, settings: { modelMaxTokens: 4000 }, model, + defaultTemperature: 0, }), ).toEqual({ format: "openrouter", diff --git a/src/api/transform/__tests__/reasoning.spec.ts b/src/api/transform/__tests__/reasoning.spec.ts index 352aac8e7b..0b402c6d55 100644 --- a/src/api/transform/__tests__/reasoning.spec.ts +++ b/src/api/transform/__tests__/reasoning.spec.ts @@ -765,6 +765,7 @@ describe("reasoning.ts", () => { } const result = getGeminiReasoning(options) + // "none" is not a valid GeminiThinkingLevel, so no fallback — returns undefined expect(result).toBeUndefined() }) @@ -838,6 +839,128 @@ describe("reasoning.ts", () => { const result = getGeminiReasoning(options) as GeminiReasoningParams | undefined expect(result).toEqual({ thinkingLevel: "medium", includeThoughts: true }) }) + + it("should fall back to model default when settings effort is not in supportsReasoningEffort array", () => { + // Simulates gemini-3-pro-preview which only supports ["low", "high"] + // but user has reasoningEffort: "medium" from a different model + const geminiModel: ModelInfo = { + ...baseModel, + supportsReasoningEffort: ["low", "high"] as ModelInfo["supportsReasoningEffort"], + reasoningEffort: "low", + } + + const settings: ProviderSettings = { + apiProvider: "gemini", + reasoningEffort: "medium", + } + + const options: GetModelReasoningOptions = { + model: geminiModel, + reasoningBudget: undefined, + reasoningEffort: "medium", + settings, + } + + const result = getGeminiReasoning(options) as GeminiReasoningParams | undefined + // "medium" is not in ["low", "high"], so falls back to model.reasoningEffort "low" + expect(result).toEqual({ thinkingLevel: "low", includeThoughts: true }) + }) + + it("should return undefined when unsupported effort and model default is also invalid", () => { + const geminiModel: ModelInfo = { + ...baseModel, + supportsReasoningEffort: ["low", "high"] as ModelInfo["supportsReasoningEffort"], + // No reasoningEffort default set + } + + const settings: ProviderSettings = { + apiProvider: "gemini", + reasoningEffort: "medium", + } + + const options: GetModelReasoningOptions = { + model: geminiModel, + reasoningBudget: undefined, + reasoningEffort: "medium", + settings, + } + + const result = getGeminiReasoning(options) + // "medium" is not in ["low", "high"], fallback is undefined → returns undefined + expect(result).toBeUndefined() + }) + + it("should pass through effort that IS in the supportsReasoningEffort array", () => { + const geminiModel: ModelInfo = { + ...baseModel, + supportsReasoningEffort: ["low", "high"] as ModelInfo["supportsReasoningEffort"], + reasoningEffort: "low", + } + + const settings: ProviderSettings = { + apiProvider: "gemini", + reasoningEffort: "high", + } + + const options: GetModelReasoningOptions = { + model: geminiModel, + reasoningBudget: undefined, + reasoningEffort: "high", + settings, + } + + const result = getGeminiReasoning(options) as GeminiReasoningParams | undefined + // "high" IS in ["low", "high"], so it should be used directly + expect(result).toEqual({ thinkingLevel: "high", includeThoughts: true }) + }) + + it("should skip validation when supportsReasoningEffort is boolean (not array)", () => { + const geminiModel: ModelInfo = { + ...baseModel, + supportsReasoningEffort: true, + reasoningEffort: "low", + } + + const settings: ProviderSettings = { + apiProvider: "gemini", + reasoningEffort: "medium", + } + + const options: GetModelReasoningOptions = { + model: geminiModel, + reasoningBudget: undefined, + reasoningEffort: "medium", + settings, + } + + const result = getGeminiReasoning(options) as GeminiReasoningParams | undefined + // boolean supportsReasoningEffort should not trigger array validation + expect(result).toEqual({ thinkingLevel: "medium", includeThoughts: true }) + }) + + it("should fall back to model default when settings has 'minimal' but model only supports ['low', 'high']", () => { + const geminiModel: ModelInfo = { + ...baseModel, + supportsReasoningEffort: ["low", "high"] as ModelInfo["supportsReasoningEffort"], + reasoningEffort: "low", + } + + const settings: ProviderSettings = { + apiProvider: "gemini", + reasoningEffort: "minimal", + } + + const options: GetModelReasoningOptions = { + model: geminiModel, + reasoningBudget: undefined, + reasoningEffort: "minimal", + settings, + } + + const result = getGeminiReasoning(options) as GeminiReasoningParams | undefined + // "minimal" is not in ["low", "high"], falls back to "low" + expect(result).toEqual({ thinkingLevel: "low", includeThoughts: true }) + }) }) describe("Integration scenarios", () => { diff --git a/src/api/transform/model-params.ts b/src/api/transform/model-params.ts index e862c5cf5e..ac04bce37d 100644 --- a/src/api/transform/model-params.ts +++ b/src/api/transform/model-params.ts @@ -33,7 +33,7 @@ type GetModelParamsOptions = { modelId: string model: ModelInfo settings: ProviderSettings - defaultTemperature?: number + defaultTemperature: number } type BaseModelParams = { @@ -77,7 +77,7 @@ export function getModelParams({ modelId, model, settings, - defaultTemperature = 0, + defaultTemperature, }: GetModelParamsOptions): ModelParams { const { modelMaxTokens: customMaxTokens, diff --git a/src/api/transform/reasoning.ts b/src/api/transform/reasoning.ts index e726ce3223..446221d256 100644 --- a/src/api/transform/reasoning.ts +++ b/src/api/transform/reasoning.ts @@ -150,10 +150,20 @@ export const getGeminiReasoning = ({ return undefined } + // Validate that the selected effort is supported by this specific model. + // e.g. gemini-3-pro-preview only supports ["low", "high"] — sending + // "medium" (carried over from a different model's settings) causes errors. + const effortToUse = + Array.isArray(model.supportsReasoningEffort) && + isGeminiThinkingLevel(selectedEffort) && + !model.supportsReasoningEffort.includes(selectedEffort) + ? model.reasoningEffort + : selectedEffort + // Effort-based models on Google GenAI support minimal/low/medium/high levels. - if (!isGeminiThinkingLevel(selectedEffort)) { + if (!effortToUse || !isGeminiThinkingLevel(effortToUse)) { return undefined } - return { thinkingLevel: selectedEffort, includeThoughts: true } + return { thinkingLevel: effortToUse, includeThoughts: true } } diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index af9ac3364c..c9417f7226 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -943,7 +943,8 @@ export class ClineProvider // Load the saved API config for the restored mode if it exists. // Skip mode-based profile activation if historyItem.apiConfigName exists, // since the task's specific provider profile will override it anyway. - if (!historyItem.apiConfigName) { + const lockApiConfigAcrossModes = this.context.workspaceState.get("lockApiConfigAcrossModes", false) + if (!historyItem.apiConfigName && !lockApiConfigAcrossModes) { const savedConfigId = await this.providerSettingsManager.getModeConfigId(historyItem.mode) const listApiConfig = await this.providerSettingsManager.listConfig() @@ -1368,6 +1369,13 @@ export class ClineProvider this.emit(RooCodeEventName.ModeChanged, newMode) + // If workspace lock is on, keep the current API config — don't load mode-specific config + const lockApiConfigAcrossModes = this.context.workspaceState.get("lockApiConfigAcrossModes", false) + if (lockApiConfigAcrossModes) { + await this.postStateToWebview() + return + } + // Load the saved API config for the new mode if it exists. const savedConfigId = await this.providerSettingsManager.getModeConfigId(newMode) const listApiConfig = await this.providerSettingsManager.listConfig() @@ -2155,6 +2163,7 @@ export class ClineProvider openRouterImageGenerationSelectedModel, featureRoomoteControlEnabled, isBrowserSessionActive, + lockApiConfigAcrossModes, } = await this.getState() let cloudOrganizations: CloudOrganizationMembership[] = [] @@ -2298,6 +2307,7 @@ export class ClineProvider profileThresholds: profileThresholds ?? {}, cloudApiUrl: getRooCodeApiUrl(), hasOpenedModeSelector: this.getGlobalState("hasOpenedModeSelector") ?? false, + lockApiConfigAcrossModes: lockApiConfigAcrossModes ?? false, alwaysAllowFollowupQuestions: alwaysAllowFollowupQuestions ?? false, followupAutoApproveTimeoutMs: followupAutoApproveTimeoutMs ?? 60000, includeDiagnosticMessages: includeDiagnosticMessages ?? true, @@ -2528,6 +2538,7 @@ export class ClineProvider stateValues.codebaseIndexConfig?.codebaseIndexOpenRouterSpecificProvider, }, profileThresholds: stateValues.profileThresholds ?? {}, + lockApiConfigAcrossModes: this.context.workspaceState.get("lockApiConfigAcrossModes", false), includeDiagnosticMessages: stateValues.includeDiagnosticMessages ?? true, maxDiagnosticMessages: stateValues.maxDiagnosticMessages ?? 50, includeTaskHistoryInEnhance: stateValues.includeTaskHistoryInEnhance ?? true, diff --git a/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts b/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts index 04f5d57792..9e57ae94b8 100644 --- a/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts @@ -171,6 +171,11 @@ describe("ClineProvider - API Handler Rebuild Guard", () => { store: vi.fn().mockImplementation((key: string, value: string | undefined) => (secrets[key] = value)), delete: vi.fn().mockImplementation((key: string) => delete secrets[key]), }, + workspaceState: { + get: vi.fn().mockReturnValue(undefined), + update: vi.fn().mockResolvedValue(undefined), + keys: vi.fn().mockReturnValue([]), + }, subscriptions: [], extension: { packageJSON: { version: "1.0.0" }, diff --git a/src/core/webview/__tests__/ClineProvider.lockApiConfig.spec.ts b/src/core/webview/__tests__/ClineProvider.lockApiConfig.spec.ts new file mode 100644 index 0000000000..9b5e3b16ee --- /dev/null +++ b/src/core/webview/__tests__/ClineProvider.lockApiConfig.spec.ts @@ -0,0 +1,372 @@ +// npx vitest run core/webview/__tests__/ClineProvider.lockApiConfig.spec.ts + +import * as vscode from "vscode" +import { TelemetryService } from "@roo-code/telemetry" +import { ClineProvider } from "../ClineProvider" +import { ContextProxy } from "../../config/ContextProxy" + +vi.mock("vscode", () => ({ + ExtensionContext: vi.fn(), + OutputChannel: vi.fn(), + WebviewView: vi.fn(), + Uri: { + joinPath: vi.fn(), + file: vi.fn(), + }, + CodeActionKind: { + QuickFix: { value: "quickfix" }, + RefactorRewrite: { value: "refactor.rewrite" }, + }, + commands: { + executeCommand: vi.fn().mockResolvedValue(undefined), + }, + window: { + showInformationMessage: vi.fn(), + showWarningMessage: vi.fn(), + showErrorMessage: vi.fn(), + onDidChangeActiveTextEditor: vi.fn(() => ({ dispose: vi.fn() })), + }, + workspace: { + getConfiguration: vi.fn().mockReturnValue({ + get: vi.fn().mockReturnValue([]), + update: vi.fn(), + }), + onDidChangeConfiguration: vi.fn().mockImplementation(() => ({ + dispose: vi.fn(), + })), + onDidSaveTextDocument: vi.fn(() => ({ dispose: vi.fn() })), + onDidChangeTextDocument: vi.fn(() => ({ dispose: vi.fn() })), + onDidOpenTextDocument: vi.fn(() => ({ dispose: vi.fn() })), + onDidCloseTextDocument: vi.fn(() => ({ dispose: vi.fn() })), + }, + env: { + uriScheme: "vscode", + language: "en", + appName: "Visual Studio Code", + }, + ExtensionMode: { + Production: 1, + Development: 2, + Test: 3, + }, + version: "1.85.0", +})) + +vi.mock("../../task/Task", () => ({ + Task: vi.fn().mockImplementation((options) => ({ + taskId: options.taskId || "test-task-id", + saveClineMessages: vi.fn(), + clineMessages: [], + apiConversationHistory: [], + overwriteClineMessages: vi.fn(), + overwriteApiConversationHistory: vi.fn(), + abortTask: vi.fn(), + handleWebviewAskResponse: vi.fn(), + getTaskNumber: vi.fn().mockReturnValue(0), + setTaskNumber: vi.fn(), + setParentTask: vi.fn(), + setRootTask: vi.fn(), + emit: vi.fn(), + parentTask: options.parentTask, + updateApiConfiguration: vi.fn(), + setTaskApiConfigName: vi.fn(), + _taskApiConfigName: options.historyItem?.apiConfigName, + taskApiConfigName: options.historyItem?.apiConfigName, + })), +})) + +vi.mock("../../prompts/sections/custom-instructions") + +vi.mock("../../../utils/safeWriteJson") + +vi.mock("../../../api", () => ({ + buildApiHandler: vi.fn().mockReturnValue({ + getModel: vi.fn().mockReturnValue({ + id: "claude-3-sonnet", + }), + }), +})) + +vi.mock("../../../integrations/workspace/WorkspaceTracker", () => ({ + default: vi.fn().mockImplementation(() => ({ + initializeFilePaths: vi.fn(), + dispose: vi.fn(), + })), +})) + +vi.mock("../../diff/strategies/multi-search-replace", () => ({ + MultiSearchReplaceDiffStrategy: vi.fn().mockImplementation(() => ({ + getName: () => "test-strategy", + applyDiff: vi.fn(), + })), +})) + +vi.mock("@roo-code/cloud", () => ({ + CloudService: { + hasInstance: vi.fn().mockReturnValue(true), + get instance() { + return { + isAuthenticated: vi.fn().mockReturnValue(false), + } + }, + }, + BridgeOrchestrator: { + isEnabled: vi.fn().mockReturnValue(false), + }, + getRooCodeApiUrl: vi.fn().mockReturnValue("https://app.roocode.com"), +})) + +vi.mock("../../../shared/modes", () => { + const mockModes = [ + { + slug: "code", + name: "Code Mode", + roleDefinition: "You are a code assistant", + groups: ["read", "edit", "browser"], + }, + { + slug: "architect", + name: "Architect Mode", + roleDefinition: "You are an architect", + groups: ["read", "edit"], + }, + { + slug: "ask", + name: "Ask Mode", + roleDefinition: "You are an assistant", + groups: ["read"], + }, + { + slug: "debug", + name: "Debug Mode", + roleDefinition: "You are a debugger", + groups: ["read", "edit"], + }, + { + slug: "orchestrator", + name: "Orchestrator Mode", + roleDefinition: "You are an orchestrator", + groups: [], + }, + ] + + return { + modes: mockModes, + getAllModes: vi.fn((customModes?: Array<{ slug: string }>) => { + if (!customModes?.length) { + return [...mockModes] + } + const allModes = [...mockModes] + customModes.forEach((cm) => { + const idx = allModes.findIndex((m) => m.slug === cm.slug) + if (idx !== -1) { + allModes[idx] = cm as (typeof mockModes)[number] + } else { + allModes.push(cm as (typeof mockModes)[number]) + } + }) + return allModes + }), + getModeBySlug: vi.fn().mockReturnValue({ + slug: "code", + name: "Code Mode", + roleDefinition: "You are a code assistant", + groups: ["read", "edit", "browser"], + }), + defaultModeSlug: "code", + } +}) + +vi.mock("../../prompts/system", () => ({ + SYSTEM_PROMPT: vi.fn().mockResolvedValue("mocked system prompt"), + codeMode: "code", +})) + +vi.mock("../../../api/providers/fetchers/modelCache", () => ({ + getModels: vi.fn().mockResolvedValue({}), + flushModels: vi.fn(), +})) + +vi.mock("../../../integrations/misc/extract-text", () => ({ + extractTextFromFile: vi.fn().mockResolvedValue("Mock file content"), +})) + +vi.mock("p-wait-for", () => ({ + default: vi.fn().mockImplementation(async () => Promise.resolve()), +})) + +vi.mock("fs/promises", () => ({ + mkdir: vi.fn().mockResolvedValue(undefined), + writeFile: vi.fn().mockResolvedValue(undefined), + readFile: vi.fn().mockResolvedValue(""), + unlink: vi.fn().mockResolvedValue(undefined), + rmdir: vi.fn().mockResolvedValue(undefined), +})) + +vi.mock("@roo-code/telemetry", () => ({ + TelemetryService: { + hasInstance: vi.fn().mockReturnValue(true), + createInstance: vi.fn(), + get instance() { + return { + trackEvent: vi.fn(), + trackError: vi.fn(), + setProvider: vi.fn(), + captureModeSwitch: vi.fn(), + } + }, + }, +})) + +describe("ClineProvider - Lock API Config Across Modes", () => { + let provider: ClineProvider + let mockContext: vscode.ExtensionContext + let mockOutputChannel: vscode.OutputChannel + let mockWebviewView: vscode.WebviewView + + beforeEach(() => { + vi.clearAllMocks() + + if (!TelemetryService.hasInstance()) { + TelemetryService.createInstance([]) + } + + const globalState: Record = { + mode: "code", + currentApiConfigName: "default-profile", + } + + const workspaceState: Record = {} + + const secrets: Record = {} + + mockContext = { + extensionPath: "/test/path", + extensionUri: {} as vscode.Uri, + globalState: { + get: vi.fn().mockImplementation((key: string) => globalState[key]), + update: vi.fn().mockImplementation((key: string, value: unknown) => { + globalState[key] = value + return Promise.resolve() + }), + keys: vi.fn().mockImplementation(() => Object.keys(globalState)), + }, + secrets: { + get: vi.fn().mockImplementation((key: string) => secrets[key]), + store: vi.fn().mockImplementation((key: string, value: string | undefined) => { + secrets[key] = value + return Promise.resolve() + }), + delete: vi.fn().mockImplementation((key: string) => { + delete secrets[key] + return Promise.resolve() + }), + }, + workspaceState: { + get: vi.fn().mockImplementation((key: string, defaultValue?: unknown) => { + return key in workspaceState ? workspaceState[key] : defaultValue + }), + update: vi.fn().mockImplementation((key: string, value: unknown) => { + workspaceState[key] = value + return Promise.resolve() + }), + keys: vi.fn().mockImplementation(() => Object.keys(workspaceState)), + }, + subscriptions: [], + extension: { + packageJSON: { version: "1.0.0" }, + }, + globalStorageUri: { + fsPath: "/test/storage/path", + }, + } as unknown as vscode.ExtensionContext + + mockOutputChannel = { + appendLine: vi.fn(), + clear: vi.fn(), + dispose: vi.fn(), + } as unknown as vscode.OutputChannel + + const mockPostMessage = vi.fn() + + mockWebviewView = { + webview: { + postMessage: mockPostMessage, + html: "", + options: {}, + onDidReceiveMessage: vi.fn(), + asWebviewUri: vi.fn(), + cspSource: "vscode-webview://test-csp-source", + }, + visible: true, + onDidDispose: vi.fn().mockImplementation((callback) => { + callback() + return { dispose: vi.fn() } + }), + onDidChangeVisibility: vi.fn().mockImplementation(() => ({ dispose: vi.fn() })), + } as unknown as vscode.WebviewView + + provider = new ClineProvider(mockContext, mockOutputChannel, "sidebar", new ContextProxy(mockContext)) + + // Mock getMcpHub method + provider.getMcpHub = vi.fn().mockReturnValue({ + listTools: vi.fn().mockResolvedValue([]), + callTool: vi.fn().mockResolvedValue({ content: [] }), + listResources: vi.fn().mockResolvedValue([]), + readResource: vi.fn().mockResolvedValue({ contents: [] }), + getAllServers: vi.fn().mockReturnValue([]), + }) + }) + + describe("handleModeSwitch honors lockApiConfigAcrossModes as a read-time override", () => { + beforeEach(async () => { + await provider.resolveWebviewView(mockWebviewView) + }) + + it("skips mode-specific config lookup/load when lockApiConfigAcrossModes is true", async () => { + await mockContext.workspaceState.update("lockApiConfigAcrossModes", true) + + const getModeConfigIdSpy = vi + .spyOn(provider.providerSettingsManager, "getModeConfigId") + .mockResolvedValue("architect-profile-id") + const listConfigSpy = vi + .spyOn(provider.providerSettingsManager, "listConfig") + .mockResolvedValue([ + { name: "architect-profile", id: "architect-profile-id", apiProvider: "anthropic" }, + ]) + const activateProviderProfileSpy = vi + .spyOn(provider, "activateProviderProfile") + .mockResolvedValue(undefined) + + await provider.handleModeSwitch("architect") + + expect(getModeConfigIdSpy).not.toHaveBeenCalled() + expect(listConfigSpy).not.toHaveBeenCalled() + expect(activateProviderProfileSpy).not.toHaveBeenCalled() + }) + + it("keeps normal mode-specific lookup/load behavior when lockApiConfigAcrossModes is false", async () => { + await mockContext.workspaceState.update("lockApiConfigAcrossModes", false) + + const getModeConfigIdSpy = vi + .spyOn(provider.providerSettingsManager, "getModeConfigId") + .mockResolvedValue("architect-profile-id") + vi.spyOn(provider.providerSettingsManager, "listConfig").mockResolvedValue([ + { name: "architect-profile", id: "architect-profile-id", apiProvider: "anthropic" }, + ]) + vi.spyOn(provider.providerSettingsManager, "getProfile").mockResolvedValue({ + name: "architect-profile", + apiProvider: "anthropic", + }) + + const activateProviderProfileSpy = vi + .spyOn(provider, "activateProviderProfile") + .mockResolvedValue(undefined) + + await provider.handleModeSwitch("architect") + + expect(getModeConfigIdSpy).toHaveBeenCalledWith("architect") + expect(activateProviderProfileSpy).toHaveBeenCalledWith({ name: "architect-profile" }) + }) + }) +}) diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index 2dec19f90a..4c69746be3 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -406,6 +406,11 @@ describe("ClineProvider", () => { store: vi.fn().mockImplementation((key: string, value: string | undefined) => (secrets[key] = value)), delete: vi.fn().mockImplementation((key: string) => delete secrets[key]), }, + workspaceState: { + get: vi.fn().mockReturnValue(undefined), + update: vi.fn().mockResolvedValue(undefined), + keys: vi.fn().mockReturnValue([]), + }, subscriptions: [], extension: { packageJSON: { version: "1.0.0" }, @@ -2185,6 +2190,11 @@ describe("Project MCP Settings", () => { store: vi.fn(), delete: vi.fn(), }, + workspaceState: { + get: vi.fn().mockReturnValue(undefined), + update: vi.fn().mockResolvedValue(undefined), + keys: vi.fn().mockReturnValue([]), + }, subscriptions: [], extension: { packageJSON: { version: "1.0.0" }, @@ -2315,6 +2325,11 @@ describe.skip("ContextProxy integration", () => { update: vi.fn(), keys: vi.fn().mockReturnValue([]), }, + workspaceState: { + get: vi.fn().mockReturnValue(undefined), + update: vi.fn().mockResolvedValue(undefined), + keys: vi.fn().mockReturnValue([]), + }, secrets: { get: vi.fn(), store: vi.fn(), delete: vi.fn() }, extensionUri: {} as vscode.Uri, globalStorageUri: { fsPath: "/test/path" }, @@ -2380,6 +2395,11 @@ describe("getTelemetryProperties", () => { update: vi.fn(), keys: vi.fn().mockReturnValue([]), }, + workspaceState: { + get: vi.fn().mockReturnValue(undefined), + update: vi.fn().mockResolvedValue(undefined), + keys: vi.fn().mockReturnValue([]), + }, secrets: { get: vi.fn(), store: vi.fn(), delete: vi.fn() }, extensionUri: {} as vscode.Uri, globalStorageUri: { fsPath: "/test/path" }, @@ -2542,6 +2562,11 @@ describe("ClineProvider - Router Models", () => { store: vi.fn().mockImplementation((key: string, value: string | undefined) => (secrets[key] = value)), delete: vi.fn().mockImplementation((key: string) => delete secrets[key]), }, + workspaceState: { + get: vi.fn().mockReturnValue(undefined), + update: vi.fn().mockResolvedValue(undefined), + keys: vi.fn().mockReturnValue([]), + }, subscriptions: [], extension: { packageJSON: { version: "1.0.0" }, @@ -2895,6 +2920,11 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => { store: vi.fn().mockImplementation((key: string, value: string | undefined) => (secrets[key] = value)), delete: vi.fn().mockImplementation((key: string) => delete secrets[key]), }, + workspaceState: { + get: vi.fn().mockReturnValue(undefined), + update: vi.fn().mockResolvedValue(undefined), + keys: vi.fn().mockReturnValue([]), + }, subscriptions: [], extension: { packageJSON: { version: "1.0.0" }, diff --git a/src/core/webview/__tests__/ClineProvider.sticky-mode.spec.ts b/src/core/webview/__tests__/ClineProvider.sticky-mode.spec.ts index 27aab0b7da..af674d7a5e 100644 --- a/src/core/webview/__tests__/ClineProvider.sticky-mode.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.sticky-mode.spec.ts @@ -227,6 +227,11 @@ describe("ClineProvider - Sticky Mode", () => { return Promise.resolve() }), }, + workspaceState: { + get: vi.fn().mockReturnValue(undefined), + update: vi.fn().mockResolvedValue(undefined), + keys: vi.fn().mockReturnValue([]), + }, subscriptions: [], extension: { packageJSON: { version: "1.0.0" }, diff --git a/src/core/webview/__tests__/ClineProvider.sticky-profile.spec.ts b/src/core/webview/__tests__/ClineProvider.sticky-profile.spec.ts index 80b14746a7..ee63b45b25 100644 --- a/src/core/webview/__tests__/ClineProvider.sticky-profile.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.sticky-profile.spec.ts @@ -229,6 +229,11 @@ describe("ClineProvider - Sticky Provider Profile", () => { return Promise.resolve() }), }, + workspaceState: { + get: vi.fn().mockReturnValue(undefined), + update: vi.fn().mockResolvedValue(undefined), + keys: vi.fn().mockReturnValue([]), + }, subscriptions: [], extension: { packageJSON: { version: "1.0.0" }, diff --git a/src/core/webview/__tests__/ClineProvider.taskHistory.spec.ts b/src/core/webview/__tests__/ClineProvider.taskHistory.spec.ts index 0cf8e6c89b..aefed79744 100644 --- a/src/core/webview/__tests__/ClineProvider.taskHistory.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.taskHistory.spec.ts @@ -287,6 +287,11 @@ describe("ClineProvider Task History Synchronization", () => { store: vi.fn().mockImplementation((key: string, value: string | undefined) => (secrets[key] = value)), delete: vi.fn().mockImplementation((key: string) => delete secrets[key]), }, + workspaceState: { + get: vi.fn().mockReturnValue(undefined), + update: vi.fn().mockResolvedValue(undefined), + keys: vi.fn().mockReturnValue([]), + }, subscriptions: [], extension: { packageJSON: { version: "1.0.0" }, diff --git a/src/core/webview/__tests__/webviewMessageHandler.lockApiConfig.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.lockApiConfig.spec.ts new file mode 100644 index 0000000000..fd9b4a7740 --- /dev/null +++ b/src/core/webview/__tests__/webviewMessageHandler.lockApiConfig.spec.ts @@ -0,0 +1,68 @@ +// npx vitest run core/webview/__tests__/webviewMessageHandler.lockApiConfig.spec.ts + +import { webviewMessageHandler } from "../webviewMessageHandler" +import type { ClineProvider } from "../ClineProvider" + +describe("webviewMessageHandler - lockApiConfigAcrossModes", () => { + let mockProvider: { + context: { + workspaceState: { + get: ReturnType + update: ReturnType + } + } + getState: ReturnType + postStateToWebview: ReturnType + providerSettingsManager: { + setModeConfig: ReturnType + } + postMessageToWebview: ReturnType + getCurrentTask: ReturnType + } + + beforeEach(() => { + vi.clearAllMocks() + + mockProvider = { + context: { + workspaceState: { + get: vi.fn(), + update: vi.fn().mockResolvedValue(undefined), + }, + }, + getState: vi.fn().mockResolvedValue({ + currentApiConfigName: "test-config", + listApiConfigMeta: [{ name: "test-config", id: "config-123" }], + customModes: [], + }), + postStateToWebview: vi.fn(), + providerSettingsManager: { + setModeConfig: vi.fn(), + }, + postMessageToWebview: vi.fn(), + getCurrentTask: vi.fn(), + } + }) + + it("sets lockApiConfigAcrossModes to true and posts state without mode config fan-out", async () => { + await webviewMessageHandler(mockProvider as unknown as ClineProvider, { + type: "lockApiConfigAcrossModes", + bool: true, + }) + + expect(mockProvider.context.workspaceState.update).toHaveBeenCalledWith("lockApiConfigAcrossModes", true) + expect(mockProvider.providerSettingsManager.setModeConfig).not.toHaveBeenCalled() + expect(mockProvider.postStateToWebview).toHaveBeenCalled() + }) + + it("sets lockApiConfigAcrossModes to false without applying to all modes", async () => { + await webviewMessageHandler(mockProvider as unknown as ClineProvider, { + type: "lockApiConfigAcrossModes", + bool: false, + }) + + expect(mockProvider.context.workspaceState.update).toHaveBeenCalledWith("lockApiConfigAcrossModes", false) + expect(mockProvider.providerSettingsManager.setModeConfig).not.toHaveBeenCalled() + expect(mockProvider.postStateToWebview).toHaveBeenCalled() + }) +}) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 3d1afa918f..b66e3403f7 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1653,6 +1653,14 @@ export const webviewMessageHandler = async ( await provider.postStateToWebview() break + case "lockApiConfigAcrossModes": { + const enabled = message.bool ?? false + await provider.context.workspaceState.update("lockApiConfigAcrossModes", enabled) + + await provider.postStateToWebview() + break + } + case "toggleApiConfigPin": if (message.text) { const currentPinned = getGlobalState("pinnedApiConfigs") ?? {} diff --git a/src/extension/api.ts b/src/extension/api.ts index aa889da73f..25c81a6589 100644 --- a/src/extension/api.ts +++ b/src/extension/api.ts @@ -21,10 +21,13 @@ import { IpcMessageType, } from "@roo-code/types" import { IpcServer } from "@roo-code/ipc" +import { CloudService } from "@roo-code/cloud" import { Package } from "../shared/package" import { ClineProvider } from "../core/webview/ClineProvider" import { openClineInNewTab } from "../activate/registerCommands" +import { getCommands } from "../services/command/commands" +import { getModels } from "../api/providers/fetchers/modelCache" export class API extends EventEmitter implements RooCodeAPI { private readonly outputChannel: vscode.OutputChannel @@ -65,7 +68,15 @@ export class API extends EventEmitter implements RooCodeAPI { ipc.listen() this.log(`[API] ipc server started: socketPath=${socketPath}, pid=${process.pid}, ppid=${process.ppid}`) - ipc.on(IpcMessageType.TaskCommand, async (_clientId, command) => { + ipc.on(IpcMessageType.TaskCommand, async (clientId, command) => { + const sendResponse = (eventName: RooCodeEventName, payload: unknown[]) => { + ipc.send(clientId, { + type: IpcMessageType.TaskEvent, + origin: IpcOrigin.Server, + data: { eventName, payload } as TaskEvent, + }) + } + switch (command.commandName) { case TaskCommandName.StartNewTask: this.log( @@ -89,13 +100,56 @@ export class API extends EventEmitter implements RooCodeAPI { } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) this.log(`[API] ResumeTask failed for taskId ${command.data}: ${errorMessage}`) - // Don't rethrow - we want to prevent IPC server crashes - // The error is logged for debugging purposes + // Don't rethrow - we want to prevent IPC server crashes. + // The error is logged for debugging purposes. } break case TaskCommandName.SendMessage: this.log(`[API] SendMessage -> ${command.data.text}`) await this.sendMessage(command.data.text, command.data.images) + break + case TaskCommandName.GetCommands: + try { + const commands = await getCommands(this.sidebarProvider.cwd) + + sendResponse(RooCodeEventName.CommandsResponse, [ + commands.map((cmd) => ({ + name: cmd.name, + source: cmd.source, + filePath: cmd.filePath, + description: cmd.description, + argumentHint: cmd.argumentHint, + })), + ]) + } catch (error) { + sendResponse(RooCodeEventName.CommandsResponse, [[]]) + } + + break + case TaskCommandName.GetModes: + try { + const modes = await this.sidebarProvider.getModes() + sendResponse(RooCodeEventName.ModesResponse, [modes]) + } catch (error) { + sendResponse(RooCodeEventName.ModesResponse, [[]]) + } + + break + case TaskCommandName.GetModels: + try { + const models = await getModels({ + provider: "roo" as const, + baseUrl: process.env.ROO_CODE_PROVIDER_URL ?? "https://api.roocode.com/proxy", + apiKey: CloudService.hasInstance() + ? CloudService.instance.authService?.getSessionToken() + : undefined, + }) + + sendResponse(RooCodeEventName.ModelsResponse, [models]) + } catch (error) { + sendResponse(RooCodeEventName.ModelsResponse, [{}]) + } + break } }) diff --git a/webview-ui/src/components/chat/ApiConfigSelector.tsx b/webview-ui/src/components/chat/ApiConfigSelector.tsx index 4396019a2d..e370296ec3 100644 --- a/webview-ui/src/components/chat/ApiConfigSelector.tsx +++ b/webview-ui/src/components/chat/ApiConfigSelector.tsx @@ -20,6 +20,8 @@ interface ApiConfigSelectorProps { listApiConfigMeta: Array<{ id: string; name: string; modelId?: string }> pinnedApiConfigs?: Record togglePinnedApiConfig: (id: string) => void + lockApiConfigAcrossModes: boolean + onToggleLockApiConfig: () => void } export const ApiConfigSelector = ({ @@ -32,6 +34,8 @@ export const ApiConfigSelector = ({ listApiConfigMeta, pinnedApiConfigs, togglePinnedApiConfig, + lockApiConfigAcrossModes, + onToggleLockApiConfig, }: ApiConfigSelectorProps) => { const { t } = useAppTranslation() const [open, setOpen] = useState(false) @@ -228,6 +232,16 @@ export const ApiConfigSelector = ({ onClick={handleEditClick} tooltip={false} /> + {/* Info icon and title on the right with matching spacing */} diff --git a/webview-ui/src/components/chat/BatchDiffApproval.tsx b/webview-ui/src/components/chat/BatchDiffApproval.tsx index a88914cd88..f128e4310d 100644 --- a/webview-ui/src/components/chat/BatchDiffApproval.tsx +++ b/webview-ui/src/components/chat/BatchDiffApproval.tsx @@ -35,12 +35,12 @@ export const BatchDiffApproval = memo(({ files = [], ts }: BatchDiffApprovalProp return (
- {files.map((file) => { + {files.map((file, index) => { // Use backend-provided unified diff only. Stats also provided by backend. const unified = file.content || "" return ( -
+
{/* Individual files */}
- {files.map((file) => { + {files.map((file, index) => { return ( -
+
vscode.postMessage({ type: "openFile", text: file.content })}> diff --git a/webview-ui/src/components/chat/BatchListFilesPermission.tsx b/webview-ui/src/components/chat/BatchListFilesPermission.tsx new file mode 100644 index 0000000000..a5d08c244b --- /dev/null +++ b/webview-ui/src/components/chat/BatchListFilesPermission.tsx @@ -0,0 +1,45 @@ +import { memo } from "react" + +import { ToolUseBlock, ToolUseBlockHeader } from "../common/ToolUseBlock" +import { PathTooltip } from "../ui/PathTooltip" + +interface DirPermissionItem { + path: string + key: string +} + +interface BatchListFilesPermissionProps { + dirs: DirPermissionItem[] + ts: number +} + +export const BatchListFilesPermission = memo(({ dirs = [], ts }: BatchListFilesPermissionProps) => { + if (!dirs?.length) { + return null + } + + return ( +
+
+ {dirs.map((dir, index) => { + return ( +
+ + + + + {dir.path} + + +
+
+
+
+ ) + })} +
+
+ ) +}) + +BatchListFilesPermission.displayName = "BatchListFilesPermission" diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index 654f2e1011..4c0b2bbfd0 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -103,6 +103,7 @@ export const ChatTextArea = forwardRef( commands, cloudUserInfo, enterBehavior, + lockApiConfigAcrossModes, } = useExtensionState() // Find the ID and display text for the currently selected API configuration. @@ -945,6 +946,11 @@ export const ChatTextArea = forwardRef( vscode.postMessage({ type: "loadApiConfigurationById", text: value }) }, []) + const handleToggleLockApiConfig = useCallback(() => { + const newValue = !lockApiConfigAcrossModes + vscode.postMessage({ type: "lockApiConfigAcrossModes", bool: newValue }) + }, [lockApiConfigAcrossModes]) + return (
( listApiConfigMeta={listApiConfigMeta || []} pinnedApiConfigs={pinnedApiConfigs} togglePinnedApiConfig={togglePinnedApiConfig} + lockApiConfigAcrossModes={!!lockApiConfigAcrossModes} + onToggleLockApiConfig={handleToggleLockApiConfig} />
diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 21ef29874a..52b4a3703b 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -11,6 +11,7 @@ import { Trans } from "react-i18next" import { useDebounceEffect } from "@src/utils/useDebounceEffect" import { appendImages } from "@src/utils/imageUtils" import { getCostBreakdownIfNeeded } from "@src/utils/costFormatting" +import { batchConsecutive } from "@src/utils/batchConsecutive" import type { ClineAsk, ClineSayTool, ClineMessage, ExtensionMessage, AudioType } from "@roo-code/types" @@ -70,8 +71,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction { - const w = window as any - return w.AUDIO_BASE_URI || "" + return (window as unknown as { AUDIO_BASE_URI?: string }).AUDIO_BASE_URI || "" }) const { t } = useAppTranslation() @@ -318,6 +318,14 @@ const ChatViewComponent: React.ForwardRefRenderFunction 1) { - // Create a synthetic batch message - const batchFiles = batch.map((batchMsg) => { - try { - const tool = JSON.parse(batchMsg.text || "{}") - return { - path: tool.path || "", - lineSnippet: tool.reason || "", - isOutsideWorkspace: tool.isOutsideWorkspace || false, - key: `${tool.path}${tool.reason ? ` (${tool.reason})` : ""}`, - content: tool.content || "", - } - } catch { - return { path: "", lineSnippet: "", key: "", content: "" } - } - }) - - // Use the first message as the base, but add batchFiles - const firstTool = JSON.parse(msg.text || "{}") - const syntheticMessage: ClineMessage = { - ...msg, - text: JSON.stringify({ - ...firstTool, - batchFiles, - }), - // Store original messages for response handling - _batchedMessages: batch, - } as ClineMessage & { _batchedMessages: ClineMessage[] } - - result.push(syntheticMessage) - i = j // Skip past all batched messages - } else { - // Single read_file ask, keep as-is - result.push(msg) - i++ - } - } else { - result.push(msg) - i++ + // Helper to check if a message is a list_files ask that should be batched + const isListFilesAsk = (msg: ClineMessage): boolean => { + if (msg.type !== "ask" || msg.ask !== "tool") return false + try { + const tool = JSON.parse(msg.text || "{}") + return ( + (tool.tool === "listFilesTopLevel" || tool.tool === "listFilesRecursive") && !tool.batchDirs // Don't re-batch already batched + ) + } catch { + return false } } + // Set of tool names that represent file-editing operations + const editFileTools = new Set([ + "editedExistingFile", + "appliedDiff", + "newFileCreated", + "insertContent", + "searchAndReplace", + ]) + + // Helper to check if a message is a file-edit ask that should be batched + const isEditFileAsk = (msg: ClineMessage): boolean => { + if (msg.type !== "ask" || msg.ask !== "tool") return false + try { + const tool = JSON.parse(msg.text || "{}") + return editFileTools.has(tool.tool) && !tool.batchDiffs // Don't re-batch already batched + } catch { + return false + } + } + + // Synthesize a batch of consecutive read_file asks into a single message + const synthesizeReadFileBatch = (batch: ClineMessage[]): ClineMessage => { + const batchFiles = batch.map((batchMsg) => { + try { + const tool = JSON.parse(batchMsg.text || "{}") + return { + path: tool.path || "", + lineSnippet: tool.reason || "", + isOutsideWorkspace: tool.isOutsideWorkspace || false, + key: `${tool.path}${tool.reason ? ` (${tool.reason})` : ""}`, + content: tool.content || "", + } + } catch { + return { path: "", lineSnippet: "", key: "", content: "" } + } + }) + + let firstTool + try { + firstTool = JSON.parse(batch[0].text || "{}") + } catch { + return batch[0] + } + return { + ...batch[0], + text: JSON.stringify({ ...firstTool, batchFiles }), + } + } + + // Synthesize a batch of consecutive list_files asks into a single message + const synthesizeListFilesBatch = (batch: ClineMessage[]): ClineMessage => { + const batchDirs = batch.map((batchMsg) => { + try { + const tool = JSON.parse(batchMsg.text || "{}") + return { + path: tool.path || "", + recursive: tool.tool === "listFilesRecursive", + isOutsideWorkspace: tool.isOutsideWorkspace || false, + key: tool.path || "", + } + } catch { + return { path: "", recursive: false, key: "" } + } + }) + + let firstTool + try { + firstTool = JSON.parse(batch[0].text || "{}") + } catch { + return batch[0] + } + return { + ...batch[0], + text: JSON.stringify({ ...firstTool, batchDirs }), + } + } + + // Synthesize a batch of consecutive file-edit asks into a single message + const synthesizeEditFileBatch = (batch: ClineMessage[]): ClineMessage => { + const batchDiffs = batch.map((batchMsg) => { + try { + const tool = JSON.parse(batchMsg.text || "{}") + return { + path: tool.path || "", + changeCount: 1, + key: tool.path || "", + content: tool.content || tool.diff || "", + diffStats: tool.diffStats, + } + } catch { + return { path: "", changeCount: 0, key: "", content: "" } + } + }) + + let firstTool + try { + firstTool = JSON.parse(batch[0].text || "{}") + } catch { + return batch[0] + } + return { + ...batch[0], + text: JSON.stringify({ ...firstTool, batchDiffs }), + } + } + + // Consolidate consecutive ask messages into batches + const readFileBatched = batchConsecutive(filtered, isReadFileAsk, synthesizeReadFileBatch) + const listFilesBatched = batchConsecutive(readFileBatched, isListFilesAsk, synthesizeListFilesBatch) + const result = batchConsecutive(listFilesBatched, isEditFileAsk, synthesizeEditFileBatch) + if (isCondensing) { result.push({ type: "say", say: "condense_context", ts: Date.now(), partial: true, - } as any) + } as ClineMessage) } return result }, [isCondensing, visibleMessages, isBrowserSessionMessage]) @@ -1263,9 +1347,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction { return () => { - if (scrollToBottomSmooth && typeof (scrollToBottomSmooth as any).cancel === "function") { - ;(scrollToBottomSmooth as any).cancel() - } + scrollToBottomSmooth.clear() } }, [scrollToBottomSmooth]) diff --git a/webview-ui/src/components/chat/__tests__/ApiConfigSelector.spec.tsx b/webview-ui/src/components/chat/__tests__/ApiConfigSelector.spec.tsx index ff1b95f949..a71216d96f 100644 --- a/webview-ui/src/components/chat/__tests__/ApiConfigSelector.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ApiConfigSelector.spec.tsx @@ -72,6 +72,8 @@ describe("ApiConfigSelector", () => { ], pinnedApiConfigs: { config1: true }, togglePinnedApiConfig: mockTogglePinnedApiConfig, + lockApiConfigAcrossModes: false, + onToggleLockApiConfig: vi.fn(), } beforeEach(() => { diff --git a/webview-ui/src/components/chat/__tests__/BatchListFilesPermission.spec.tsx b/webview-ui/src/components/chat/__tests__/BatchListFilesPermission.spec.tsx new file mode 100644 index 0000000000..21ea05192f --- /dev/null +++ b/webview-ui/src/components/chat/__tests__/BatchListFilesPermission.spec.tsx @@ -0,0 +1,103 @@ +import { render, screen } from "@/utils/test-utils" + +import { TranslationProvider } from "@/i18n/__mocks__/TranslationContext" + +import { BatchListFilesPermission } from "../BatchListFilesPermission" + +describe("BatchListFilesPermission", () => { + const mockDirs = [ + { + key: "apps/cli", + path: "apps/cli", + }, + { + key: "apps/web-roo-code", + path: "apps/web-roo-code", + }, + { + key: "packages/core", + path: "packages/core", + }, + ] + + beforeEach(() => { + vi.clearAllMocks() + }) + + it("renders directory list correctly", () => { + render( + + + , + ) + + expect(screen.getByText("apps/cli")).toBeInTheDocument() + expect(screen.getByText("apps/web-roo-code")).toBeInTheDocument() + expect(screen.getByText("packages/core")).toBeInTheDocument() + }) + + it("renders nothing when dirs array is empty", () => { + const { container } = render( + + + , + ) + + expect(container.firstChild).toBeNull() + }) + + it("re-renders when timestamp changes", () => { + const { rerender } = render( + + + , + ) + + expect(screen.getByText("apps/cli")).toBeInTheDocument() + + rerender( + + + , + ) + + expect(screen.getByText("apps/cli")).toBeInTheDocument() + }) + + it("renders all directories in a single container", () => { + render( + + + , + ) + + // All directories should be within a single bordered container + const container = screen.getByText("apps/cli").closest(".border.border-border.rounded-md") + expect(container).toBeInTheDocument() + + // All 3 dirs should be inside this container + expect(container?.querySelectorAll(".flex.items-center.gap-2")).toHaveLength(mockDirs.length) + }) + + it("renders a single directory", () => { + const singleDir = [ + { + key: "apps/cli", + path: "apps/cli", + }, + ] + + render( + + + , + ) + + expect(screen.getByText("apps/cli")).toBeInTheDocument() + + // Single directory should still be rendered inside the container + const bordered = screen.getByText("apps/cli").closest(".border.border-border.rounded-md") + expect(bordered).toBeInTheDocument() + expect(bordered?.querySelectorAll(".flex.items-center.gap-2")).toHaveLength(1) + }) +}) diff --git a/webview-ui/src/components/chat/__tests__/ChatTextArea.lockApiConfig.spec.tsx b/webview-ui/src/components/chat/__tests__/ChatTextArea.lockApiConfig.spec.tsx new file mode 100644 index 0000000000..d3fb2b6890 --- /dev/null +++ b/webview-ui/src/components/chat/__tests__/ChatTextArea.lockApiConfig.spec.tsx @@ -0,0 +1,156 @@ +import { defaultModeSlug } from "@roo/modes" + +import { render, fireEvent, screen } from "@src/utils/test-utils" +import { useExtensionState } from "@src/context/ExtensionStateContext" +import { vscode } from "@src/utils/vscode" + +import { ChatTextArea } from "../ChatTextArea" + +vi.mock("@src/utils/vscode", () => ({ + vscode: { + postMessage: vi.fn(), + }, +})) + +vi.mock("@src/components/common/CodeBlock") +vi.mock("@src/components/common/MarkdownBlock") +vi.mock("@src/utils/path-mentions", () => ({ + convertToMentionPath: vi.fn((path: string) => path), +})) + +// Mock ExtensionStateContext +vi.mock("@src/context/ExtensionStateContext") + +const mockPostMessage = vscode.postMessage as ReturnType + +describe("ChatTextArea - lockApiConfigAcrossModes toggle", () => { + const defaultProps = { + inputValue: "", + setInputValue: vi.fn(), + onSend: vi.fn(), + sendingDisabled: false, + selectApiConfigDisabled: false, + onSelectImages: vi.fn(), + shouldDisableImages: false, + placeholderText: "Type a message...", + selectedImages: [] as string[], + setSelectedImages: vi.fn(), + onHeightChange: vi.fn(), + mode: defaultModeSlug, + setMode: vi.fn(), + modeShortcutText: "(⌘. for next mode)", + } + + const defaultState = { + filePaths: [], + openedTabs: [], + apiConfiguration: { apiProvider: "anthropic" }, + taskHistory: [], + cwd: "/test/workspace", + listApiConfigMeta: [{ id: "default", name: "Default", modelId: "claude-3" }], + currentApiConfigName: "Default", + pinnedApiConfigs: {}, + togglePinnedApiConfig: vi.fn(), + } + + beforeEach(() => { + vi.clearAllMocks() + }) + + /** + * Helper: Opens the ApiConfigSelector popover by clicking the trigger, + * then returns the lock toggle button by its aria-label. + */ + const openPopoverAndGetLockToggle = (ariaLabel: string) => { + const trigger = screen.getByTestId("dropdown-trigger") + fireEvent.click(trigger) + return screen.getByRole("button", { name: ariaLabel }) + } + + describe("rendering", () => { + it("renders with muted opacity when lockApiConfigAcrossModes is false", () => { + ;(useExtensionState as ReturnType).mockReturnValue({ + ...defaultState, + lockApiConfigAcrossModes: false, + }) + + render() + + const button = openPopoverAndGetLockToggle("chat:lockApiConfigAcrossModes") + expect(button).toBeInTheDocument() + // Unlocked state has muted opacity + expect(button.className).toContain("opacity-60") + expect(button.className).not.toContain("text-vscode-focusBorder") + }) + + it("renders with highlight color when lockApiConfigAcrossModes is true", () => { + ;(useExtensionState as ReturnType).mockReturnValue({ + ...defaultState, + lockApiConfigAcrossModes: true, + }) + + render() + + const button = openPopoverAndGetLockToggle("chat:unlockApiConfigAcrossModes") + expect(button).toBeInTheDocument() + // Locked state has the focus border highlight color + expect(button.className).toContain("text-vscode-focusBorder") + expect(button.className).not.toContain("opacity-60") + }) + + it("renders in unlocked state when lockApiConfigAcrossModes is undefined (default)", () => { + ;(useExtensionState as ReturnType).mockReturnValue({ + ...defaultState, + }) + + render() + + const button = openPopoverAndGetLockToggle("chat:lockApiConfigAcrossModes") + expect(button).toBeInTheDocument() + // Default (undefined/falsy) renders in unlocked style + expect(button.className).toContain("opacity-60") + }) + }) + + describe("interaction", () => { + it("posts lockApiConfigAcrossModes=true message when locking", () => { + ;(useExtensionState as ReturnType).mockReturnValue({ + ...defaultState, + lockApiConfigAcrossModes: false, + }) + + render() + + // Clear any initialization messages + mockPostMessage.mockClear() + + const button = openPopoverAndGetLockToggle("chat:lockApiConfigAcrossModes") + fireEvent.click(button) + + expect(mockPostMessage).toHaveBeenCalledWith({ + type: "lockApiConfigAcrossModes", + bool: true, + }) + }) + + it("posts lockApiConfigAcrossModes=false message when unlocking", () => { + ;(useExtensionState as ReturnType).mockReturnValue({ + ...defaultState, + lockApiConfigAcrossModes: true, + }) + + render() + + // Clear any initialization messages + mockPostMessage.mockClear() + + const button = openPopoverAndGetLockToggle("chat:unlockApiConfigAcrossModes") + fireEvent.click(button) + + expect(mockPostMessage).toHaveBeenCalledWith({ + type: "lockApiConfigAcrossModes", + bool: false, + }) + }) + }) +}) diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index 9bbc4ca9b5..dbcd592fc1 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -277,6 +277,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode openRouterImageGenerationSelectedModel: "", includeCurrentTime: true, includeCurrentCost: true, + lockApiConfigAcrossModes: false, }) const [didHydrateState, setDidHydrateState] = useState(false) diff --git a/webview-ui/src/i18n/locales/ca/chat.json b/webview-ui/src/i18n/locales/ca/chat.json index 4cadb61368..4c3dcae0d5 100644 --- a/webview-ui/src/i18n/locales/ca/chat.json +++ b/webview-ui/src/i18n/locales/ca/chat.json @@ -111,6 +111,8 @@ }, "selectMode": "Selecciona el mode d'interacció", "selectApiConfig": "Seleccioneu la configuració de l'API", + "lockApiConfigAcrossModes": "Bloqueja la configuració de l'API a tots els modes en aquest espai de treball", + "unlockApiConfigAcrossModes": "La configuració de l'API està bloquejada a tots els modes en aquest espai de treball (fes clic per desbloquejar)", "enhancePrompt": "Millora la sol·licitud amb context addicional", "addImages": "Afegeix imatges al missatge", "sendMessage": "Envia el missatge", @@ -218,7 +220,8 @@ "wantsToViewTopLevelOutsideWorkspace": "Roo vol veure els fitxers de nivell superior en aquest directori (fora de l'espai de treball)", "didViewTopLevelOutsideWorkspace": "Roo ha vist els fitxers de nivell superior en aquest directori (fora de l'espai de treball)", "wantsToViewRecursiveOutsideWorkspace": "Roo vol veure recursivament tots els fitxers en aquest directori (fora de l'espai de treball)", - "didViewRecursiveOutsideWorkspace": "Roo ha vist recursivament tots els fitxers en aquest directori (fora de l'espai de treball)" + "didViewRecursiveOutsideWorkspace": "Roo ha vist recursivament tots els fitxers en aquest directori (fora de l'espai de treball)", + "wantsToViewMultipleDirectories": "Roo vol veure diversos directoris" }, "commandOutput": "Sortida de la comanda", "commandExecution": { @@ -437,6 +440,22 @@ "title": "Denegar tot" } }, + "list-batch": { + "approve": { + "title": "Aprovar tot" + }, + "deny": { + "title": "Denegar tot" + } + }, + "edit-batch": { + "approve": { + "title": "Desar tot" + }, + "deny": { + "title": "Denegar tot" + } + }, "indexingStatus": { "ready": "Índex preparat", "indexing": "Indexant {{percentage}}%", diff --git a/webview-ui/src/i18n/locales/de/chat.json b/webview-ui/src/i18n/locales/de/chat.json index 5883bd4769..c031509956 100644 --- a/webview-ui/src/i18n/locales/de/chat.json +++ b/webview-ui/src/i18n/locales/de/chat.json @@ -111,6 +111,8 @@ }, "selectMode": "Interaktionsmodus auswählen", "selectApiConfig": "API-Konfiguration auswählen", + "lockApiConfigAcrossModes": "API-Konfiguration für alle Modi in diesem Arbeitsbereich sperren", + "unlockApiConfigAcrossModes": "API-Konfiguration ist für alle Modi in diesem Arbeitsbereich gesperrt (klicke zum Entsperren)", "enhancePrompt": "Prompt mit zusätzlichem Kontext verbessern", "addImages": "Bilder zur Nachricht hinzufügen", "sendMessage": "Nachricht senden", @@ -218,7 +220,8 @@ "wantsToViewTopLevelOutsideWorkspace": "Roo möchte die Dateien auf oberster Ebene in diesem Verzeichnis (außerhalb des Arbeitsbereichs) anzeigen", "didViewTopLevelOutsideWorkspace": "Roo hat die Dateien auf oberster Ebene in diesem Verzeichnis (außerhalb des Arbeitsbereichs) angezeigt", "wantsToViewRecursiveOutsideWorkspace": "Roo möchte rekursiv alle Dateien in diesem Verzeichnis (außerhalb des Arbeitsbereichs) anzeigen", - "didViewRecursiveOutsideWorkspace": "Roo hat rekursiv alle Dateien in diesem Verzeichnis (außerhalb des Arbeitsbereichs) angezeigt" + "didViewRecursiveOutsideWorkspace": "Roo hat rekursiv alle Dateien in diesem Verzeichnis (außerhalb des Arbeitsbereichs) angezeigt", + "wantsToViewMultipleDirectories": "Roo möchte mehrere Verzeichnisse anzeigen" }, "commandOutput": "Befehlsausgabe", "commandExecution": { @@ -437,6 +440,22 @@ "title": "Alle ablehnen" } }, + "list-batch": { + "approve": { + "title": "Alle genehmigen" + }, + "deny": { + "title": "Alle ablehnen" + } + }, + "edit-batch": { + "approve": { + "title": "Alle speichern" + }, + "deny": { + "title": "Alle ablehnen" + } + }, "indexingStatus": { "ready": "Index bereit", "indexing": "Indizierung {{percentage}}%", diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json index 9aa491915b..3cb19572dd 100644 --- a/webview-ui/src/i18n/locales/en/chat.json +++ b/webview-ui/src/i18n/locales/en/chat.json @@ -74,6 +74,22 @@ "title": "Deny All" } }, + "list-batch": { + "approve": { + "title": "Approve All" + }, + "deny": { + "title": "Deny All" + } + }, + "edit-batch": { + "approve": { + "title": "Save All" + }, + "deny": { + "title": "Deny All" + } + }, "runCommand": { "title": "Run", "tooltip": "Execute this command" @@ -122,6 +138,8 @@ }, "selectMode": "Select mode for interaction", "selectApiConfig": "Select API configuration", + "lockApiConfigAcrossModes": "Lock API configuration across all modes in this workspace", + "unlockApiConfigAcrossModes": "API configuration is locked across all modes in this workspace (click to unlock)", "enhancePrompt": "Enhance prompt with additional context", "modeSelector": { "title": "Modes", @@ -235,6 +253,7 @@ "didViewRecursive": "Roo recursively viewed all files in this directory", "wantsToViewRecursiveOutsideWorkspace": "Roo wants to recursively view all files in this directory (outside workspace)", "didViewRecursiveOutsideWorkspace": "Roo recursively viewed all files in this directory (outside workspace)", + "wantsToViewMultipleDirectories": "Roo wants to view multiple directories", "wantsToSearch": "Roo wants to search this directory for {{regex}}", "didSearch": "Roo searched this directory for {{regex}}", "wantsToSearchOutsideWorkspace": "Roo wants to search this directory (outside workspace) for {{regex}}", diff --git a/webview-ui/src/i18n/locales/es/chat.json b/webview-ui/src/i18n/locales/es/chat.json index 58af7ae9a8..2fdbe08c62 100644 --- a/webview-ui/src/i18n/locales/es/chat.json +++ b/webview-ui/src/i18n/locales/es/chat.json @@ -111,6 +111,8 @@ }, "selectMode": "Seleccionar modo de interacción", "selectApiConfig": "Seleccionar configuración de API", + "lockApiConfigAcrossModes": "Bloquear la configuración de API en todos los modos de este espacio de trabajo", + "unlockApiConfigAcrossModes": "La configuración de API está bloqueada en todos los modos de este espacio de trabajo (clic para desbloquear)", "enhancePrompt": "Mejorar el mensaje con contexto adicional", "addImages": "Agregar imágenes al mensaje", "sendMessage": "Enviar mensaje", @@ -218,7 +220,8 @@ "wantsToViewTopLevelOutsideWorkspace": "Roo quiere ver los archivos de nivel superior en este directorio (fuera del espacio de trabajo)", "didViewTopLevelOutsideWorkspace": "Roo vio los archivos de nivel superior en este directorio (fuera del espacio de trabajo)", "wantsToViewRecursiveOutsideWorkspace": "Roo quiere ver recursivamente todos los archivos en este directorio (fuera del espacio de trabajo)", - "didViewRecursiveOutsideWorkspace": "Roo vio recursivamente todos los archivos en este directorio (fuera del espacio de trabajo)" + "didViewRecursiveOutsideWorkspace": "Roo vio recursivamente todos los archivos en este directorio (fuera del espacio de trabajo)", + "wantsToViewMultipleDirectories": "Roo quiere ver varios directorios" }, "commandOutput": "Salida del comando", "commandExecution": { @@ -437,6 +440,22 @@ "title": "Denegar todo" } }, + "list-batch": { + "approve": { + "title": "Aprobar todo" + }, + "deny": { + "title": "Denegar todo" + } + }, + "edit-batch": { + "approve": { + "title": "Guardar todo" + }, + "deny": { + "title": "Denegar todo" + } + }, "indexingStatus": { "ready": "Índice listo", "indexing": "Indexando {{percentage}}%", diff --git a/webview-ui/src/i18n/locales/fr/chat.json b/webview-ui/src/i18n/locales/fr/chat.json index 0e6b198db8..b0fe94f8fb 100644 --- a/webview-ui/src/i18n/locales/fr/chat.json +++ b/webview-ui/src/i18n/locales/fr/chat.json @@ -111,6 +111,8 @@ }, "selectMode": "Sélectionner le mode d'interaction", "selectApiConfig": "Sélectionner la configuration de l'API", + "lockApiConfigAcrossModes": "Verrouiller la configuration API pour tous les modes dans cet espace de travail", + "unlockApiConfigAcrossModes": "La configuration API est verrouillée pour tous les modes dans cet espace de travail (cliquer pour déverrouiller)", "enhancePrompt": "Améliorer la requête avec un contexte supplémentaire", "addImages": "Ajouter des images au message", "sendMessage": "Envoyer le message", @@ -218,7 +220,8 @@ "wantsToViewTopLevelOutsideWorkspace": "Roo veut voir les fichiers de premier niveau dans ce répertoire (hors espace de travail)", "didViewTopLevelOutsideWorkspace": "Roo a vu les fichiers de premier niveau dans ce répertoire (hors espace de travail)", "wantsToViewRecursiveOutsideWorkspace": "Roo veut voir récursivement tous les fichiers dans ce répertoire (hors espace de travail)", - "didViewRecursiveOutsideWorkspace": "Roo a vu récursivement tous les fichiers dans ce répertoire (hors espace de travail)" + "didViewRecursiveOutsideWorkspace": "Roo a vu récursivement tous les fichiers dans ce répertoire (hors espace de travail)", + "wantsToViewMultipleDirectories": "Roo veut voir plusieurs répertoires" }, "commandOutput": "Sortie de la Commande", "commandExecution": { @@ -437,6 +440,22 @@ "title": "Tout refuser" } }, + "list-batch": { + "approve": { + "title": "Tout approuver" + }, + "deny": { + "title": "Tout refuser" + } + }, + "edit-batch": { + "approve": { + "title": "Tout enregistrer" + }, + "deny": { + "title": "Tout refuser" + } + }, "indexingStatus": { "ready": "Index prêt", "indexing": "Indexation {{percentage}}%", diff --git a/webview-ui/src/i18n/locales/hi/chat.json b/webview-ui/src/i18n/locales/hi/chat.json index 53e6dc1cb4..a16c13958a 100644 --- a/webview-ui/src/i18n/locales/hi/chat.json +++ b/webview-ui/src/i18n/locales/hi/chat.json @@ -111,6 +111,8 @@ }, "selectMode": "इंटरैक्शन मोड चुनें", "selectApiConfig": "एपीआई कॉन्फ़िगरेशन का चयन करें", + "lockApiConfigAcrossModes": "इस कार्यक्षेत्र में सभी मोड के लिए API कॉन्फ़िगरेशन लॉक करें", + "unlockApiConfigAcrossModes": "इस कार्यक्षेत्र में सभी मोड के लिए API कॉन्फ़िगरेशन लॉक है (अनलॉक करने के लिए क्लिक करें)", "enhancePrompt": "अतिरिक्त संदर्भ के साथ प्रॉम्प्ट बढ़ाएँ", "addImages": "संदेश में चित्र जोड़ें", "sendMessage": "संदेश भेजें", @@ -218,7 +220,8 @@ "wantsToViewTopLevelOutsideWorkspace": "Roo इस निर्देशिका (कार्यक्षेत्र के बाहर) में शीर्ष स्तर की फ़ाइलें देखना चाहता है", "didViewTopLevelOutsideWorkspace": "Roo ने इस निर्देशिका (कार्यक्षेत्र के बाहर) में शीर्ष स्तर की फ़ाइलें देखीं", "wantsToViewRecursiveOutsideWorkspace": "Roo इस निर्देशिका (कार्यक्षेत्र के बाहर) में सभी फ़ाइलों को पुनरावर्ती रूप से देखना चाहता है", - "didViewRecursiveOutsideWorkspace": "Roo ने इस निर्देशिका (कार्यक्षेत्र के बाहर) में सभी फ़ाइलों को पुनरावर्ती रूप से देखा" + "didViewRecursiveOutsideWorkspace": "Roo ने इस निर्देशिका (कार्यक्षेत्र के बाहर) में सभी फ़ाइलों को पुनरावर्ती रूप से देखा", + "wantsToViewMultipleDirectories": "Roo कई डायरेक्ट्रीज़ देखना चाहता है" }, "commandOutput": "कमांड आउटपुट", "commandExecution": { @@ -437,6 +440,22 @@ "title": "सभी अस्वीकार करें" } }, + "list-batch": { + "approve": { + "title": "सभी स्वीकृत करें" + }, + "deny": { + "title": "सभी अस्वीकार करें" + } + }, + "edit-batch": { + "approve": { + "title": "सभी सहेजें" + }, + "deny": { + "title": "सभी अस्वीकार करें" + } + }, "indexingStatus": { "ready": "इंडेक्स तैयार", "indexing": "इंडेक्सिंग {{percentage}}%", diff --git a/webview-ui/src/i18n/locales/id/chat.json b/webview-ui/src/i18n/locales/id/chat.json index 6201bbe21c..53c524e66f 100644 --- a/webview-ui/src/i18n/locales/id/chat.json +++ b/webview-ui/src/i18n/locales/id/chat.json @@ -80,6 +80,22 @@ "title": "Tolak Semua" } }, + "list-batch": { + "approve": { + "title": "Setujui Semua" + }, + "deny": { + "title": "Tolak Semua" + } + }, + "edit-batch": { + "approve": { + "title": "Simpan Semua" + }, + "deny": { + "title": "Tolak Semua" + } + }, "runCommand": { "title": "Perintah", "tooltip": "Jalankan perintah ini" @@ -125,6 +141,8 @@ }, "selectMode": "Pilih mode untuk interaksi", "selectApiConfig": "Pilih konfigurasi API", + "lockApiConfigAcrossModes": "Kunci konfigurasi API di semua mode dalam workspace ini", + "unlockApiConfigAcrossModes": "Konfigurasi API terkunci di semua mode dalam workspace ini (klik untuk membuka kunci)", "enhancePrompt": "Tingkatkan prompt dengan konteks tambahan", "enhancePromptDescription": "Tombol 'Tingkatkan Prompt' membantu memperbaiki prompt kamu dengan memberikan konteks tambahan, klarifikasi, atau penyusunan ulang. Coba ketik prompt di sini dan klik tombol lagi untuk melihat cara kerjanya.", "modeSelector": { @@ -246,7 +264,8 @@ "wantsToViewTopLevelOutsideWorkspace": "Roo ingin melihat file tingkat atas di direktori ini (di luar workspace)", "didViewTopLevelOutsideWorkspace": "Roo melihat file tingkat atas di direktori ini (di luar workspace)", "wantsToViewRecursiveOutsideWorkspace": "Roo ingin melihat semua file secara rekursif di direktori ini (di luar workspace)", - "didViewRecursiveOutsideWorkspace": "Roo melihat semua file secara rekursif di direktori ini (di luar workspace)" + "didViewRecursiveOutsideWorkspace": "Roo melihat semua file secara rekursif di direktori ini (di luar workspace)", + "wantsToViewMultipleDirectories": "Roo ingin melihat beberapa direktori" }, "codebaseSearch": { "wantsToSearch": "Roo ingin mencari codebase untuk {{query}}", diff --git a/webview-ui/src/i18n/locales/it/chat.json b/webview-ui/src/i18n/locales/it/chat.json index e6cbe1402e..06a21b9b12 100644 --- a/webview-ui/src/i18n/locales/it/chat.json +++ b/webview-ui/src/i18n/locales/it/chat.json @@ -111,6 +111,8 @@ }, "selectMode": "Seleziona modalità di interazione", "selectApiConfig": "Seleziona la configurazione API", + "lockApiConfigAcrossModes": "Blocca la configurazione API per tutte le modalità in questo workspace", + "unlockApiConfigAcrossModes": "La configurazione API è bloccata per tutte le modalità in questo workspace (clicca per sbloccare)", "enhancePrompt": "Migliora prompt con contesto aggiuntivo", "addImages": "Aggiungi immagini al messaggio", "sendMessage": "Invia messaggio", @@ -218,7 +220,8 @@ "wantsToViewTopLevelOutsideWorkspace": "Roo vuole visualizzare i file di primo livello in questa directory (fuori dall'area di lavoro)", "didViewTopLevelOutsideWorkspace": "Roo ha visualizzato i file di primo livello in questa directory (fuori dall'area di lavoro)", "wantsToViewRecursiveOutsideWorkspace": "Roo vuole visualizzare ricorsivamente tutti i file in questa directory (fuori dall'area di lavoro)", - "didViewRecursiveOutsideWorkspace": "Roo ha visualizzato ricorsivamente tutti i file in questa directory (fuori dall'area di lavoro)" + "didViewRecursiveOutsideWorkspace": "Roo ha visualizzato ricorsivamente tutti i file in questa directory (fuori dall'area di lavoro)", + "wantsToViewMultipleDirectories": "Roo vuole visualizzare più directory" }, "commandOutput": "Output del Comando", "commandExecution": { @@ -437,6 +440,22 @@ "title": "Nega tutto" } }, + "list-batch": { + "approve": { + "title": "Approva tutto" + }, + "deny": { + "title": "Nega tutto" + } + }, + "edit-batch": { + "approve": { + "title": "Salva tutto" + }, + "deny": { + "title": "Nega tutto" + } + }, "indexingStatus": { "ready": "Indice pronto", "indexing": "Indicizzazione {{percentage}}%", diff --git a/webview-ui/src/i18n/locales/ja/chat.json b/webview-ui/src/i18n/locales/ja/chat.json index 1b3295c671..de904b1214 100644 --- a/webview-ui/src/i18n/locales/ja/chat.json +++ b/webview-ui/src/i18n/locales/ja/chat.json @@ -111,6 +111,8 @@ }, "selectMode": "対話モードを選択", "selectApiConfig": "API構成を選択", + "lockApiConfigAcrossModes": "このワークスペースのすべてのモードでAPI構成をロック", + "unlockApiConfigAcrossModes": "このワークスペースのすべてのモードでAPI構成がロックされています(クリックで解除)", "enhancePrompt": "追加コンテキストでプロンプトを強化", "addImages": "メッセージに画像を追加", "sendMessage": "メッセージを送信", @@ -218,7 +220,8 @@ "wantsToViewTopLevelOutsideWorkspace": "Rooはこのディレクトリ(ワークスペース外)のトップレベルファイルを表示したい", "didViewTopLevelOutsideWorkspace": "Rooはこのディレクトリ(ワークスペース外)のトップレベルファイルを表示しました", "wantsToViewRecursiveOutsideWorkspace": "Rooはこのディレクトリ(ワークスペース外)のすべてのファイルを再帰的に表示したい", - "didViewRecursiveOutsideWorkspace": "Rooはこのディレクトリ(ワークスペース外)のすべてのファイルを再帰的に表示しました" + "didViewRecursiveOutsideWorkspace": "Rooはこのディレクトリ(ワークスペース外)のすべてのファイルを再帰的に表示しました", + "wantsToViewMultipleDirectories": "Roo は複数のディレクトリを表示したい" }, "commandOutput": "コマンド出力", "commandExecution": { @@ -437,6 +440,22 @@ "title": "すべて拒否" } }, + "list-batch": { + "approve": { + "title": "すべて承認" + }, + "deny": { + "title": "すべて拒否" + } + }, + "edit-batch": { + "approve": { + "title": "すべて保存" + }, + "deny": { + "title": "すべて拒否" + } + }, "indexingStatus": { "ready": "インデックス準備完了", "indexing": "インデックス作成中 {{percentage}}%", diff --git a/webview-ui/src/i18n/locales/ko/chat.json b/webview-ui/src/i18n/locales/ko/chat.json index ac0f0080ca..00f91779e5 100644 --- a/webview-ui/src/i18n/locales/ko/chat.json +++ b/webview-ui/src/i18n/locales/ko/chat.json @@ -111,6 +111,8 @@ }, "selectMode": "상호작용 모드 선택", "selectApiConfig": "API 구성 선택", + "lockApiConfigAcrossModes": "이 워크스페이스의 모든 모드에서 API 구성 잠금", + "unlockApiConfigAcrossModes": "이 워크스페이스의 모든 모드에서 API 구성이 잠겨 있습니다 (클릭하여 해제)", "enhancePrompt": "추가 컨텍스트로 프롬프트 향상", "addImages": "메시지에 이미지 추가", "sendMessage": "메시지 보내기", @@ -218,7 +220,8 @@ "wantsToViewTopLevelOutsideWorkspace": "Roo가 이 디렉토리(워크스페이스 외부)의 최상위 파일을 보고 싶어합니다", "didViewTopLevelOutsideWorkspace": "Roo가 이 디렉토리(워크스페이스 외부)의 최상위 파일을 보았습니다", "wantsToViewRecursiveOutsideWorkspace": "Roo가 이 디렉토리(워크스페이스 외부)의 모든 파일을 재귀적으로 보고 싶어합니다", - "didViewRecursiveOutsideWorkspace": "Roo가 이 디렉토리(워크스페이스 외부)의 모든 파일을 재귀적으로 보았습니다" + "didViewRecursiveOutsideWorkspace": "Roo가 이 디렉토리(워크스페이스 외부)의 모든 파일을 재귀적으로 보았습니다", + "wantsToViewMultipleDirectories": "Roo가 여러 디렉토리를 보려고 합니다" }, "commandOutput": "명령 출력", "commandExecution": { @@ -437,6 +440,22 @@ "title": "모두 거부" } }, + "list-batch": { + "approve": { + "title": "모두 승인" + }, + "deny": { + "title": "모두 거부" + } + }, + "edit-batch": { + "approve": { + "title": "모두 저장" + }, + "deny": { + "title": "모두 거부" + } + }, "indexingStatus": { "ready": "인덱스 준비됨", "indexing": "인덱싱 중 {{percentage}}%", diff --git a/webview-ui/src/i18n/locales/nl/chat.json b/webview-ui/src/i18n/locales/nl/chat.json index e982ccf70d..978574f3bd 100644 --- a/webview-ui/src/i18n/locales/nl/chat.json +++ b/webview-ui/src/i18n/locales/nl/chat.json @@ -111,6 +111,8 @@ }, "selectMode": "Selecteer modus voor interactie", "selectApiConfig": "Selecteer API-configuratie", + "lockApiConfigAcrossModes": "API-configuratie vergrendelen voor alle modi in deze werkruimte", + "unlockApiConfigAcrossModes": "API-configuratie is vergrendeld voor alle modi in deze werkruimte (klik om te ontgrendelen)", "enhancePrompt": "Prompt verbeteren met extra context", "enhancePromptDescription": "De knop 'Prompt verbeteren' helpt je prompt te verbeteren door extra context, verduidelijking of herformulering te bieden. Probeer hier een prompt te typen en klik opnieuw op de knop om te zien hoe het werkt.", "modeSelector": { @@ -145,14 +147,14 @@ "rateLimitWait": "Snelheidsbeperking", "errorTitle": "Fout van provider {{code}}", "errorMessage": { - "docs": "Documentatie", - "goToSettings": "Instellingen", "400": "De provider kon het verzoek niet verwerken zoals ingediend. Stop de taak en probeer een ander benadering.", "401": "Kon niet authenticeren met provider. Controleer je API-sleutelconfiguratie.", "402": "Het lijkt erop dat je funds/credits op je account op zijn. Ga naar je provider en voeg meer toe om door te gaan.", "403": "Niet geautoriseerd. Je API-sleutel is geldig, maar de provider weigerde dit verzoek in te willigen.", "429": "Te veel verzoeken. Je bent rate-gelimiteerd door de provider. Wacht alsjeblieft even voor je volgende API-aanroep.", "500": "Provider-serverfout. Er is iets mis aan de kant van de provider, er is niets mis met je verzoek.", + "docs": "Documentatie", + "goToSettings": "Instellingen", "unknown": "Onbekende API-fout. Neem alsjeblieft contact op met Roo Code-ondersteuning.", "connection": "Verbindingsfout. Zorg ervoor dat je een werkende internetverbinding hebt.", "claudeCodeNotAuthenticated": "Je moet inloggen om Claude Code te gebruiken. Ga naar Instellingen en klik op \"Inloggen bij Claude Code\" om te authenticeren." @@ -213,7 +215,8 @@ "wantsToViewTopLevelOutsideWorkspace": "Roo wil de bovenliggende bestanden in deze map (buiten werkruimte) bekijken", "didViewTopLevelOutsideWorkspace": "Roo heeft de bovenliggende bestanden in deze map (buiten werkruimte) bekeken", "wantsToViewRecursiveOutsideWorkspace": "Roo wil alle bestanden in deze map (buiten werkruimte) recursief bekijken", - "didViewRecursiveOutsideWorkspace": "Roo heeft alle bestanden in deze map (buiten werkruimte) recursief bekeken" + "didViewRecursiveOutsideWorkspace": "Roo heeft alle bestanden in deze map (buiten werkruimte) recursief bekeken", + "wantsToViewMultipleDirectories": "Roo wil meerdere mappen bekijken" }, "commandOutput": "Commando-uitvoer", "commandExecution": { @@ -437,6 +440,22 @@ "title": "Alles weigeren" } }, + "list-batch": { + "approve": { + "title": "Alles goedkeuren" + }, + "deny": { + "title": "Alles weigeren" + } + }, + "edit-batch": { + "approve": { + "title": "Alles opslaan" + }, + "deny": { + "title": "Alles weigeren" + } + }, "indexingStatus": { "ready": "Index gereed", "indexing": "Indexeren {{percentage}}%", diff --git a/webview-ui/src/i18n/locales/pl/chat.json b/webview-ui/src/i18n/locales/pl/chat.json index 3935ef9450..b520a63e6d 100644 --- a/webview-ui/src/i18n/locales/pl/chat.json +++ b/webview-ui/src/i18n/locales/pl/chat.json @@ -111,6 +111,8 @@ }, "selectMode": "Wybierz tryb interakcji", "selectApiConfig": "Wybierz konfigurację API", + "lockApiConfigAcrossModes": "Zablokuj konfigurację API dla wszystkich trybów w tym obszarze roboczym", + "unlockApiConfigAcrossModes": "Konfiguracja API jest zablokowana dla wszystkich trybów w tym obszarze roboczym (kliknij, aby odblokować)", "enhancePrompt": "Ulepsz podpowiedź dodatkowym kontekstem", "addImages": "Dodaj obrazy do wiadomości", "sendMessage": "Wyślij wiadomość", @@ -150,14 +152,14 @@ "rateLimitWait": "Ograniczenie szybkości", "errorTitle": "Błąd dostawcy {{code}}", "errorMessage": { - "docs": "Dokumentacja", - "goToSettings": "Ustawienia", "400": "Dostawca nie mógł przetworzyć żądania. Zatrzymaj zadanie i spróbuj innego podejścia.", "401": "Nie można uwierzytelnić u dostawcy. Sprawdź konfigurację klucza API.", "402": "Wygląda na to, że wyczerpałeś środki/kredyty na swoim koncie. Przejdź do dostawcy i dodaj więcej, aby kontynuować.", "403": "Brak autoryzacji. Twój klucz API jest ważny, ale dostawca odmówił ukończenia tego żądania.", "429": "Zbyt wiele żądań. Dostawca ogranicza Ci szybkość żądań. Poczekaj chwilę przed następnym wywołaniem API.", "500": "Błąd serwera dostawcy. Po stronie dostawcy coś się nie powiodło, w Twoim żądaniu nie ma nic złego.", + "docs": "Dokumentacja", + "goToSettings": "Ustawienia", "unknown": "Nieznany błąd API. Skontaktuj się z pomocą techniczną Roo Code.", "connection": "Błąd połączenia. Upewnij się, że masz działające połączenie internetowe.", "claudeCodeNotAuthenticated": "Musisz się zalogować, aby korzystać z Claude Code. Przejdź do Ustawień i kliknij \"Zaloguj się do Claude Code\", aby się uwierzytelnić." @@ -218,7 +220,8 @@ "wantsToViewTopLevelOutsideWorkspace": "Roo chce zobaczyć pliki najwyższego poziomu w tym katalogu (poza obszarem roboczym)", "didViewTopLevelOutsideWorkspace": "Roo zobaczył pliki najwyższego poziomu w tym katalogu (poza obszarem roboczym)", "wantsToViewRecursiveOutsideWorkspace": "Roo chce rekurencyjnie zobaczyć wszystkie pliki w tym katalogu (poza obszarem roboczym)", - "didViewRecursiveOutsideWorkspace": "Roo rekurencyjnie zobaczył wszystkie pliki w tym katalogu (poza obszarem roboczym)" + "didViewRecursiveOutsideWorkspace": "Roo rekurencyjnie zobaczył wszystkie pliki w tym katalogu (poza obszarem roboczym)", + "wantsToViewMultipleDirectories": "Roo chce wyświetlić wiele katalogów" }, "commandOutput": "Wyjście polecenia", "commandExecution": { @@ -437,6 +440,22 @@ "title": "Odrzuć wszystko" } }, + "list-batch": { + "approve": { + "title": "Zatwierdź wszystko" + }, + "deny": { + "title": "Odrzuć wszystko" + } + }, + "edit-batch": { + "approve": { + "title": "Zapisz wszystko" + }, + "deny": { + "title": "Odrzuć wszystko" + } + }, "indexingStatus": { "ready": "Indeks gotowy", "indexing": "Indeksowanie {{percentage}}%", diff --git a/webview-ui/src/i18n/locales/pt-BR/chat.json b/webview-ui/src/i18n/locales/pt-BR/chat.json index ce6b9cda10..bf03b3a529 100644 --- a/webview-ui/src/i18n/locales/pt-BR/chat.json +++ b/webview-ui/src/i18n/locales/pt-BR/chat.json @@ -111,6 +111,8 @@ }, "selectMode": "Selecionar modo de interação", "selectApiConfig": "Selecionar configuração da API", + "lockApiConfigAcrossModes": "Bloquear configuração da API em todos os modos neste workspace", + "unlockApiConfigAcrossModes": "A configuração da API está bloqueada em todos os modos neste workspace (clique para desbloquear)", "enhancePrompt": "Aprimorar prompt com contexto adicional", "addImages": "Adicionar imagens à mensagem", "sendMessage": "Enviar mensagem", @@ -218,7 +220,8 @@ "wantsToViewTopLevelOutsideWorkspace": "Roo quer visualizar os arquivos de nível superior neste diretório (fora do espaço de trabalho)", "didViewTopLevelOutsideWorkspace": "Roo visualizou os arquivos de nível superior neste diretório (fora do espaço de trabalho)", "wantsToViewRecursiveOutsideWorkspace": "Roo quer visualizar recursivamente todos os arquivos neste diretório (fora do espaço de trabalho)", - "didViewRecursiveOutsideWorkspace": "Roo visualizou recursivamente todos os arquivos neste diretório (fora do espaço de trabalho)" + "didViewRecursiveOutsideWorkspace": "Roo visualizou recursivamente todos os arquivos neste diretório (fora do espaço de trabalho)", + "wantsToViewMultipleDirectories": "Roo quer visualizar vários diretórios" }, "commandOutput": "Saída do comando", "commandExecution": { @@ -437,6 +440,22 @@ "title": "Negar tudo" } }, + "list-batch": { + "approve": { + "title": "Aprovar tudo" + }, + "deny": { + "title": "Negar tudo" + } + }, + "edit-batch": { + "approve": { + "title": "Salvar tudo" + }, + "deny": { + "title": "Negar tudo" + } + }, "indexingStatus": { "ready": "Índice pronto", "indexing": "Indexando {{percentage}}%", diff --git a/webview-ui/src/i18n/locales/ru/chat.json b/webview-ui/src/i18n/locales/ru/chat.json index fa7c66fc0f..0c68bbd7e8 100644 --- a/webview-ui/src/i18n/locales/ru/chat.json +++ b/webview-ui/src/i18n/locales/ru/chat.json @@ -111,6 +111,8 @@ }, "selectMode": "Выберите режим взаимодействия", "selectApiConfig": "Выберите конфигурацию API", + "lockApiConfigAcrossModes": "Заблокировать конфигурацию API для всех режимов в этом рабочем пространстве", + "unlockApiConfigAcrossModes": "Конфигурация API заблокирована для всех режимов в этом рабочем пространстве (нажми, чтобы разблокировать)", "enhancePrompt": "Улучшить запрос с дополнительным контекстом", "enhancePromptDescription": "Кнопка 'Улучшить запрос' помогает сделать ваш запрос лучше, предоставляя дополнительный контекст, уточнения или переформулировку. Попробуйте ввести запрос и снова нажать кнопку, чтобы увидеть, как это работает.", "modeSelector": { @@ -213,7 +215,8 @@ "wantsToViewTopLevelOutsideWorkspace": "Roo хочет просмотреть файлы верхнего уровня в этой директории (вне рабочего пространства)", "didViewTopLevelOutsideWorkspace": "Roo просмотрел файлы верхнего уровня в этой директории (вне рабочего пространства)", "wantsToViewRecursiveOutsideWorkspace": "Roo хочет рекурсивно просмотреть все файлы в этой директории (вне рабочего пространства)", - "didViewRecursiveOutsideWorkspace": "Roo рекурсивно просмотрел все файлы в этой директории (вне рабочего пространства)" + "didViewRecursiveOutsideWorkspace": "Roo рекурсивно просмотрел все файлы в этой директории (вне рабочего пространства)", + "wantsToViewMultipleDirectories": "Roo хочет просмотреть несколько директорий" }, "commandOutput": "Вывод команды", "commandExecution": { @@ -438,6 +441,22 @@ "title": "Отклонить все" } }, + "list-batch": { + "approve": { + "title": "Одобрить все" + }, + "deny": { + "title": "Отклонить все" + } + }, + "edit-batch": { + "approve": { + "title": "Сохранить все" + }, + "deny": { + "title": "Отклонить все" + } + }, "indexingStatus": { "ready": "Индекс готов", "indexing": "Индексация {{percentage}}%", diff --git a/webview-ui/src/i18n/locales/tr/chat.json b/webview-ui/src/i18n/locales/tr/chat.json index 5b9bb3ebe0..0ffdb54c48 100644 --- a/webview-ui/src/i18n/locales/tr/chat.json +++ b/webview-ui/src/i18n/locales/tr/chat.json @@ -111,6 +111,8 @@ }, "selectMode": "Etkileşim modunu seçin", "selectApiConfig": "API yapılandırmasını seçin", + "lockApiConfigAcrossModes": "Bu çalışma alanındaki tüm modlarda API yapılandırmasını kilitle", + "unlockApiConfigAcrossModes": "Bu çalışma alanındaki tüm modlarda API yapılandırması kilitli (kilidi açmak için tıkla)", "enhancePrompt": "Ek bağlamla istemi geliştir", "addImages": "Mesaja resim ekle", "sendMessage": "Mesaj gönder", @@ -218,7 +220,8 @@ "wantsToViewTopLevelOutsideWorkspace": "Roo bu dizindeki (çalışma alanı dışında) üst düzey dosyaları görüntülemek istiyor", "didViewTopLevelOutsideWorkspace": "Roo bu dizindeki (çalışma alanı dışında) üst düzey dosyaları görüntüledi", "wantsToViewRecursiveOutsideWorkspace": "Roo bu dizindeki (çalışma alanı dışında) tüm dosyaları özyinelemeli olarak görüntülemek istiyor", - "didViewRecursiveOutsideWorkspace": "Roo bu dizindeki (çalışma alanı dışında) tüm dosyaları özyinelemeli olarak görüntüledi" + "didViewRecursiveOutsideWorkspace": "Roo bu dizindeki (çalışma alanı dışında) tüm dosyaları özyinelemeli olarak görüntüledi", + "wantsToViewMultipleDirectories": "Roo birden fazla dizini görüntülemek istiyor" }, "commandOutput": "Komut Çıktısı", "commandExecution": { @@ -438,6 +441,22 @@ "title": "Tümünü Reddet" } }, + "list-batch": { + "approve": { + "title": "Tümünü Onayla" + }, + "deny": { + "title": "Tümünü Reddet" + } + }, + "edit-batch": { + "approve": { + "title": "Tümünü Kaydet" + }, + "deny": { + "title": "Tümünü Reddet" + } + }, "indexingStatus": { "ready": "İndeks hazır", "indexing": "İndeksleniyor {{percentage}}%", diff --git a/webview-ui/src/i18n/locales/vi/chat.json b/webview-ui/src/i18n/locales/vi/chat.json index e9b1410e36..9c138507ad 100644 --- a/webview-ui/src/i18n/locales/vi/chat.json +++ b/webview-ui/src/i18n/locales/vi/chat.json @@ -111,6 +111,8 @@ }, "selectMode": "Chọn chế độ tương tác", "selectApiConfig": "Chọn cấu hình API", + "lockApiConfigAcrossModes": "Khóa cấu hình API cho tất cả chế độ trong workspace này", + "unlockApiConfigAcrossModes": "Cấu hình API đã bị khóa cho tất cả chế độ trong workspace này (nhấn để mở khóa)", "enhancePrompt": "Nâng cao yêu cầu với ngữ cảnh bổ sung", "addImages": "Thêm hình ảnh vào tin nhắn", "sendMessage": "Gửi tin nhắn", @@ -218,7 +220,8 @@ "wantsToViewTopLevelOutsideWorkspace": "Roo muốn xem các tệp cấp cao nhất trong thư mục này (ngoài không gian làm việc)", "didViewTopLevelOutsideWorkspace": "Roo đã xem các tệp cấp cao nhất trong thư mục này (ngoài không gian làm việc)", "wantsToViewRecursiveOutsideWorkspace": "Roo muốn xem đệ quy tất cả các tệp trong thư mục này (ngoài không gian làm việc)", - "didViewRecursiveOutsideWorkspace": "Roo đã xem đệ quy tất cả các tệp trong thư mục này (ngoài không gian làm việc)" + "didViewRecursiveOutsideWorkspace": "Roo đã xem đệ quy tất cả các tệp trong thư mục này (ngoài không gian làm việc)", + "wantsToViewMultipleDirectories": "Roo muốn xem nhiều thư mục" }, "commandOutput": "Kết quả lệnh", "commandExecution": { @@ -438,6 +441,22 @@ "title": "Từ chối tất cả" } }, + "list-batch": { + "approve": { + "title": "Chấp nhận tất cả" + }, + "deny": { + "title": "Từ chối tất cả" + } + }, + "edit-batch": { + "approve": { + "title": "Lưu tất cả" + }, + "deny": { + "title": "Từ chối tất cả" + } + }, "indexingStatus": { "ready": "Chỉ mục sẵn sàng", "indexing": "Đang lập chỉ mục {{percentage}}%", diff --git a/webview-ui/src/i18n/locales/zh-CN/chat.json b/webview-ui/src/i18n/locales/zh-CN/chat.json index 5b115a5b84..0dda18d9ce 100644 --- a/webview-ui/src/i18n/locales/zh-CN/chat.json +++ b/webview-ui/src/i18n/locales/zh-CN/chat.json @@ -111,6 +111,8 @@ }, "selectMode": "选择交互模式", "selectApiConfig": "选择 API 配置", + "lockApiConfigAcrossModes": "锁定此工作区所有模式的 API 配置", + "unlockApiConfigAcrossModes": "此工作区所有模式的 API 配置已锁定(点击解锁)", "enhancePrompt": "增强提示词", "addImages": "添加图片到消息", "sendMessage": "发送消息", @@ -218,7 +220,8 @@ "wantsToViewTopLevelOutsideWorkspace": "需要查看目录文件列表(工作区外)", "didViewTopLevelOutsideWorkspace": "已查看目录文件列表(工作区外)", "wantsToViewRecursiveOutsideWorkspace": "需要查看目录所有文件(工作区外)", - "didViewRecursiveOutsideWorkspace": "已查看目录所有文件(工作区外)" + "didViewRecursiveOutsideWorkspace": "已查看目录所有文件(工作区外)", + "wantsToViewMultipleDirectories": "Roo 想要查看多个目录" }, "commandOutput": "命令输出", "commandExecution": { @@ -438,6 +441,22 @@ "title": "全部拒绝" } }, + "list-batch": { + "approve": { + "title": "全部批准" + }, + "deny": { + "title": "全部拒绝" + } + }, + "edit-batch": { + "approve": { + "title": "全部保存" + }, + "deny": { + "title": "全部拒绝" + } + }, "indexingStatus": { "ready": "索引就绪", "indexing": "索引中 {{percentage}}%", diff --git a/webview-ui/src/i18n/locales/zh-TW/chat.json b/webview-ui/src/i18n/locales/zh-TW/chat.json index db54a6b3ad..9975a1b377 100644 --- a/webview-ui/src/i18n/locales/zh-TW/chat.json +++ b/webview-ui/src/i18n/locales/zh-TW/chat.json @@ -74,6 +74,22 @@ "title": "全部拒絕" } }, + "list-batch": { + "approve": { + "title": "全部核准" + }, + "deny": { + "title": "全部拒絕" + } + }, + "edit-batch": { + "approve": { + "title": "全部儲存" + }, + "deny": { + "title": "全部拒絕" + } + }, "runCommand": { "title": "執行", "tooltip": "執行此命令" @@ -122,6 +138,8 @@ }, "selectMode": "選擇互動模式", "selectApiConfig": "選取 API 設定", + "lockApiConfigAcrossModes": "鎖定此工作區所有模式的 API 設定", + "unlockApiConfigAcrossModes": "此工作區所有模式的 API 設定已鎖定(點擊解鎖)", "enhancePrompt": "使用額外內容強化提示詞", "modeSelector": { "title": "模式", @@ -156,14 +174,14 @@ "rateLimitWait": "速率限制", "errorTitle": "供應商錯誤 {{code}}", "errorMessage": { - "docs": "說明文件", - "goToSettings": "設定", "400": "供應商無法按照此方式處理請求。請停止工作並嘗試其他方法。", "401": "無法向供應商進行身份驗證。請檢查您的 API 金鑰設定。", "402": "您的帳戶資金/額度似乎已用盡。請前往供應商增加額度以繼續。", "403": "無權存取。您的 API 金鑰有效,但供應商拒絕完成此請求。", "429": "請求次數過多。供應商已對您的請求進行速率限制。請在下一次 API 呼叫前稍候。", "500": "供應商伺服器錯誤。伺服器端發生問題,您的請求沒有問題。", + "docs": "說明文件", + "goToSettings": "設定", "connection": "連線錯誤。請確保您有可用的網際網路連線。", "unknown": "未知 API 錯誤。請聯絡 Roo Code 技術支援。", "claudeCodeNotAuthenticated": "您需要登入才能使用 Claude Code。前往設定並點選「登入 Claude Code」以進行驗證。" @@ -241,7 +259,8 @@ "wantsToSearch": "Roo 想要在此目錄中搜尋 {{regex}}", "didSearch": "Roo 已在此目錄中搜尋 {{regex}}", "wantsToSearchOutsideWorkspace": "Roo 想要在此目錄(工作區外)中搜尋 {{regex}}", - "didSearchOutsideWorkspace": "Roo 已在此目錄(工作區外)中搜尋 {{regex}}" + "didSearchOutsideWorkspace": "Roo 已在此目錄(工作區外)中搜尋 {{regex}}", + "wantsToViewMultipleDirectories": "Roo 想要查看多個目錄" }, "codebaseSearch": { "wantsToSearch": "Roo 想要在程式碼庫中搜尋 {{query}}", diff --git a/webview-ui/src/utils/__tests__/batchConsecutive.spec.ts b/webview-ui/src/utils/__tests__/batchConsecutive.spec.ts new file mode 100644 index 0000000000..b3919fdbd6 --- /dev/null +++ b/webview-ui/src/utils/__tests__/batchConsecutive.spec.ts @@ -0,0 +1,116 @@ +import { batchConsecutive } from "../batchConsecutive" + +interface TestItem { + ts: number + type: string + text: string +} + +/** Helper: create a minimal test item with an identifiable text field. */ +function msg(text: string, type = "say"): TestItem { + return { ts: Date.now(), type, text } +} + +/** Predicate: matches items whose text starts with "match". */ +const isMatch = (m: TestItem) => !!m.text?.startsWith("match") + +/** Synthesize: merges a batch into a single item with a "BATCH:" marker. */ +const synthesizeBatch = (batch: TestItem[]): TestItem => ({ + ...batch[0], + text: `BATCH:${batch.map((m) => m.text).join(",")}`, +}) + +describe("batchConsecutive", () => { + test("empty input returns empty output", () => { + expect(batchConsecutive([], isMatch, synthesizeBatch)).toEqual([]) + }) + + test("no matches returns passthrough", () => { + const messages = [msg("a"), msg("b"), msg("c")] + const result = batchConsecutive(messages, isMatch, synthesizeBatch) + expect(result).toEqual(messages) + }) + + test("single match is passed through without batching", () => { + const messages = [msg("a"), msg("match-1"), msg("b")] + const result = batchConsecutive(messages, isMatch, synthesizeBatch) + expect(result).toHaveLength(3) + expect(result[1].text).toBe("match-1") + }) + + test("two consecutive matches produce one synthetic message", () => { + const messages = [msg("a"), msg("match-1"), msg("match-2"), msg("b")] + const result = batchConsecutive(messages, isMatch, synthesizeBatch) + expect(result).toHaveLength(3) + expect(result[0].text).toBe("a") + expect(result[1].text).toBe("BATCH:match-1,match-2") + expect(result[2].text).toBe("b") + }) + + test("non-consecutive matches are not batched", () => { + const messages = [msg("match-1"), msg("other"), msg("match-2")] + const result = batchConsecutive(messages, isMatch, synthesizeBatch) + expect(result).toHaveLength(3) + expect(result[0].text).toBe("match-1") + expect(result[1].text).toBe("other") + expect(result[2].text).toBe("match-2") + }) + + test("mixed sequences are correctly interleaved", () => { + const messages = [ + msg("match-1"), + msg("match-2"), + msg("match-3"), + msg("other-1"), + msg("match-4"), + msg("other-2"), + msg("match-5"), + msg("match-6"), + ] + const result = batchConsecutive(messages, isMatch, synthesizeBatch) + expect(result).toHaveLength(5) + expect(result[0].text).toBe("BATCH:match-1,match-2,match-3") + expect(result[1].text).toBe("other-1") + expect(result[2].text).toBe("match-4") // single — not batched + expect(result[3].text).toBe("other-2") + expect(result[4].text).toBe("BATCH:match-5,match-6") + }) + + test("all items match → single synthetic message", () => { + const items = [msg("match-1"), msg("match-2"), msg("match-3")] + const result = batchConsecutive(items, isMatch, synthesizeBatch) + expect(result).toHaveLength(1) + expect(result[0].text).toBe("BATCH:match-1,match-2,match-3") + }) + + test("does not mutate the input array", () => { + const items = [msg("match-1"), msg("match-2")] + const original = [...items] + batchConsecutive(items, isMatch, synthesizeBatch) + expect(items).toHaveLength(2) + expect(items).toEqual(original) + }) + + test("returns a new array, not the same reference", () => { + const items = [msg("a"), msg("b")] + const result = batchConsecutive(items, isMatch, synthesizeBatch) + expect(result).not.toBe(items) + }) + + test("synthesize callback receives the correct batches", () => { + const spy = vi.fn(synthesizeBatch) + const items = [msg("match-1"), msg("match-2"), msg("other"), msg("match-3"), msg("match-4")] + batchConsecutive(items, isMatch, spy) + expect(spy).toHaveBeenCalledTimes(2) + expect(spy.mock.calls[0][0]).toHaveLength(2) + expect(spy.mock.calls[1][0]).toHaveLength(2) + }) + + test("batch at the end of the array", () => { + const items = [msg("other"), msg("match-1"), msg("match-2")] + const result = batchConsecutive(items, isMatch, synthesizeBatch) + expect(result).toHaveLength(2) + expect(result[0].text).toBe("other") + expect(result[1].text).toBe("BATCH:match-1,match-2") + }) +}) diff --git a/webview-ui/src/utils/batchConsecutive.ts b/webview-ui/src/utils/batchConsecutive.ts new file mode 100644 index 0000000000..336d8a74a6 --- /dev/null +++ b/webview-ui/src/utils/batchConsecutive.ts @@ -0,0 +1,38 @@ +/** + * Walk an item array and batch runs of consecutive items that match + * `predicate` into synthetic items produced by `synthesize`. + * + * - Runs of length 1 are passed through unchanged. + * - Runs of length >= 2 are replaced by a single synthetic item. + * - Non-matching items are preserved in-order. + */ +export function batchConsecutive(items: T[], predicate: (item: T) => boolean, synthesize: (batch: T[]) => T): T[] { + const result: T[] = [] + let i = 0 + + while (i < items.length) { + if (predicate(items[i])) { + // Collect consecutive matches into a batch + const batch: T[] = [items[i]] + let j = i + 1 + + while (j < items.length && predicate(items[j])) { + batch.push(items[j]) + j++ + } + + if (batch.length > 1) { + result.push(synthesize(batch)) + } else { + result.push(batch[0]) + } + + i = j + } else { + result.push(items[i]) + i++ + } + } + + return result +}